kapusta 0.3.0 → 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 +1 -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/packet-router.kap +2 -5
- 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/expressions.rb +34 -0
- data/lib/kapusta/compiler/emitter/patterns.rb +7 -11
- data/lib/kapusta/compiler/emitter/support.rb +2 -1
- data/lib/kapusta/compiler/macro_expander.rb +256 -0
- data/lib/kapusta/compiler.rb +8 -0
- 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 +5 -5
- data/spec/examples_spec.rb +51 -0
- data/spec/formatter_spec.rb +8 -10
- metadata +11 -1
|
@@ -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
|
data/lib/kapusta/compiler.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative 'error'
|
|
4
4
|
require_relative 'compiler/normalizer'
|
|
5
5
|
require_relative 'compiler/emitter'
|
|
6
|
+
require_relative 'compiler/macro_expander'
|
|
6
7
|
|
|
7
8
|
module Kapusta
|
|
8
9
|
module Compiler
|
|
@@ -27,10 +28,17 @@ module Kapusta
|
|
|
27
28
|
= not= < <= > >=
|
|
28
29
|
+ - * / %
|
|
29
30
|
print
|
|
31
|
+
macro macros import-macros
|
|
32
|
+
quasi-sym quasi-list quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym
|
|
30
33
|
].freeze
|
|
31
34
|
|
|
32
35
|
def self.compile(source, path: '(kapusta)')
|
|
33
36
|
forms = Reader.read_all(source)
|
|
37
|
+
expanded = MacroExpander.new.expand_all(forms)
|
|
38
|
+
compile_forms(expanded, path:)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.compile_forms(forms, path: '(kapusta)')
|
|
34
42
|
normalized = Normalizer.new.normalize_all(forms)
|
|
35
43
|
Emitter.new(path:).emit_file(normalized)
|
|
36
44
|
end
|