kapusta 0.2.4 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +7 -2
- data/bin/check-all +19 -0
- data/bin/fennel-parity +157 -0
- data/examples/macros-dbg.kap +9 -0
- data/examples/macros-multi.kap +12 -0
- data/examples/macros-swap.kap +9 -0
- data/examples/macros-thrice-if.kap +18 -0
- data/examples/macros-unless.kap +7 -0
- data/examples/macros-when-let.kap +7 -0
- data/examples/manhattan-distance.kap +9 -0
- data/examples/packet-router.kap +2 -5
- data/examples/subtract-product-sum.kap +14 -0
- data/examples/tic-tac-toe.kap +4 -9
- data/examples/ugly-number.kap +22 -0
- data/lib/kapusta/ast.rb +42 -0
- data/lib/kapusta/compiler/emitter/bindings.rb +18 -7
- data/lib/kapusta/compiler/emitter/collections.rb +69 -45
- data/lib/kapusta/compiler/emitter/control_flow.rb +39 -66
- data/lib/kapusta/compiler/emitter/expressions.rb +34 -0
- data/lib/kapusta/compiler/emitter/interop.rb +65 -72
- data/lib/kapusta/compiler/emitter/patterns.rb +192 -109
- data/lib/kapusta/compiler/emitter/support.rb +4 -16
- data/lib/kapusta/compiler/emitter.rb +1 -3
- data/lib/kapusta/compiler/macro_expander.rb +256 -0
- data/lib/kapusta/compiler/normalizer.rb +2 -2
- data/lib/kapusta/compiler.rb +8 -1
- data/lib/kapusta/formatter.rb +216 -87
- data/lib/kapusta/reader.rb +46 -10
- data/lib/kapusta/version.rb +1 -1
- data/lib/kapusta.rb +28 -4
- data/spec/examples_spec.rb +59 -0
- data/spec/formatter_spec.rb +8 -10
- metadata +13 -2
- data/lib/kapusta/compiler/runtime.rb +0 -226
|
@@ -11,174 +11,257 @@ module Kapusta
|
|
|
11
11
|
return ['', env] if pattern.name == '_'
|
|
12
12
|
|
|
13
13
|
ruby_name = define_local(env, pattern)
|
|
14
|
-
["#{ruby_name} = #{value_code}", env]
|
|
14
|
+
return ["#{ruby_name} = #{value_code}", env]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
native = try_emit_native_pattern_bind(pattern, value_code, env)
|
|
18
|
+
return native if native
|
|
19
|
+
|
|
20
|
+
emit_error!("destructure pattern this compiler cannot translate: #{pattern.inspect}")
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def try_emit_native_pattern_bind(pattern, value_code, env)
|
|
24
|
+
case pattern
|
|
25
|
+
when Vec
|
|
26
|
+
try_emit_native_vec_bind(pattern, value_code, env)
|
|
27
|
+
when HashLit
|
|
28
|
+
try_emit_native_hash_bind(pattern, value_code, env)
|
|
29
|
+
end
|
|
30
|
+
rescue PatternNotTranslatable
|
|
31
|
+
nil
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def try_emit_native_vec_bind(pattern, value_code, env)
|
|
35
|
+
parts = []
|
|
36
|
+
deferred = []
|
|
37
|
+
current_env = env
|
|
38
|
+
items = pattern.items
|
|
39
|
+
i = 0
|
|
40
|
+
while i < items.length
|
|
41
|
+
if items[i].is_a?(Sym) && items[i].name == '&'
|
|
42
|
+
sub = items[i + 1]
|
|
43
|
+
raise PatternNotTranslatable unless sub.is_a?(Sym)
|
|
44
|
+
|
|
45
|
+
parts << native_rest_target(sub, current_env)
|
|
46
|
+
i += 2
|
|
47
|
+
else
|
|
48
|
+
code, current_env, follow_up = native_destructure_target(items[i], current_env, allow_follow_up: true)
|
|
49
|
+
parts << code
|
|
50
|
+
deferred << follow_up if follow_up
|
|
51
|
+
i += 1
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
if deferred.empty?
|
|
55
|
+
["#{parts.join(', ')} = #{value_code}", current_env]
|
|
15
56
|
else
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
lines =
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
lines << "#{ruby_name} = #{bindings_var}.fetch(#{name.inspect})"
|
|
57
|
+
arr_var = simple_expression?(value_code) ? value_code : temp('array')
|
|
58
|
+
lines = []
|
|
59
|
+
lines << "#{arr_var} = #{value_code}" unless arr_var == value_code
|
|
60
|
+
lines << "#{parts.join(', ')} = #{arr_var}"
|
|
61
|
+
deferred.each do |follow_up|
|
|
62
|
+
sub_code, current_env = follow_up.call(current_env)
|
|
63
|
+
lines << sub_code
|
|
24
64
|
end
|
|
25
65
|
[lines.join("\n"), current_env]
|
|
26
66
|
end
|
|
27
67
|
end
|
|
28
68
|
|
|
29
|
-
def
|
|
30
|
-
|
|
69
|
+
def try_emit_native_hash_bind(pattern, value_code, env)
|
|
70
|
+
pairs = pattern.pairs
|
|
71
|
+
raise PatternNotTranslatable if pairs.empty?
|
|
72
|
+
|
|
73
|
+
temp_var = simple_expression?(value_code) ? value_code : temp('hash')
|
|
31
74
|
lines = []
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
75
|
+
lines << "#{temp_var} = #{value_code}" unless temp_var == value_code
|
|
76
|
+
current_env = env
|
|
77
|
+
pairs.each do |key, sub|
|
|
78
|
+
access = "#{temp_var}[#{key.inspect}]"
|
|
79
|
+
if sub.is_a?(Sym)
|
|
80
|
+
raise PatternNotTranslatable if sub.name == '_'
|
|
81
|
+
|
|
82
|
+
ruby_name = define_local(current_env, sub.name)
|
|
83
|
+
lines << "#{ruby_name} = #{access}"
|
|
84
|
+
else
|
|
85
|
+
sub_code, current_env = try_emit_native_pattern_bind(sub, access, current_env) ||
|
|
86
|
+
raise(PatternNotTranslatable)
|
|
87
|
+
lines << sub_code
|
|
88
|
+
end
|
|
35
89
|
end
|
|
36
90
|
[lines.join("\n"), current_env]
|
|
37
91
|
end
|
|
38
92
|
|
|
39
|
-
def
|
|
40
|
-
|
|
93
|
+
def native_destructure_target(pattern, env, allow_follow_up: false)
|
|
94
|
+
case pattern
|
|
95
|
+
when Sym
|
|
96
|
+
return ['_', env, nil] if pattern.name == '_'
|
|
97
|
+
|
|
98
|
+
ruby_name = define_local(env, pattern.name)
|
|
99
|
+
[ruby_name, env, nil]
|
|
100
|
+
when Vec
|
|
101
|
+
inner = []
|
|
102
|
+
current = env
|
|
103
|
+
deferred = []
|
|
104
|
+
items = pattern.items
|
|
105
|
+
i = 0
|
|
106
|
+
while i < items.length
|
|
107
|
+
if items[i].is_a?(Sym) && items[i].name == '&'
|
|
108
|
+
inner << native_rest_target(items[i + 1], current)
|
|
109
|
+
i += 2
|
|
110
|
+
else
|
|
111
|
+
code, current, follow_up = native_destructure_target(items[i], current)
|
|
112
|
+
inner << code
|
|
113
|
+
deferred << follow_up if follow_up
|
|
114
|
+
i += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
raise PatternNotTranslatable unless deferred.empty?
|
|
118
|
+
|
|
119
|
+
["(#{inner.join(', ')})", current, nil]
|
|
120
|
+
when HashLit
|
|
121
|
+
raise PatternNotTranslatable unless allow_follow_up
|
|
122
|
+
|
|
123
|
+
slot = temp('slot')
|
|
124
|
+
follow_up = lambda do |outer_env|
|
|
125
|
+
try_emit_native_hash_bind(pattern, slot, outer_env) || raise(PatternNotTranslatable)
|
|
126
|
+
end
|
|
127
|
+
[slot, env, follow_up]
|
|
128
|
+
else
|
|
129
|
+
raise PatternNotTranslatable
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def native_rest_target(sym, env)
|
|
134
|
+
raise PatternNotTranslatable unless sym.is_a?(Sym)
|
|
135
|
+
|
|
136
|
+
return '*' if sym.name == '_'
|
|
137
|
+
|
|
138
|
+
"*#{define_local(env, sym.name)}"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
class PatternNotTranslatable < StandardError; end
|
|
142
|
+
|
|
143
|
+
def native_pattern_plan(pattern, env, mode:, allow_pins:)
|
|
144
|
+
state = { bound_names: {}, binding_names: [], guards: [] }
|
|
145
|
+
ruby_pattern = compile_native_pattern(pattern, env, mode:, allow_pins:, state:)
|
|
41
146
|
{
|
|
42
|
-
pattern:
|
|
147
|
+
pattern: ruby_pattern,
|
|
148
|
+
guards: state[:guards],
|
|
43
149
|
bindings: state[:binding_names]
|
|
44
150
|
}
|
|
151
|
+
rescue PatternNotTranslatable
|
|
152
|
+
nil
|
|
45
153
|
end
|
|
46
154
|
|
|
47
|
-
def
|
|
155
|
+
def compile_native_pattern(pattern, env, mode:, allow_pins:, state:)
|
|
48
156
|
case pattern
|
|
49
|
-
when Sym
|
|
50
|
-
|
|
51
|
-
when
|
|
52
|
-
emit_sequence_match_pattern(pattern.items, env, mode:, allow_pins:, state:)
|
|
53
|
-
when HashLit
|
|
54
|
-
pairs = pattern.pairs.map do |key, value|
|
|
55
|
-
"[#{key.inspect}, #{emit_match_pattern(value, env, mode:, allow_pins:, state:)}]"
|
|
56
|
-
end
|
|
57
|
-
"[:hash, [#{pairs.join(', ')}]]"
|
|
157
|
+
when Sym then compile_native_symbol(pattern, env, mode:, state:)
|
|
158
|
+
when Vec then compile_native_sequence(pattern.items, env, mode:, allow_pins:, state:)
|
|
159
|
+
when HashLit then compile_native_hash(pattern, env, mode:, allow_pins:, state:)
|
|
58
160
|
when List
|
|
59
161
|
if pin_pattern?(pattern)
|
|
60
|
-
|
|
162
|
+
compile_native_pin(pattern, env, mode:, allow_pins:)
|
|
61
163
|
elsif or_pattern?(pattern)
|
|
62
|
-
|
|
63
|
-
elsif where_pattern?(pattern)
|
|
64
|
-
emit_error!('`where` is only valid as a case/match clause head')
|
|
164
|
+
compile_native_or(pattern, env, mode:, allow_pins:, state:)
|
|
65
165
|
else
|
|
66
|
-
|
|
166
|
+
compile_native_sequence(pattern.items, env, mode:, allow_pins:, state:)
|
|
67
167
|
end
|
|
68
|
-
when nil
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
"[:lit, #{pattern.inspect}]"
|
|
72
|
-
else
|
|
73
|
-
emit_error!("bad pattern: #{pattern.inspect}")
|
|
168
|
+
when nil then 'nil'
|
|
169
|
+
when Symbol, String, Numeric, true, false then pattern.inspect
|
|
170
|
+
else raise PatternNotTranslatable
|
|
74
171
|
end
|
|
75
172
|
end
|
|
76
173
|
|
|
77
|
-
def
|
|
174
|
+
def compile_native_symbol(pattern, env, mode:, state:)
|
|
78
175
|
name = pattern.name
|
|
176
|
+
return '_' if name == '_'
|
|
79
177
|
|
|
80
|
-
if name
|
|
81
|
-
|
|
82
|
-
elsif nil_allowing_pattern_name?(name)
|
|
83
|
-
bind_name = name.start_with?('?') ? name.delete_prefix('?') : name
|
|
84
|
-
emit_named_match_pattern(bind_name, env, mode:, state:, allow_nil: true, prefer_pin: false)
|
|
85
|
-
else
|
|
86
|
-
emit_named_match_pattern(name, env, mode:, state:, allow_nil: false, prefer_pin: true)
|
|
87
|
-
end
|
|
88
|
-
end
|
|
178
|
+
if nil_allowing_pattern_name?(name)
|
|
179
|
+
raise PatternNotTranslatable if state[:bound_names].key?(name)
|
|
89
180
|
|
|
90
|
-
def emit_named_match_pattern(name, env, mode:, state:, allow_nil:, prefer_pin:)
|
|
91
|
-
binding = prefer_pin && mode == :match ? env.lookup_if_defined(name) : nil
|
|
92
|
-
if state[:bound_names].key?(name)
|
|
93
|
-
"[:ref, #{name.inspect}]"
|
|
94
|
-
elsif binding
|
|
95
|
-
"[:pin, #{binding_value_code(binding)}]"
|
|
96
|
-
else
|
|
97
181
|
state[:bound_names][name] = true
|
|
98
182
|
state[:binding_names] << name
|
|
99
|
-
|
|
183
|
+
sanitize_local(name)
|
|
184
|
+
else
|
|
185
|
+
binding = mode == :match ? env.lookup_if_defined(name) : nil
|
|
186
|
+
if state[:bound_names].key?(name)
|
|
187
|
+
raise PatternNotTranslatable
|
|
188
|
+
elsif binding
|
|
189
|
+
"^(#{binding_value_code(binding)})"
|
|
190
|
+
else
|
|
191
|
+
state[:bound_names][name] = true
|
|
192
|
+
state[:binding_names] << name
|
|
193
|
+
ruby = sanitize_local(name)
|
|
194
|
+
state[:guards] << "!#{ruby}.nil?"
|
|
195
|
+
ruby
|
|
196
|
+
end
|
|
100
197
|
end
|
|
101
198
|
end
|
|
102
199
|
|
|
103
|
-
def
|
|
200
|
+
def compile_native_sequence(items, env, mode:, allow_pins:, state:)
|
|
104
201
|
parts = []
|
|
202
|
+
has_rest = false
|
|
105
203
|
i = 0
|
|
106
204
|
while i < items.length
|
|
107
205
|
if rest_pattern_marker?(items, i)
|
|
108
|
-
|
|
206
|
+
has_rest = true
|
|
207
|
+
sub = items[i + 1]
|
|
208
|
+
raise PatternNotTranslatable unless sub.is_a?(Sym)
|
|
209
|
+
|
|
210
|
+
if sub.name == '_'
|
|
211
|
+
parts << '*'
|
|
212
|
+
else
|
|
213
|
+
state[:bound_names][sub.name] = true
|
|
214
|
+
state[:binding_names] << sub.name
|
|
215
|
+
parts << "*#{sanitize_local(sub.name)}"
|
|
216
|
+
end
|
|
109
217
|
i += 2
|
|
110
218
|
else
|
|
111
|
-
parts <<
|
|
219
|
+
parts << compile_native_pattern(items[i], env, mode:, allow_pins:, state:)
|
|
112
220
|
i += 1
|
|
113
221
|
end
|
|
114
222
|
end
|
|
115
|
-
|
|
223
|
+
parts << '*' unless has_rest
|
|
224
|
+
"[#{parts.join(', ')}]"
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def compile_native_hash(pattern, env, mode:, allow_pins:, state:)
|
|
228
|
+
pairs = pattern.pairs.map do |key, value|
|
|
229
|
+
raise PatternNotTranslatable unless key.is_a?(Symbol)
|
|
230
|
+
|
|
231
|
+
"#{key}: #{compile_native_pattern(value, env, mode:, allow_pins:, state:)}"
|
|
232
|
+
end
|
|
233
|
+
"{#{pairs.join(', ')}}"
|
|
116
234
|
end
|
|
117
235
|
|
|
118
|
-
def
|
|
119
|
-
|
|
236
|
+
def compile_native_pin(pattern, env, mode:, allow_pins:)
|
|
237
|
+
raise PatternNotTranslatable unless allow_pins && mode == :case
|
|
120
238
|
|
|
121
239
|
name_sym = pattern.items[1]
|
|
122
|
-
|
|
240
|
+
raise PatternNotTranslatable unless name_sym.is_a?(Sym)
|
|
123
241
|
|
|
124
242
|
binding = env.lookup_if_defined(name_sym.name)
|
|
125
|
-
|
|
243
|
+
raise PatternNotTranslatable unless binding
|
|
126
244
|
|
|
127
|
-
"
|
|
245
|
+
"^(#{binding_value_code(binding)})"
|
|
128
246
|
end
|
|
129
247
|
|
|
130
|
-
def
|
|
131
|
-
initial_names = state[:binding_names].length
|
|
248
|
+
def compile_native_or(pattern, env, mode:, allow_pins:, state:)
|
|
132
249
|
initial_bound = state[:bound_names].dup
|
|
133
|
-
|
|
250
|
+
initial_names = state[:binding_names].length
|
|
251
|
+
initial_guards = state[:guards].length
|
|
134
252
|
variants = pattern.items[1..].map do |subpattern|
|
|
135
253
|
alt_state = {
|
|
136
254
|
bound_names: initial_bound.dup,
|
|
137
|
-
binding_names: state[:binding_names].dup
|
|
255
|
+
binding_names: state[:binding_names].dup,
|
|
256
|
+
guards: state[:guards].dup
|
|
138
257
|
}
|
|
139
|
-
compiled =
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
emit_error!('all `or` patterns must bind the same names') if canonical_names.sort != alt_names.sort
|
|
258
|
+
compiled = compile_native_pattern(subpattern, env, mode:, allow_pins:, state: alt_state)
|
|
259
|
+
raise PatternNotTranslatable if alt_state[:binding_names].length > initial_names
|
|
260
|
+
raise PatternNotTranslatable if alt_state[:guards].length > initial_guards
|
|
143
261
|
|
|
144
262
|
compiled
|
|
145
263
|
end
|
|
146
|
-
|
|
147
|
-
canonical_names.each do |name|
|
|
148
|
-
state[:bound_names][name] = true
|
|
149
|
-
state[:binding_names] << name
|
|
150
|
-
end
|
|
151
|
-
"[:or, [#{variants.join(', ')}]]"
|
|
152
|
-
end
|
|
153
|
-
|
|
154
|
-
def emit_pattern(pattern)
|
|
155
|
-
case pattern
|
|
156
|
-
when Sym
|
|
157
|
-
pattern.name == '_' ? '[:sym, "_"]' : "[:sym, #{pattern.name.inspect}]"
|
|
158
|
-
when Vec
|
|
159
|
-
parts = []
|
|
160
|
-
items = pattern.items
|
|
161
|
-
i = 0
|
|
162
|
-
while i < items.length
|
|
163
|
-
if items[i].is_a?(Sym) && items[i].name == '&'
|
|
164
|
-
parts << "[:rest, #{emit_pattern(items[i + 1])}]"
|
|
165
|
-
i += 2
|
|
166
|
-
else
|
|
167
|
-
parts << emit_pattern(items[i])
|
|
168
|
-
i += 1
|
|
169
|
-
end
|
|
170
|
-
end
|
|
171
|
-
"[:vec, [#{parts.join(', ')}]]"
|
|
172
|
-
when HashLit
|
|
173
|
-
pairs = pattern.pairs.map { |key, value| "[#{key.inspect}, #{emit_pattern(value)}]" }
|
|
174
|
-
"[:hash, [#{pairs.join(', ')}]]"
|
|
175
|
-
when nil
|
|
176
|
-
'[:nil]'
|
|
177
|
-
when Symbol, String, Numeric, true, false
|
|
178
|
-
"[:lit, #{pattern.inspect}]"
|
|
179
|
-
else
|
|
180
|
-
emit_error!("bad pattern: #{pattern.inspect}")
|
|
181
|
-
end
|
|
264
|
+
"(#{variants.join(' | ')})"
|
|
182
265
|
end
|
|
183
266
|
|
|
184
267
|
def pattern_names(pattern)
|
|
@@ -62,7 +62,7 @@ module Kapusta
|
|
|
62
62
|
else
|
|
63
63
|
name_sym, supers, = split_class_args(form.items[1..])
|
|
64
64
|
body = emit_forms_with_headers(remaining_forms, env, :class, result: false)
|
|
65
|
-
emit_direct_class_header(name_sym, supers, body) || emit_class_wrapper(name_sym, supers, env, body)
|
|
65
|
+
emit_direct_class_header(name_sym, supers, body, env) || emit_class_wrapper(name_sym, supers, env, body)
|
|
66
66
|
end
|
|
67
67
|
end
|
|
68
68
|
|
|
@@ -128,7 +128,7 @@ module Kapusta
|
|
|
128
128
|
return [emit_for_statement(form.rest, env, current_scope), env]
|
|
129
129
|
when 'each'
|
|
130
130
|
code = emit_each_statement(form.rest, env, current_scope)
|
|
131
|
-
return ["#{code}\nnil", env] if result_needed
|
|
131
|
+
return ["#{code}\nnil", env] if result_needed && current_scope != :toplevel
|
|
132
132
|
|
|
133
133
|
return [code, env]
|
|
134
134
|
end
|
|
@@ -219,19 +219,6 @@ module Kapusta
|
|
|
219
219
|
source_name.is_a?(GeneratedSym)
|
|
220
220
|
end
|
|
221
221
|
|
|
222
|
-
def runtime_helper(name)
|
|
223
|
-
helper = name.to_sym
|
|
224
|
-
@runtime_helpers << helper unless @runtime_helpers.include?(helper)
|
|
225
|
-
Runtime.helper_name(helper)
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
def runtime_call(name, *args)
|
|
229
|
-
rendered_args = args.map do |arg|
|
|
230
|
-
arg.is_a?(Array) ? "[#{arg.join(', ')}]" : arg || 'nil'
|
|
231
|
-
end
|
|
232
|
-
"#{runtime_helper(name)}(#{rendered_args.join(', ')})"
|
|
233
|
-
end
|
|
234
|
-
|
|
235
222
|
def method_binding?(binding)
|
|
236
223
|
binding.is_a?(Env::MethodBinding)
|
|
237
224
|
end
|
|
@@ -276,7 +263,8 @@ module Kapusta
|
|
|
276
263
|
|
|
277
264
|
def sanitize_local(name)
|
|
278
265
|
base = Kapusta.kebab_to_snake(name.respond_to?(:name) ? name.name : name)
|
|
279
|
-
base = base.
|
|
266
|
+
base = base.sub(/\A\?/, 'q_').gsub('?', '_q')
|
|
267
|
+
base = base.sub(/\A!/, 'bang_').gsub('!', '_bang')
|
|
280
268
|
base = base.gsub(/[^a-zA-Z0-9_]/, '_')
|
|
281
269
|
if base.empty? || base.match?(/\A\d/) || base.match?(/\A[A-Z]/) || self.class::RUBY_KEYWORDS.include?(base)
|
|
282
270
|
base = "_#{base}"
|
|
@@ -27,14 +27,12 @@ module Kapusta
|
|
|
27
27
|
def initialize(path:)
|
|
28
28
|
@path = path
|
|
29
29
|
@temp_index = 0
|
|
30
|
-
@runtime_helpers = []
|
|
31
30
|
end
|
|
32
31
|
|
|
33
32
|
def emit_file(forms)
|
|
34
33
|
env = Env.new
|
|
35
34
|
body = emit_forms_with_headers(forms, env, :toplevel)
|
|
36
|
-
|
|
37
|
-
[helpers, body].reject(&:empty?).join("\n\n") << "\n"
|
|
35
|
+
"#{body}\n"
|
|
38
36
|
end
|
|
39
37
|
end
|
|
40
38
|
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
module Compiler
|
|
5
|
+
class MacroExpander
|
|
6
|
+
class Error < Kapusta::Error; end
|
|
7
|
+
|
|
8
|
+
@gensym_counter = 0
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
def fresh_gensym(prefix)
|
|
12
|
+
@gensym_counter += 1
|
|
13
|
+
GeneratedSym.new("#{prefix}_g#{@gensym_counter}", @gensym_counter)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fresh_local_gensym(prefix)
|
|
17
|
+
@gensym_counter += 1
|
|
18
|
+
GeneratedSym.new("#{prefix}_local_#{@gensym_counter}", @gensym_counter)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@macros = {}
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def expand_all(forms)
|
|
27
|
+
forms.flat_map { |form| expand_top(form) }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def expand_top(form)
|
|
33
|
+
if form.is_a?(List) && form.head.is_a?(Sym)
|
|
34
|
+
case form.head.name
|
|
35
|
+
when 'macro'
|
|
36
|
+
register_macro_form(form.rest)
|
|
37
|
+
return []
|
|
38
|
+
when 'macros'
|
|
39
|
+
register_macros_form(form.rest)
|
|
40
|
+
return []
|
|
41
|
+
when 'import-macros'
|
|
42
|
+
raise Error, 'import-macros is not yet supported'
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
[expand(form)]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def expand(form)
|
|
49
|
+
case form
|
|
50
|
+
when List then expand_list(form)
|
|
51
|
+
when Vec then Vec.new(form.items.map { |item| expand(item) })
|
|
52
|
+
when HashLit
|
|
53
|
+
HashLit.new(form.entries.map do |entry|
|
|
54
|
+
entry.is_a?(Array) ? [expand(entry[0]), expand(entry[1])] : entry
|
|
55
|
+
end)
|
|
56
|
+
else
|
|
57
|
+
form
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def expand_list(list)
|
|
62
|
+
return list if list.empty?
|
|
63
|
+
|
|
64
|
+
head = list.head
|
|
65
|
+
if head.is_a?(Sym) && !head.is_a?(AutoGensym)
|
|
66
|
+
name = head.name
|
|
67
|
+
case name
|
|
68
|
+
when 'macro'
|
|
69
|
+
register_macro_form(list.rest)
|
|
70
|
+
return List.new([Sym.new('do')])
|
|
71
|
+
when 'macros'
|
|
72
|
+
register_macros_form(list.rest)
|
|
73
|
+
return List.new([Sym.new('do')])
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
key = lookup_key(name)
|
|
77
|
+
if @macros.key?(key)
|
|
78
|
+
args = list.rest
|
|
79
|
+
result = invoke_macro(key, args)
|
|
80
|
+
return expand(result)
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
List.new(list.items.map { |item| expand(item) })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def lookup_key(name)
|
|
88
|
+
Kapusta.kebab_to_snake(name).to_sym
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def register_macro_form(args)
|
|
92
|
+
name_sym, params, *body = args
|
|
93
|
+
raise Error, 'macro name must be a symbol' unless name_sym.is_a?(Sym)
|
|
94
|
+
raise Error, 'macro params must be a vector' unless params.is_a?(Vec)
|
|
95
|
+
|
|
96
|
+
register(name_sym.name, params, body)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def register_macros_form(args)
|
|
100
|
+
hash_lit = args[0]
|
|
101
|
+
raise Error, 'macros expects a hash literal' unless hash_lit.is_a?(HashLit)
|
|
102
|
+
|
|
103
|
+
hash_lit.pairs.each do |key, value|
|
|
104
|
+
raise Error, "macros entry value must be a fn form, got #{value.inspect}" unless fn_form?(value)
|
|
105
|
+
|
|
106
|
+
name = key.to_s
|
|
107
|
+
params = value.items[1]
|
|
108
|
+
body = value.items[2..]
|
|
109
|
+
raise Error, 'macros entry params must be a vector' unless params.is_a?(Vec)
|
|
110
|
+
|
|
111
|
+
register(name, params, body)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def fn_form?(value)
|
|
116
|
+
value.is_a?(List) && value.head.is_a?(Sym) && %w[fn lambda λ].include?(value.head.name)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def register(source_name, params, body)
|
|
120
|
+
proc_obj = compile_macro(source_name, params, body)
|
|
121
|
+
@macros[lookup_key(source_name)] = proc_obj
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def compile_macro(name, params, body)
|
|
125
|
+
lowering = Lowering.new
|
|
126
|
+
lowered_body = body.map { |form| lowering.lower(form) }
|
|
127
|
+
gensym_locals = lowering.collected_gensyms
|
|
128
|
+
|
|
129
|
+
wrapped =
|
|
130
|
+
if gensym_locals.empty?
|
|
131
|
+
List.new([Sym.new('fn'), params, *lowered_body])
|
|
132
|
+
else
|
|
133
|
+
let_bindings = gensym_locals.flat_map do |gensym_sym, prefix|
|
|
134
|
+
[gensym_sym, List.new([Sym.new('quasi-gensym'), prefix])]
|
|
135
|
+
end
|
|
136
|
+
inner = lowered_body.length == 1 ? lowered_body.first : List.new([Sym.new('do'), *lowered_body])
|
|
137
|
+
List.new([Sym.new('fn'), params, List.new([Sym.new('let'), Vec.new(let_bindings), inner])])
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
ruby = Compiler.compile_forms([wrapped], path: "(macro #{name})")
|
|
141
|
+
TOPLEVEL_BINDING.eval(ruby, "(macro #{name})", 1)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def invoke_macro(key, args)
|
|
145
|
+
proc_obj = @macros[key]
|
|
146
|
+
proc_obj.call(*args)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
class Lowering
|
|
150
|
+
def initialize
|
|
151
|
+
@gensyms = {}
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def collected_gensyms
|
|
155
|
+
@gensyms.map { |prefix, sym| [sym, prefix] }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def lower(form)
|
|
159
|
+
case form
|
|
160
|
+
when Quasiquote then lower_quasi(form.form)
|
|
161
|
+
when Unquote, UnquoteSplice
|
|
162
|
+
raise Error, 'unquote outside quasiquote'
|
|
163
|
+
when AutoGensym
|
|
164
|
+
raise Error, "auto-gensym #{form.name}# outside quasiquote"
|
|
165
|
+
when List then List.new(form.items.map { |item| lower(item) })
|
|
166
|
+
when Vec then Vec.new(form.items.map { |item| lower(item) })
|
|
167
|
+
when HashLit
|
|
168
|
+
HashLit.new(form.entries.map do |entry|
|
|
169
|
+
entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
|
|
170
|
+
end)
|
|
171
|
+
else
|
|
172
|
+
form
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def lower_quasi(form)
|
|
177
|
+
case form
|
|
178
|
+
when AutoGensym then gensym_local_for(form.name)
|
|
179
|
+
when Sym then List.new([Sym.new('quasi-sym'), form.name])
|
|
180
|
+
when List then lower_quasi_list(form)
|
|
181
|
+
when Vec then lower_quasi_vec(form)
|
|
182
|
+
when HashLit then lower_quasi_hash(form)
|
|
183
|
+
when Unquote then lower(form.form)
|
|
184
|
+
when UnquoteSplice
|
|
185
|
+
raise Error, 'unquote-splice must appear inside a quoted list/vec'
|
|
186
|
+
when Quasiquote
|
|
187
|
+
raise Error, 'nested quasiquote is not supported'
|
|
188
|
+
else
|
|
189
|
+
form
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def lower_quasi_list(list)
|
|
194
|
+
items = list.items
|
|
195
|
+
return List.new([Sym.new('quasi-list')]) if items.empty?
|
|
196
|
+
|
|
197
|
+
if (tail_expr = splice_tail(items))
|
|
198
|
+
head_items = items[0...-1].map { |item| lower_quasi(item) }
|
|
199
|
+
return List.new([Sym.new('quasi-list-tail'), Vec.new(head_items), tail_expr])
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
lowered_items = items.map { |item| lower_quasi_item(item) }
|
|
203
|
+
List.new([Sym.new('quasi-list'), *lowered_items])
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def lower_quasi_vec(vec)
|
|
207
|
+
items = vec.items
|
|
208
|
+
if (tail_expr = splice_tail(items))
|
|
209
|
+
head_items = items[0...-1].map { |item| lower_quasi(item) }
|
|
210
|
+
return List.new([Sym.new('quasi-vec-tail'), Vec.new(head_items), tail_expr])
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
lowered_items = items.map { |item| lower_quasi_item(item) }
|
|
214
|
+
List.new([Sym.new('quasi-vec'), *lowered_items])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def lower_quasi_hash(hash)
|
|
218
|
+
parts = []
|
|
219
|
+
hash.entries.each do |entry|
|
|
220
|
+
next unless entry.is_a?(Array)
|
|
221
|
+
|
|
222
|
+
key, value = entry
|
|
223
|
+
parts << lower_quasi(key) << lower_quasi(value)
|
|
224
|
+
end
|
|
225
|
+
List.new([Sym.new('quasi-hash'), *parts])
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def lower_quasi_item(item)
|
|
229
|
+
if item.is_a?(Unquote) && unpack_call?(item.form)
|
|
230
|
+
inner = lower(item.form.items[1])
|
|
231
|
+
List.new([Sym.new('.'), inner, 0])
|
|
232
|
+
else
|
|
233
|
+
lower_quasi(item)
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def splice_tail(items)
|
|
238
|
+
last = items.last
|
|
239
|
+
return unless last
|
|
240
|
+
return lower(last.form) if last.is_a?(UnquoteSplice)
|
|
241
|
+
return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
|
|
242
|
+
|
|
243
|
+
nil
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def unpack_call?(form)
|
|
247
|
+
form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def gensym_local_for(prefix)
|
|
251
|
+
@gensyms[prefix] ||= MacroExpander.fresh_local_gensym(prefix)
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|