apricot 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|