apricot 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +3 -0
- data/.rspec +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +7 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +26 -0
- data/README.md +90 -0
- data/Rakefile +9 -0
- data/apricot.gemspec +22 -0
- data/bin/apricot +58 -0
- data/examples/bot.apr +23 -0
- data/examples/cinch-bot.apr +12 -0
- data/examples/hanoi.apr +10 -0
- data/examples/hello.apr +1 -0
- data/examples/plot.apr +28 -0
- data/examples/quine.apr +1 -0
- data/kernel/core.apr +928 -0
- data/lib/apricot/ast/identifier.rb +111 -0
- data/lib/apricot/ast/list.rb +99 -0
- data/lib/apricot/ast/literals.rb +240 -0
- data/lib/apricot/ast/node.rb +45 -0
- data/lib/apricot/ast/scopes.rb +147 -0
- data/lib/apricot/ast/toplevel.rb +66 -0
- data/lib/apricot/ast/variables.rb +64 -0
- data/lib/apricot/ast.rb +3 -0
- data/lib/apricot/compiler.rb +55 -0
- data/lib/apricot/cons.rb +27 -0
- data/lib/apricot/errors.rb +38 -0
- data/lib/apricot/generator.rb +15 -0
- data/lib/apricot/identifier.rb +91 -0
- data/lib/apricot/list.rb +96 -0
- data/lib/apricot/macroexpand.rb +47 -0
- data/lib/apricot/misc.rb +11 -0
- data/lib/apricot/namespace.rb +59 -0
- data/lib/apricot/parser.rb +541 -0
- data/lib/apricot/printers.rb +12 -0
- data/lib/apricot/repl.rb +254 -0
- data/lib/apricot/ruby_ext.rb +254 -0
- data/lib/apricot/seq.rb +44 -0
- data/lib/apricot/special_forms.rb +735 -0
- data/lib/apricot/stages.rb +60 -0
- data/lib/apricot/version.rb +3 -0
- data/lib/apricot.rb +30 -0
- data/spec/compiler_spec.rb +499 -0
- data/spec/identifier_spec.rb +58 -0
- data/spec/list_spec.rb +96 -0
- data/spec/parser_spec.rb +312 -0
- data/spec/spec_helper.rb +10 -0
- metadata +188 -0
data/lib/apricot/repl.rb
ADDED
@@ -0,0 +1,254 @@
|
|
1
|
+
require 'readline'
|
2
|
+
require 'yaml'
|
3
|
+
|
4
|
+
module Apricot
|
5
|
+
class REPL
|
6
|
+
# TODO: make history more configurable
|
7
|
+
HISTORY_FILE = "~/.apricot-history"
|
8
|
+
MAX_HISTORY_LINES = 1000
|
9
|
+
|
10
|
+
COMMANDS = {
|
11
|
+
"!backtrace" => {
|
12
|
+
doc: "Print the backtrace of the most recent exception",
|
13
|
+
code: proc do
|
14
|
+
puts (@exception ? @exception.awesome_backtrace : "No backtrace")
|
15
|
+
end
|
16
|
+
},
|
17
|
+
|
18
|
+
"!bytecode" => {
|
19
|
+
doc: "Print the bytecode generated from the previous line",
|
20
|
+
code: proc do
|
21
|
+
puts (@compiled_code ? @compiled_code.decode : "No previous line")
|
22
|
+
end
|
23
|
+
},
|
24
|
+
|
25
|
+
"!exit" => {doc: "Exit the REPL", code: proc { exit }},
|
26
|
+
|
27
|
+
"!help" => {
|
28
|
+
doc: "Print this message",
|
29
|
+
code: proc do
|
30
|
+
width = 14
|
31
|
+
|
32
|
+
puts "(doc foo)".ljust(width) +
|
33
|
+
"Print the documentation for a function or macro"
|
34
|
+
COMMANDS.sort.each {|name, c| puts name.ljust(width) + c[:doc] }
|
35
|
+
end
|
36
|
+
}
|
37
|
+
}
|
38
|
+
|
39
|
+
COMMAND_COMPLETIONS = COMMANDS.keys.sort
|
40
|
+
SPECIAL_COMPLETIONS = SpecialForm::Specials.keys.map(&:to_s)
|
41
|
+
|
42
|
+
def initialize(prompt = 'apr> ', bytecode = false, history_file = nil)
|
43
|
+
@prompt = prompt
|
44
|
+
@bytecode = bytecode
|
45
|
+
@history_file = File.expand_path(history_file || HISTORY_FILE)
|
46
|
+
@line = 1
|
47
|
+
end
|
48
|
+
|
49
|
+
def run
|
50
|
+
Readline.completion_append_character = " "
|
51
|
+
Readline.basic_word_break_characters = " \t\n\"'`~@;{[("
|
52
|
+
Readline.basic_quote_characters = "\""
|
53
|
+
|
54
|
+
Readline.completion_proc = proc do |s|
|
55
|
+
if s.start_with? '!'
|
56
|
+
# User is typing a REPL command
|
57
|
+
COMMAND_COMPLETIONS.select {|c| c.start_with? s }
|
58
|
+
elsif ('A'..'Z').include? s[0]
|
59
|
+
# User is typing a constant
|
60
|
+
constant_completion(s)
|
61
|
+
else
|
62
|
+
# User is typing a regular name
|
63
|
+
comps = SPECIAL_COMPLETIONS +
|
64
|
+
Apricot.current_namespace.vars.keys.map(&:to_s)
|
65
|
+
comps.select {|c| c.start_with? s }.sort
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
load_history
|
70
|
+
terminal_state = `stty -g`.chomp
|
71
|
+
|
72
|
+
while code = readline_with_history
|
73
|
+
stripped = code.strip
|
74
|
+
if stripped.empty?
|
75
|
+
next
|
76
|
+
elsif stripped.start_with?('!')
|
77
|
+
if COMMANDS.include?(stripped) && block = COMMANDS[stripped][:code]
|
78
|
+
instance_eval(&block)
|
79
|
+
else
|
80
|
+
puts "Unknown command: #{stripped}"
|
81
|
+
end
|
82
|
+
next
|
83
|
+
end
|
84
|
+
|
85
|
+
begin
|
86
|
+
@compiled_code =
|
87
|
+
Apricot::Compiler.compile_string(code, "(eval)", @line, @bytecode)
|
88
|
+
value = Rubinius.run_script @compiled_code
|
89
|
+
puts "=> #{value.apricot_inspect}"
|
90
|
+
Apricot.current_namespace.set_var(:_, value)
|
91
|
+
e = nil
|
92
|
+
rescue Apricot::SyntaxError => e
|
93
|
+
if e.incomplete?
|
94
|
+
begin
|
95
|
+
more_code = Readline.readline(' ' * @prompt.length, false)
|
96
|
+
if more_code
|
97
|
+
code << "\n" << more_code
|
98
|
+
Readline::HISTORY << Readline::HISTORY.pop + "\n" +
|
99
|
+
' ' * @prompt.length + more_code
|
100
|
+
retry
|
101
|
+
else
|
102
|
+
print "\r" # print the exception at the start of the line
|
103
|
+
end
|
104
|
+
rescue Interrupt
|
105
|
+
# This is raised by Ctrl-C. Stop trying to read more code and
|
106
|
+
# just give up. Remove the current input from history.
|
107
|
+
current_code = Readline::HISTORY.pop
|
108
|
+
@line -= current_code.count "\n"
|
109
|
+
e = nil # ignore the syntax error since the code was Ctrl-C'd
|
110
|
+
end
|
111
|
+
end
|
112
|
+
rescue Interrupt => e
|
113
|
+
# Raised by Ctrl-C. Print a newline so the error message is on the
|
114
|
+
# next line.
|
115
|
+
puts
|
116
|
+
rescue SystemExit, SignalException
|
117
|
+
raise
|
118
|
+
rescue Exception => e
|
119
|
+
end
|
120
|
+
|
121
|
+
if e
|
122
|
+
@exception = e
|
123
|
+
puts "#{e.class}: #{e.message}"
|
124
|
+
end
|
125
|
+
|
126
|
+
@line += 1 + code.count("\n")
|
127
|
+
end
|
128
|
+
|
129
|
+
puts # Print a newline after Ctrl-D (EOF)
|
130
|
+
|
131
|
+
ensure
|
132
|
+
save_history
|
133
|
+
system('stty', terminal_state) if terminal_state # Restore the terminal
|
134
|
+
end
|
135
|
+
|
136
|
+
def load_history
|
137
|
+
if File.exist?(@history_file)
|
138
|
+
hist = YAML.load_file @history_file
|
139
|
+
|
140
|
+
if hist.is_a? Array
|
141
|
+
hist.each {|x| Readline::HISTORY << x }
|
142
|
+
else
|
143
|
+
File.open(@history_file) do |f|
|
144
|
+
f.each {|line| Readline::HISTORY << line.chomp }
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def save_history
|
151
|
+
return if Readline::HISTORY.empty?
|
152
|
+
|
153
|
+
File.open(@history_file, "w") do |f|
|
154
|
+
hist = Readline::HISTORY.to_a
|
155
|
+
hist.shift(hist.size - MAX_HISTORY_LINES) if hist.size > MAX_HISTORY_LINES
|
156
|
+
YAML.dump(hist, f, header: true)
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Smarter Readline to prevent empty and dups
|
161
|
+
# 1. Read a line and append to history
|
162
|
+
# 2. Quick Break on nil
|
163
|
+
# 3. Remove from history if empty or dup
|
164
|
+
def readline_with_history
|
165
|
+
line = Readline.readline(@prompt, true)
|
166
|
+
return nil if line.nil?
|
167
|
+
|
168
|
+
if line =~ /\A\s*\z/ || (Readline::HISTORY.size > 1 &&
|
169
|
+
Readline::HISTORY[-2] == line)
|
170
|
+
Readline::HISTORY.pop
|
171
|
+
end
|
172
|
+
|
173
|
+
line
|
174
|
+
rescue Interrupt
|
175
|
+
# This is raised by Ctrl-C. Remove the line from history then try to
|
176
|
+
# read another line.
|
177
|
+
puts "^C"
|
178
|
+
Readline::HISTORY.pop
|
179
|
+
@line -= 1
|
180
|
+
retry
|
181
|
+
end
|
182
|
+
|
183
|
+
# Find constant Foo::Bar::Baz from ["Foo", "Bar", "Baz"] array. Helper for
|
184
|
+
# tab-completion of constants.
|
185
|
+
def find_constant(const_names)
|
186
|
+
const_names.reduce(Object) do |mod, name|
|
187
|
+
mod.const_get(name)
|
188
|
+
end
|
189
|
+
rescue NameError
|
190
|
+
# Return nil if the constant doesn't exist.
|
191
|
+
nil
|
192
|
+
end
|
193
|
+
|
194
|
+
# Tab-completion for constants and namespaced identifiers
|
195
|
+
def constant_completion(s)
|
196
|
+
# Split Foo/bar into Foo and bar. If there is no / then id will be nil.
|
197
|
+
constant_str, id = s.split('/', 2)
|
198
|
+
|
199
|
+
# If we have a Foo/bar case, complete the 'bar' part if possible.
|
200
|
+
if id
|
201
|
+
# Split with -1 returns an extra empty string if constant_str ends in
|
202
|
+
# '::'. Then it will fail to find the constant for Foo::/ and we won't
|
203
|
+
# try completing Foo::/ to Foo/whatever.
|
204
|
+
const_names = constant_str.split('::', -1)
|
205
|
+
|
206
|
+
const = find_constant(const_names)
|
207
|
+
|
208
|
+
# If we can't find the constant the user is typing, don't return any
|
209
|
+
# completions. If it isn't a Module or Namespace (subclass of Module),
|
210
|
+
# we can't complete methods or vars below it. (e.g. in Math::PI/<tab>
|
211
|
+
# we can't do any completions)
|
212
|
+
return [] unless const && const.is_a?(Module)
|
213
|
+
|
214
|
+
# Complete the vars of the namespace or the methods of the module.
|
215
|
+
potential_completions =
|
216
|
+
const.is_a?(Apricot::Namespace) ? const.vars.keys : const.methods
|
217
|
+
|
218
|
+
# Select the matching vars or methods and format them properly as
|
219
|
+
# completions.
|
220
|
+
potential_completions.select do |c|
|
221
|
+
c.to_s.start_with? id
|
222
|
+
end.sort.map do |c|
|
223
|
+
"#{constant_str}/#{c}"
|
224
|
+
end
|
225
|
+
|
226
|
+
# Otherwise there is no / and we complete constant names.
|
227
|
+
else
|
228
|
+
# Split with -1 returns an extra empty string if constant_str ends in
|
229
|
+
# '::'. This allows us to differentiate Foo:: and Foo cases.
|
230
|
+
const_names = constant_str.split('::', -1)
|
231
|
+
curr_name = const_names.pop # The user is currently typing the last name.
|
232
|
+
|
233
|
+
const = find_constant(const_names)
|
234
|
+
|
235
|
+
# If we can't find the constant the user is typing, don't return any
|
236
|
+
# completions. If it isn't a Module, we can't complete constants below
|
237
|
+
# it. (e.g. in Math::PI::<tab> we can't do anything)
|
238
|
+
return [] unless const && const.is_a?(Module)
|
239
|
+
|
240
|
+
# Select the matching constants and format them properly as
|
241
|
+
# completions.
|
242
|
+
const.constants.select do |c|
|
243
|
+
c.to_s.start_with? curr_name
|
244
|
+
end.sort.map do |name|
|
245
|
+
if const_names.size == 0
|
246
|
+
name.to_s
|
247
|
+
else
|
248
|
+
"#{const_names.join('::')}::#{name}"
|
249
|
+
end
|
250
|
+
end
|
251
|
+
end
|
252
|
+
end
|
253
|
+
end
|
254
|
+
end
|
@@ -0,0 +1,254 @@
|
|
1
|
+
class Object
|
2
|
+
attr_accessor :apricot_meta
|
3
|
+
|
4
|
+
def apricot_inspect
|
5
|
+
inspect
|
6
|
+
end
|
7
|
+
|
8
|
+
def apricot_str
|
9
|
+
to_s
|
10
|
+
end
|
11
|
+
|
12
|
+
def apricot_call(*args)
|
13
|
+
call(*args)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class Array
|
18
|
+
# Adapted from Array#inspect. This version prints no commas and calls
|
19
|
+
# #apricot_inspect on its elements. e.g. [1 2 3]
|
20
|
+
def apricot_inspect
|
21
|
+
return '[]' if size == 0
|
22
|
+
|
23
|
+
str = '['
|
24
|
+
|
25
|
+
return '[...]' if Thread.detect_recursion self do
|
26
|
+
each {|x| str << x.apricot_inspect << ' ' }
|
27
|
+
end
|
28
|
+
|
29
|
+
str.chop!
|
30
|
+
str << ']'
|
31
|
+
end
|
32
|
+
|
33
|
+
def apricot_call(idx)
|
34
|
+
self[idx]
|
35
|
+
end
|
36
|
+
|
37
|
+
alias_method :apricot_str, :apricot_inspect
|
38
|
+
|
39
|
+
def to_seq
|
40
|
+
if length == 0
|
41
|
+
nil
|
42
|
+
else
|
43
|
+
Seq.new(self, 0)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
class Seq
|
48
|
+
include Apricot::Seq
|
49
|
+
|
50
|
+
def initialize(array, offset = 0)
|
51
|
+
@array = array
|
52
|
+
@offset = offset
|
53
|
+
end
|
54
|
+
|
55
|
+
def first
|
56
|
+
@array[@offset]
|
57
|
+
end
|
58
|
+
|
59
|
+
def next
|
60
|
+
if @offset + 1 < @array.length
|
61
|
+
Seq.new(@array, @offset + 1)
|
62
|
+
else
|
63
|
+
nil
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def each
|
68
|
+
@array[@offset..-1].each {|x| yield x }
|
69
|
+
end
|
70
|
+
|
71
|
+
def count
|
72
|
+
@array.length - @offset
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
class Hash
|
78
|
+
# Adapted from Hash#inspect. Outputs Apricot hash syntax, e.g. {:a 1, :b 2}
|
79
|
+
def apricot_inspect
|
80
|
+
return '{}' if size == 0
|
81
|
+
|
82
|
+
str = '{'
|
83
|
+
|
84
|
+
return '{...}' if Thread.detect_recursion self do
|
85
|
+
each_item do |item|
|
86
|
+
str << item.key.apricot_inspect
|
87
|
+
str << ' '
|
88
|
+
str << item.value.apricot_inspect
|
89
|
+
str << ', '
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
str.shorten!(2)
|
94
|
+
str << '}'
|
95
|
+
end
|
96
|
+
|
97
|
+
def apricot_call(key)
|
98
|
+
self[key]
|
99
|
+
end
|
100
|
+
|
101
|
+
alias_method :apricot_str, :apricot_inspect
|
102
|
+
|
103
|
+
def to_seq
|
104
|
+
each_pair.to_a.to_seq
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Set
|
109
|
+
def apricot_inspect
|
110
|
+
return '#{}' if size == 0
|
111
|
+
|
112
|
+
str = '#{'
|
113
|
+
|
114
|
+
return '#{...}' if Thread.detect_recursion self do
|
115
|
+
each {|x| str << x.apricot_inspect << ' ' }
|
116
|
+
end
|
117
|
+
|
118
|
+
str.chop!
|
119
|
+
str << '}'
|
120
|
+
end
|
121
|
+
|
122
|
+
def [](elem)
|
123
|
+
elem if self.include? elem
|
124
|
+
end
|
125
|
+
|
126
|
+
alias_method :apricot_call, :[]
|
127
|
+
alias_method :apricot_str, :apricot_inspect
|
128
|
+
|
129
|
+
def to_seq
|
130
|
+
to_a.to_seq
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
class Rational
|
135
|
+
def apricot_inspect
|
136
|
+
if @denominator == 1
|
137
|
+
@numerator.to_s
|
138
|
+
else
|
139
|
+
to_s
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
alias_method :apricot_str, :apricot_inspect
|
144
|
+
end
|
145
|
+
|
146
|
+
class Regexp
|
147
|
+
def apricot_inspect
|
148
|
+
"#r#{inspect}"
|
149
|
+
end
|
150
|
+
|
151
|
+
alias_method :apricot_str, :apricot_inspect
|
152
|
+
end
|
153
|
+
|
154
|
+
class Symbol
|
155
|
+
def apricot_inspect
|
156
|
+
str = to_s
|
157
|
+
|
158
|
+
if str =~ /\A#{Apricot::Parser::IDENTIFIER}+\z/
|
159
|
+
":#{str}"
|
160
|
+
else
|
161
|
+
":#{str.inspect}"
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def apricot_call(o)
|
166
|
+
o[self]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
class Range
|
171
|
+
def to_seq
|
172
|
+
if first > last || (first == last && exclude_end?)
|
173
|
+
nil
|
174
|
+
else
|
175
|
+
Seq.new(first, last, exclude_end?)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
class Seq
|
180
|
+
include Apricot::Seq
|
181
|
+
|
182
|
+
def initialize(first, last, exclude)
|
183
|
+
@first = first
|
184
|
+
@last = last
|
185
|
+
@exclude = exclude
|
186
|
+
end
|
187
|
+
|
188
|
+
def first
|
189
|
+
@first
|
190
|
+
end
|
191
|
+
|
192
|
+
def next
|
193
|
+
next_val = @first.succ
|
194
|
+
|
195
|
+
if @first == @last || (next_val == @last && @exclude)
|
196
|
+
nil
|
197
|
+
else
|
198
|
+
Seq.new(next_val, @last, @exclude)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
|
202
|
+
def each
|
203
|
+
prev = nil
|
204
|
+
val = @first
|
205
|
+
|
206
|
+
until prev == @last || (val == @last && @exclude)
|
207
|
+
yield val
|
208
|
+
prev = val
|
209
|
+
val = val.succ
|
210
|
+
end
|
211
|
+
|
212
|
+
self
|
213
|
+
end
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
module Enumerable
|
218
|
+
def to_list
|
219
|
+
list = Apricot::List::EmptyList
|
220
|
+
reverse_each {|x| list = list.cons(x) }
|
221
|
+
list
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
class NilClass
|
226
|
+
include Enumerable
|
227
|
+
|
228
|
+
def each
|
229
|
+
end
|
230
|
+
|
231
|
+
def empty?
|
232
|
+
true
|
233
|
+
end
|
234
|
+
|
235
|
+
# Seq Methods
|
236
|
+
# Many functions that return seqs occasionally return nil, so it's
|
237
|
+
# convenient if nil can respond to some of the same methods as seqs.
|
238
|
+
|
239
|
+
def to_seq
|
240
|
+
nil
|
241
|
+
end
|
242
|
+
|
243
|
+
def first
|
244
|
+
nil
|
245
|
+
end
|
246
|
+
|
247
|
+
def next
|
248
|
+
nil
|
249
|
+
end
|
250
|
+
|
251
|
+
def rest
|
252
|
+
Apricot::List::EmptyList
|
253
|
+
end
|
254
|
+
end
|
data/lib/apricot/seq.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
module Apricot
|
2
|
+
# Every seq should include this module and define 'first' and 'next'
|
3
|
+
# methods. A seq may redefine 'rest' and 'each' if there is a more efficient
|
4
|
+
# way to implement them.
|
5
|
+
#
|
6
|
+
# 'first' should return the first item in the seq.
|
7
|
+
# 'next' should return a seq of the rest of the items in the seq, or nil
|
8
|
+
# if there are no more items.
|
9
|
+
module Seq
|
10
|
+
include Enumerable
|
11
|
+
|
12
|
+
def rest
|
13
|
+
self.next || Apricot::List::EmptyList
|
14
|
+
end
|
15
|
+
|
16
|
+
def each
|
17
|
+
s = self
|
18
|
+
|
19
|
+
while s
|
20
|
+
yield s.first
|
21
|
+
s = s.next
|
22
|
+
end
|
23
|
+
|
24
|
+
self
|
25
|
+
end
|
26
|
+
|
27
|
+
def to_seq
|
28
|
+
self
|
29
|
+
end
|
30
|
+
|
31
|
+
def empty?
|
32
|
+
false
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
str = '('
|
37
|
+
each {|x| str << x.apricot_inspect << ' ' }
|
38
|
+
str.chop!
|
39
|
+
str << ')'
|
40
|
+
end
|
41
|
+
|
42
|
+
alias_method :inspect, :to_s
|
43
|
+
end
|
44
|
+
end
|