apricot 0.0.1 → 0.0.2
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.
- checksums.yaml +7 -0
- data/.gitignore +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +1 -0
- data/Gemfile.lock +229 -11
- data/README.md +46 -29
- data/Rakefile +1 -1
- data/apricot.gemspec +7 -3
- data/benchmarks/factorial.rb +51 -0
- data/benchmarks/interpolate.rb +20 -0
- data/bin/apricot +5 -23
- data/examples/bot.apr +1 -4
- data/examples/cinch-bot.apr +3 -3
- data/examples/sinatra.apr +9 -0
- data/kernel/core.apr +124 -75
- data/kernel/repl.apr +37 -0
- data/lib/apricot.rb +7 -26
- data/lib/apricot/boot.rb +24 -0
- data/lib/apricot/code_loader.rb +108 -0
- data/lib/apricot/compiler.rb +265 -32
- data/lib/apricot/generator.rb +10 -3
- data/lib/apricot/identifier.rb +25 -10
- data/lib/apricot/list.rb +28 -41
- data/lib/apricot/macroexpand.rb +14 -8
- data/lib/apricot/misc.rb +2 -1
- data/lib/apricot/namespace.rb +20 -3
- data/lib/apricot/{parser.rb → reader.rb} +221 -194
- data/lib/apricot/repl.rb +67 -24
- data/lib/apricot/ruby_ext.rb +27 -16
- data/lib/apricot/scopes.rb +159 -0
- data/lib/apricot/seq.rb +43 -1
- data/lib/apricot/special_forms.rb +16 -695
- data/lib/apricot/special_forms/def.rb +32 -0
- data/lib/apricot/special_forms/do.rb +23 -0
- data/lib/apricot/special_forms/dot.rb +112 -0
- data/lib/apricot/special_forms/fn.rb +342 -0
- data/lib/apricot/special_forms/if.rb +31 -0
- data/lib/apricot/special_forms/let.rb +8 -0
- data/lib/apricot/special_forms/loop.rb +10 -0
- data/lib/apricot/special_forms/quote.rb +9 -0
- data/lib/apricot/special_forms/recur.rb +26 -0
- data/lib/apricot/special_forms/try.rb +146 -0
- data/lib/apricot/variables.rb +65 -0
- data/lib/apricot/version.rb +1 -1
- data/spec/compiler_spec.rb +53 -450
- data/spec/fn_spec.rb +206 -0
- data/spec/list_spec.rb +1 -1
- data/spec/reader_spec.rb +349 -0
- data/spec/spec_helper.rb +40 -4
- data/spec/special_forms_spec.rb +203 -0
- metadata +99 -133
- data/lib/apricot/ast.rb +0 -3
- data/lib/apricot/ast/identifier.rb +0 -111
- data/lib/apricot/ast/list.rb +0 -99
- data/lib/apricot/ast/literals.rb +0 -240
- data/lib/apricot/ast/node.rb +0 -45
- data/lib/apricot/ast/scopes.rb +0 -147
- data/lib/apricot/ast/toplevel.rb +0 -66
- data/lib/apricot/ast/variables.rb +0 -64
- data/lib/apricot/printers.rb +0 -12
- data/lib/apricot/stages.rb +0 -60
- data/spec/parser_spec.rb +0 -312
data/lib/apricot/boot.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
module Apricot
|
2
|
+
LIB_PATH = File.expand_path("../../../kernel", __FILE__)
|
3
|
+
$LOAD_PATH << LIB_PATH
|
4
|
+
|
5
|
+
Core = Namespace.new
|
6
|
+
|
7
|
+
Core.set_var(:"*ns*", Core)
|
8
|
+
|
9
|
+
Core.set_var(:"in-ns", lambda do |constant|
|
10
|
+
Apricot.current_namespace = Namespace.find_or_create constant
|
11
|
+
end)
|
12
|
+
|
13
|
+
Core.set_var(:ns, lambda do |constant|
|
14
|
+
List[Identifier.intern(:"in-ns"),
|
15
|
+
List[Identifier.intern(:quote), constant]]
|
16
|
+
end)
|
17
|
+
Core.get_var(:ns).apricot_meta = {:macro => true}
|
18
|
+
|
19
|
+
Apricot.require "core"
|
20
|
+
|
21
|
+
# ::User = Namespace.new
|
22
|
+
Apricot.current_namespace = Core
|
23
|
+
# TODO: make Apricot::Core public vars visible in User, default to User
|
24
|
+
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
module Apricot
|
2
|
+
def self.load(file)
|
3
|
+
CodeLoader.load(file)
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.require(file)
|
7
|
+
CodeLoader.require(file)
|
8
|
+
end
|
9
|
+
|
10
|
+
module CodeLoader
|
11
|
+
module_function
|
12
|
+
|
13
|
+
def load(path)
|
14
|
+
full_path = find_source(path)
|
15
|
+
raise LoadError, "no such file to load -- #{path}" unless full_path
|
16
|
+
|
17
|
+
load_file full_path
|
18
|
+
true
|
19
|
+
end
|
20
|
+
|
21
|
+
def require(path)
|
22
|
+
full_path = find_source(path)
|
23
|
+
raise LoadError, "no such file to load -- #{path}" unless full_path
|
24
|
+
|
25
|
+
if loaded? full_path
|
26
|
+
false
|
27
|
+
else
|
28
|
+
load_file full_path
|
29
|
+
$LOADED_FEATURES << full_path
|
30
|
+
true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def load_file(path)
|
35
|
+
compiled_name = Rubinius::ToolSet::Runtime::Compiler.compiled_name(path)
|
36
|
+
|
37
|
+
stat = File::Stat.stat path
|
38
|
+
compiled_stat = File::Stat.stat compiled_name
|
39
|
+
|
40
|
+
# Try to load the cached bytecode if it exists and is newer than the
|
41
|
+
# source file.
|
42
|
+
if stat && compiled_stat && stat.mtime < compiled_stat.mtime
|
43
|
+
begin
|
44
|
+
code = Rubinius.invoke_primitive :compiledfile_load, compiled_name,
|
45
|
+
Rubinius::Signature, Rubinius::RUBY_LIB_VERSION
|
46
|
+
usable = true
|
47
|
+
rescue Rubinius::Internal
|
48
|
+
usable = false
|
49
|
+
end
|
50
|
+
|
51
|
+
if usable
|
52
|
+
Rubinius.run_script code
|
53
|
+
return
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
Compiler.compile_and_eval_file path
|
58
|
+
end
|
59
|
+
|
60
|
+
def find_source(path)
|
61
|
+
path += ".apr" unless has_extension? path
|
62
|
+
path = File.expand_path path if home_path? path
|
63
|
+
|
64
|
+
if qualified_path? path
|
65
|
+
if loadable? path
|
66
|
+
path
|
67
|
+
else
|
68
|
+
false
|
69
|
+
end
|
70
|
+
else
|
71
|
+
search_load_path path
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def search_load_path(path)
|
76
|
+
$LOAD_PATH.each do |dir|
|
77
|
+
full_path = "#{dir}/#{path}"
|
78
|
+
return full_path if loadable? full_path
|
79
|
+
end
|
80
|
+
|
81
|
+
false
|
82
|
+
end
|
83
|
+
|
84
|
+
def has_extension?(path)
|
85
|
+
!File.extname(path).empty?
|
86
|
+
end
|
87
|
+
|
88
|
+
def home_path?(path)
|
89
|
+
path[0] == '~'
|
90
|
+
end
|
91
|
+
|
92
|
+
def qualified_path?(path)
|
93
|
+
# TODO: fix for Windows
|
94
|
+
path[0] == '/' || path.prefix?("./") || path.prefix?("../")
|
95
|
+
end
|
96
|
+
|
97
|
+
# Returns true if the path exists, is a regular file, and is readable.
|
98
|
+
def loadable?(path)
|
99
|
+
@stat = File::Stat.stat path
|
100
|
+
return false unless @stat
|
101
|
+
@stat.file? and @stat.readable?
|
102
|
+
end
|
103
|
+
|
104
|
+
def loaded?(path)
|
105
|
+
$LOADED_FEATURES.include?(path)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
data/lib/apricot/compiler.rb
CHANGED
@@ -1,55 +1,288 @@
|
|
1
1
|
module Apricot
|
2
|
-
|
3
|
-
|
4
|
-
compiler = new :apricot_file, :compiled_file
|
2
|
+
module Compiler
|
3
|
+
module_function
|
5
4
|
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
def generate(forms, file = "(none)", line = 1, evaluate = false)
|
6
|
+
g = Generator.new
|
7
|
+
g.name = :__apricot__
|
8
|
+
g.file = file.to_sym
|
9
9
|
|
10
|
-
|
11
|
-
|
10
|
+
g.scopes << TopLevelScope.new
|
11
|
+
|
12
|
+
g.set_line(line)
|
13
|
+
|
14
|
+
if forms.empty?
|
15
|
+
g.push_nil
|
16
|
+
else
|
17
|
+
forms.each_with_index do |e, i|
|
18
|
+
g.pop unless i == 0
|
19
|
+
bytecode(g, e)
|
20
|
+
|
21
|
+
# We evaluate top level forms as we generate the bytecode for them
|
22
|
+
# so macros can be used immediately after their definitions.
|
23
|
+
eval_form(e, file) if evaluate
|
24
|
+
end
|
25
|
+
end
|
12
26
|
|
13
|
-
|
14
|
-
compiler = new :apricot_string, :compiled_method
|
27
|
+
g.ret
|
15
28
|
|
16
|
-
|
17
|
-
|
29
|
+
scope = g.scopes.pop
|
30
|
+
g.local_count = scope.local_count
|
31
|
+
g.local_names = scope.local_names
|
18
32
|
|
19
|
-
|
33
|
+
g.close
|
34
|
+
g.encode
|
35
|
+
cc = g.package(Rubinius::CompiledCode)
|
36
|
+
cc.scope = Rubinius::ConstantScope.new(Object)
|
37
|
+
cc
|
20
38
|
end
|
21
39
|
|
22
|
-
def
|
23
|
-
|
40
|
+
def compile_and_eval_file(file)
|
41
|
+
cc = generate(Reader.read_file(file), file, 1, true)
|
42
|
+
|
43
|
+
if Rubinius::CodeLoader.save_compiled?
|
44
|
+
compiled_name = Rubinius::ToolSet::Runtime::Compiler.compiled_name(file)
|
24
45
|
|
25
|
-
|
26
|
-
compiler.packager.print(BytecodePrinter) if debug
|
46
|
+
dir = File.dirname(compiled_name)
|
27
47
|
|
28
|
-
|
48
|
+
unless File.directory?(dir)
|
49
|
+
parts = []
|
50
|
+
|
51
|
+
until dir == "/" or dir == "."
|
52
|
+
parts << dir
|
53
|
+
dir = File.dirname(dir)
|
54
|
+
end
|
55
|
+
|
56
|
+
parts.reverse_each do |d|
|
57
|
+
Dir.mkdir d unless File.directory?(d)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
Rubinius::ToolSet::Runtime::CompiledFile.dump cc, compiled_name,
|
62
|
+
Rubinius::Signature, Rubinius::RUBY_LIB_VERSION
|
63
|
+
end
|
64
|
+
|
65
|
+
cc
|
29
66
|
end
|
30
67
|
|
31
|
-
def
|
32
|
-
|
68
|
+
def compile_form(form, file = "(eval)", line = 1)
|
69
|
+
generate([form], file, line)
|
33
70
|
end
|
34
71
|
|
35
|
-
def
|
36
|
-
|
37
|
-
cc
|
72
|
+
def eval_form(form, file = "(eval)", line = 1)
|
73
|
+
Rubinius.run_script(compile_form(form, file, line))
|
38
74
|
end
|
39
75
|
|
40
|
-
def
|
41
|
-
|
42
|
-
|
76
|
+
def eval(code, file = "(eval)", line = 1)
|
77
|
+
forms = Reader.read_string(code, file,line)
|
78
|
+
|
79
|
+
return nil if forms.empty?
|
80
|
+
|
81
|
+
forms[0..-2].each do |form|
|
82
|
+
eval_form(form, file, line)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Return the result of the last form in the program.
|
86
|
+
eval_form(forms.last, file, line)
|
43
87
|
end
|
44
88
|
|
45
|
-
|
46
|
-
|
47
|
-
|
89
|
+
SELF = Identifier.intern(:self)
|
90
|
+
|
91
|
+
def bytecode(g, form, quoted = false, macroexpand = true)
|
92
|
+
pos(g, form)
|
93
|
+
|
94
|
+
case form
|
95
|
+
when Identifier
|
96
|
+
if quoted
|
97
|
+
g.push_const :Apricot
|
98
|
+
g.find_const :Identifier
|
99
|
+
g.push_literal form.name
|
100
|
+
g.send :intern, 1
|
101
|
+
else
|
102
|
+
if form.constant?
|
103
|
+
g.push_const form.const_names.first
|
104
|
+
form.const_names.drop(1).each {|n| g.find_const n }
|
105
|
+
elsif form == SELF
|
106
|
+
g.push_self
|
107
|
+
elsif form.qualified?
|
108
|
+
QualifiedReference.new(form.unqualified_name, form.qualifier).bytecode(g)
|
109
|
+
else
|
110
|
+
g.scope.find_var(form.name).bytecode(g)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
when Seq
|
115
|
+
if quoted || form.empty?
|
116
|
+
g.push_const :Apricot
|
117
|
+
g.find_const :List
|
118
|
+
|
119
|
+
if form.empty?
|
120
|
+
g.find_const :EMPTY_LIST
|
121
|
+
else
|
122
|
+
form.each {|e| bytecode(g, e, true) }
|
123
|
+
g.send :[], form.count
|
124
|
+
end
|
125
|
+
else
|
126
|
+
callee, args = form.first, form.rest
|
127
|
+
|
128
|
+
# Handle special forms such as def, let, fn, quote, etc
|
129
|
+
if callee.is_a?(Identifier)
|
130
|
+
if special = SpecialForm[callee.name]
|
131
|
+
special.bytecode(g, args)
|
132
|
+
return
|
133
|
+
end
|
134
|
+
|
135
|
+
if macroexpand
|
136
|
+
form = Apricot.macroexpand(form)
|
137
|
+
|
138
|
+
if form.is_a?(Seq)
|
139
|
+
# Avoid recursing and macroexpanding again if expansion returns a list
|
140
|
+
bytecode(g, form, false, false)
|
141
|
+
else
|
142
|
+
bytecode(g, form)
|
143
|
+
end
|
144
|
+
|
145
|
+
return
|
146
|
+
end
|
147
|
+
|
148
|
+
meta = callee.meta
|
149
|
+
|
150
|
+
# Handle inlinable function calls
|
151
|
+
if meta && meta[:inline] && (!meta[:'inline-arities'] ||
|
152
|
+
meta[:'inline-arities'].apricot_call(args.count))
|
153
|
+
begin
|
154
|
+
inlined_form = meta[:inline].apricot_call(*args)
|
155
|
+
rescue => e
|
156
|
+
g.compile_error "Inliner function for '#{callee.name}' raised an exception:\n #{e}"
|
157
|
+
end
|
158
|
+
|
159
|
+
g.tail_position = false
|
160
|
+
bytecode(g, inlined_form)
|
161
|
+
return
|
162
|
+
end
|
163
|
+
|
164
|
+
if callee.fn? || callee.method?
|
165
|
+
qualifier_id = Identifier.intern(callee.qualifier.name)
|
166
|
+
first_name, *rest_names = qualifier_id.const_names
|
167
|
+
|
168
|
+
g.push_const first_name
|
169
|
+
rest_names.each {|n| g.find_const(n) }
|
170
|
+
|
171
|
+
args.each {|arg| bytecode(g, arg) }
|
172
|
+
g.send callee.unqualified_name, args.count
|
173
|
+
return
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# Handle everything else
|
178
|
+
g.tail_position = false
|
179
|
+
bytecode(g, callee)
|
180
|
+
args.each {|arg| bytecode(g, arg) }
|
181
|
+
g.send :apricot_call, args.count
|
182
|
+
end
|
183
|
+
|
184
|
+
when Symbol
|
185
|
+
g.push_literal form
|
186
|
+
|
187
|
+
when Array
|
188
|
+
form.each {|e| bytecode(g, e, quoted) }
|
189
|
+
g.make_array form.size
|
190
|
+
|
191
|
+
when String
|
192
|
+
g.push_literal form
|
193
|
+
g.string_dup # Duplicate string to prevent mutating the literal
|
194
|
+
|
195
|
+
when Hash
|
196
|
+
# Create a new Hash
|
197
|
+
g.push_const :Hash
|
198
|
+
g.push form.size
|
199
|
+
g.send :new_from_literal, 1
|
200
|
+
|
201
|
+
# Add keys and values
|
202
|
+
form.each_pair do |key, value|
|
203
|
+
g.dup # the Hash
|
204
|
+
bytecode(g, key, quoted)
|
205
|
+
bytecode(g, value, quoted)
|
206
|
+
g.send :[]=, 2
|
207
|
+
g.pop # drop the return value of []=
|
208
|
+
end
|
209
|
+
|
210
|
+
when Fixnum
|
211
|
+
g.push form
|
212
|
+
|
213
|
+
when true
|
214
|
+
g.push :true
|
215
|
+
|
216
|
+
when nil
|
217
|
+
g.push :nil
|
218
|
+
|
219
|
+
when false
|
220
|
+
g.push :false
|
221
|
+
|
222
|
+
when Float
|
223
|
+
g.push_unique_literal form
|
224
|
+
|
225
|
+
when Regexp
|
226
|
+
once(g) do
|
227
|
+
g.push_const :Regexp
|
228
|
+
g.push_literal form.source
|
229
|
+
g.push form.options
|
230
|
+
g.send :new, 2
|
231
|
+
end
|
232
|
+
|
233
|
+
when Rational
|
234
|
+
once(g) do
|
235
|
+
g.push_self
|
236
|
+
g.push form.numerator
|
237
|
+
g.push form.denominator
|
238
|
+
g.send :Rational, 2, true
|
239
|
+
end
|
240
|
+
|
241
|
+
when Set
|
242
|
+
g.push_const :Set
|
243
|
+
g.send :new, 0 # TODO: Inline this new?
|
244
|
+
|
245
|
+
form.each do |elem|
|
246
|
+
bytecode(g, elem, quoted)
|
247
|
+
g.send :add, 1
|
248
|
+
end
|
249
|
+
|
250
|
+
when Bignum
|
251
|
+
g.push_unique_literal form
|
252
|
+
|
253
|
+
else
|
254
|
+
g.compile_error "Can't generate bytecode for #{form} (#{form.class})"
|
255
|
+
end
|
256
|
+
end
|
257
|
+
|
258
|
+
# Some literals, such as regexps and rationals, should only be created the
|
259
|
+
# first time they are encountered. We push a literal nil here, and then
|
260
|
+
# overwrite the literal value with the created object if it is nil, i.e.
|
261
|
+
# the first time only. Subsequent encounters will use the previously
|
262
|
+
# created object. This idea was copied from Rubinius::AST::RegexLiteral.
|
263
|
+
#
|
264
|
+
# The passed block should take a generator and generate the bytecode to
|
265
|
+
# create the object the first time.
|
266
|
+
def once(g)
|
267
|
+
idx = g.add_literal(nil)
|
268
|
+
g.push_literal_at idx
|
269
|
+
g.dup
|
270
|
+
g.is_nil
|
271
|
+
|
272
|
+
lbl = g.new_label
|
273
|
+
g.gif lbl
|
274
|
+
g.pop
|
275
|
+
|
276
|
+
yield g
|
277
|
+
|
278
|
+
g.set_literal idx
|
279
|
+
lbl.set!
|
48
280
|
end
|
49
281
|
|
50
|
-
def
|
51
|
-
|
52
|
-
|
282
|
+
def pos(g, form)
|
283
|
+
if (meta = form.apricot_meta) && (line = meta[:line])
|
284
|
+
g.set_line(line)
|
285
|
+
end
|
53
286
|
end
|
54
287
|
end
|
55
288
|
end
|