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.
@@ -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
@@ -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