kapusta 0.5.0 → 0.8.0

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.
Files changed (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +24 -6
  3. data/bin/fennel-parity +11 -4
  4. data/examples/classify-wallet.kap +11 -0
  5. data/examples/import-helpers.kapm +9 -0
  6. data/examples/macros-import-helpers.kap +3 -0
  7. data/examples/macros-import-whole.kap +5 -0
  8. data/examples/macros-import.kap +6 -0
  9. data/examples/power-of-three.kap +12 -0
  10. data/examples/shared-macros.kapm +4 -0
  11. data/exe/kapusta-ls +14 -0
  12. data/kapusta.gemspec +2 -2
  13. data/lib/kapusta/compiler/emitter/bindings.rb +38 -4
  14. data/lib/kapusta/compiler/emitter/collections.rb +51 -59
  15. data/lib/kapusta/compiler/emitter/control_flow.rb +24 -2
  16. data/lib/kapusta/compiler/emitter/expressions.rb +0 -2
  17. data/lib/kapusta/compiler/emitter/interop.rb +2 -1
  18. data/lib/kapusta/compiler/emitter/patterns.rb +52 -4
  19. data/lib/kapusta/compiler/emitter/support.rb +1 -1
  20. data/lib/kapusta/compiler/emitter.rb +1 -1
  21. data/lib/kapusta/compiler/lua_compat.rb +149 -0
  22. data/lib/kapusta/compiler/macro_expander.rb +55 -141
  23. data/lib/kapusta/compiler/macro_gensym.rb +21 -0
  24. data/lib/kapusta/compiler/macro_importer.rb +81 -0
  25. data/lib/kapusta/compiler/macro_lowerer.rb +184 -0
  26. data/lib/kapusta/compiler/normalizer.rb +4 -19
  27. data/lib/kapusta/compiler.rb +4 -2
  28. data/lib/kapusta/errors.rb +9 -3
  29. data/lib/kapusta/formatter.rb +4 -0
  30. data/lib/kapusta/lsp/definition.rb +67 -0
  31. data/lib/kapusta/lsp/diagnostics.rb +42 -0
  32. data/lib/kapusta/lsp/formatting.rb +30 -0
  33. data/lib/kapusta/lsp/identifier.rb +28 -0
  34. data/lib/kapusta/lsp/rename.rb +417 -0
  35. data/lib/kapusta/lsp/scope_walker.rb +643 -0
  36. data/lib/kapusta/lsp/workspace_index.rb +225 -0
  37. data/lib/kapusta/lsp.rb +312 -0
  38. data/lib/kapusta/reader.rb +0 -2
  39. data/lib/kapusta/version.rb +1 -1
  40. data/spec/examples_errors_spec.rb +142 -1
  41. data/spec/examples_spec.rb +12 -0
  42. data/spec/lsp_spec.rb +603 -0
  43. metadata +23 -1
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ module Compiler
5
+ module LuaCompat
6
+ SPECIAL_FORMS = %w[pcall xpcall].freeze
7
+ ITERATOR_FORMS = %w[ipairs pairs].freeze
8
+
9
+ def self.special_form?(name)
10
+ SPECIAL_FORMS.include?(name)
11
+ end
12
+
13
+ def self.iterator_form?(name)
14
+ ITERATOR_FORMS.include?(name)
15
+ end
16
+
17
+ module Normalization
18
+ private
19
+
20
+ def normalize_lua_compat_form(name, items)
21
+ case name
22
+ when 'pcall' then normalize_lua_pcall(items)
23
+ when 'xpcall' then normalize_lua_xpcall(items)
24
+ end
25
+ end
26
+
27
+ def normalize_lua_pcall(items)
28
+ fn = items[1]
29
+ args = items[2..]
30
+ List.new([
31
+ Sym.new('try'),
32
+ List.new([Sym.new('values'), true, List.new([fn, *args])]),
33
+ List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
34
+ List.new([Sym.new('values'), false, Sym.new('e')])])
35
+ ])
36
+ end
37
+
38
+ def normalize_lua_xpcall(items)
39
+ fn = items[1]
40
+ handler = items[2]
41
+ args = items[3..]
42
+ List.new([
43
+ Sym.new('try'),
44
+ List.new([Sym.new('values'), true, List.new([fn, *args])]),
45
+ List.new([Sym.new('catch'), Sym.new('StandardError'), Sym.new('e'),
46
+ List.new([Sym.new('values'), false, List.new([handler, Sym.new('e')])])])
47
+ ])
48
+ end
49
+ end
50
+
51
+ module Emission
52
+ private
53
+
54
+ def emit_lua_compat_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
55
+ init_code, body_forms)
56
+ return unless lua_iterator_expr?(iter_expr)
57
+
58
+ case iter_expr.head.name
59
+ when 'ipairs'
60
+ emit_lua_ipairs_inject(iter_expr, binding_pats, body_env, env, current_scope,
61
+ acc_var, init_code, body_forms)
62
+ when 'pairs'
63
+ emit_lua_pairs_inject(iter_expr, binding_pats, body_env, env, current_scope,
64
+ acc_var, init_code, body_forms)
65
+ end
66
+ end
67
+
68
+ def emit_lua_compat_iteration(iter_expr, binding_pats, env, current_scope, method:,
69
+ extra_block_param: nil, &block)
70
+ return unless lua_iterator_expr?(iter_expr)
71
+
72
+ case iter_expr.head.name
73
+ when 'ipairs'
74
+ emit_lua_ipairs_iteration(iter_expr, binding_pats, env, current_scope,
75
+ method:, extra_block_param:, &block)
76
+ when 'pairs'
77
+ emit_lua_pairs_iteration(iter_expr, binding_pats, env, current_scope,
78
+ method:, extra_block_param:, &block)
79
+ end
80
+ end
81
+
82
+ def lua_iterator_expr?(expr)
83
+ expr.is_a?(List) && expr.head.is_a?(Sym) && LuaCompat.iterator_form?(expr.head.name)
84
+ end
85
+
86
+ def emit_lua_ipairs_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
87
+ init_code, body_forms)
88
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
89
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
90
+ if ignored_pattern?(binding_pats[0])
91
+ body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
92
+ return inject_block(coll_code, "#{acc_var}, #{value_var}", init_code, value_bind || '', body_code)
93
+ end
94
+
95
+ index_var, index_bind = bind_iteration_param(binding_pats[0], 'index', body_env)
96
+ bind_code = [index_bind, value_bind].compact.join("\n")
97
+ body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
98
+ inject_block("#{coll_code}.each_with_index", "#{acc_var}, (#{value_var}, #{index_var})",
99
+ init_code, bind_code, body_code)
100
+ end
101
+
102
+ def emit_lua_pairs_inject(iter_expr, binding_pats, body_env, env, current_scope, acc_var,
103
+ init_code, body_forms)
104
+ key_var, key_bind = bind_iteration_param(binding_pats[0], 'key', body_env)
105
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
106
+ bind_code = [key_bind, value_bind].compact.join("\n")
107
+ body_code, = emit_sequence(body_forms, body_env, current_scope, allow_method_definitions: false)
108
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
109
+ inject_block(coll_code, "#{acc_var}, (#{key_var}, #{value_var})",
110
+ init_code, bind_code, body_code)
111
+ end
112
+
113
+ def emit_lua_ipairs_iteration(iter_expr, binding_pats, env, current_scope, method:,
114
+ extra_block_param: nil, &block)
115
+ body_env = env.child
116
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
117
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
118
+ if ignored_pattern?(binding_pats[0])
119
+ bind_code = value_bind || ''
120
+ body_code = block.call(body_env)
121
+ params = extra_block_param ? "#{value_var}, #{extra_block_param}" : value_var
122
+ return iteration_block("#{coll_code}.#{method} do |#{params}|", bind_code, body_code)
123
+ end
124
+
125
+ index_var, index_bind = bind_iteration_param(binding_pats[0], 'index', body_env)
126
+ bind_code = [index_bind, value_bind].compact.join("\n")
127
+ body_code = block.call(body_env)
128
+ receiver = method == 'each' ? "#{coll_code}.each_with_index" : "#{coll_code}.each_with_index.#{method}"
129
+ inner_params = "#{value_var}, #{index_var}"
130
+ params = extra_block_param ? "(#{inner_params}), #{extra_block_param}" : inner_params
131
+ iteration_block("#{receiver} do |#{params}|", bind_code, body_code)
132
+ end
133
+
134
+ def emit_lua_pairs_iteration(iter_expr, binding_pats, env, current_scope, method:,
135
+ extra_block_param: nil, &block)
136
+ body_env = env.child
137
+ key_var, key_bind = bind_iteration_param(binding_pats[0], 'key', body_env)
138
+ value_var, value_bind = bind_iteration_param(binding_pats[1], 'value', body_env)
139
+ bind_code = [key_bind, value_bind].compact.join("\n")
140
+ body_code = block.call(body_env)
141
+ coll_code = emit_expr(iter_expr.items[1], env, current_scope)
142
+ inner_params = "#{key_var}, #{value_var}"
143
+ params = extra_block_param ? "(#{inner_params}), #{extra_block_param}" : inner_params
144
+ iteration_block("#{coll_code}.#{method} do |#{params}|", bind_code, body_code)
145
+ end
146
+ end
147
+ end
148
+ end
149
+ end
@@ -1,27 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'macro_gensym'
4
+ require_relative 'macro_lowerer'
5
+ require_relative 'macro_importer'
6
+
3
7
  module Kapusta
4
8
  module Compiler
5
9
  class MacroExpander
6
10
  class Error < Kapusta::Error; end
7
11
 
8
- @gensym_counter = 0
9
-
10
12
  class << self
11
13
  def fresh_gensym(prefix)
12
- @gensym_counter += 1
13
- GeneratedSym.new("#{prefix}_g#{@gensym_counter}", @gensym_counter)
14
+ MacroGensym.fresh_gensym(prefix)
14
15
  end
15
16
 
16
17
  def fresh_local_gensym(prefix)
17
- @gensym_counter += 1
18
- GeneratedSym.new("#{prefix}_local_#{@gensym_counter}", @gensym_counter)
18
+ MacroGensym.fresh_local_gensym(prefix)
19
19
  end
20
20
  end
21
21
 
22
- def initialize(path: nil)
22
+ def initialize(path: nil, loading: nil)
23
23
  @macros = {}
24
24
  @path = path
25
+ @loading = loading || []
25
26
  end
26
27
 
27
28
  def expand_all(forms)
@@ -40,7 +41,8 @@ module Kapusta
40
41
  register_macros_form(form.rest)
41
42
  return []
42
43
  when 'import-macros'
43
- raise macro_error(:import_macros_unsupported, form)
44
+ handle_import_macros(form)
45
+ return []
44
46
  end
45
47
  end
46
48
  [expand(form)]
@@ -89,6 +91,9 @@ module Kapusta
89
91
  when 'macros'
90
92
  register_macros_form(list.rest)
91
93
  return List.new([Sym.new('do')])
94
+ when 'import-macros'
95
+ handle_import_macros(list)
96
+ return List.new([Sym.new('do')])
92
97
  end
93
98
 
94
99
  key = lookup_key(name)
@@ -134,152 +139,61 @@ module Kapusta
134
139
  value.is_a?(List) && value.head.is_a?(Sym) && %w[fn lambda λ].include?(value.head.name)
135
140
  end
136
141
 
137
- def register(source_name, params, body)
138
- proc_obj = compile_macro(source_name, params, body)
139
- @macros[lookup_key(source_name)] = proc_obj
140
- end
141
-
142
- def compile_macro(name, params, body)
143
- lowering = Lowering.new
144
- lowered_body = body.map { |form| lowering.lower(form) }
145
- gensym_locals = lowering.collected_gensyms
146
-
147
- wrapped =
148
- if gensym_locals.empty?
149
- List.new([Sym.new('fn'), params, *lowered_body])
150
- else
151
- let_bindings = gensym_locals.flat_map do |gensym_sym, prefix|
152
- [gensym_sym, List.new([Sym.new('quasi-gensym'), prefix])]
153
- end
154
- inner = lowered_body.length == 1 ? lowered_body.first : List.new([Sym.new('do'), *lowered_body])
155
- List.new([Sym.new('fn'), params, List.new([Sym.new('let'), Vec.new(let_bindings), inner])])
156
- end
157
-
158
- macro_path = @path || "(macro #{name})"
159
- ruby = Compiler.compile_forms([wrapped], path: macro_path)
160
- TOPLEVEL_BINDING.eval(ruby, macro_path, 1)
161
- end
162
-
163
- def invoke_macro(key, args)
164
- proc_obj = @macros[key]
165
- proc_obj.call(*args)
166
- end
167
-
168
- class Lowering
169
- def initialize
170
- @gensyms = {}
171
- end
172
-
173
- def collected_gensyms
174
- @gensyms.map { |prefix, sym| [sym, prefix] }
175
- end
176
-
177
- def lower(form)
178
- case form
179
- when Quasiquote then copy_position(lower_quasi(form.form), form)
180
- when Unquote, UnquoteSplice
181
- raise Error, Kapusta::Errors.format(:unquote_outside_quasiquote)
182
- when AutoGensym
183
- raise Error, Kapusta::Errors.format(:auto_gensym_outside_quasiquote, name: form.name)
184
- when List then copy_position(List.new(form.items.map { |item| lower(item) }), form)
185
- when Vec then copy_position(Vec.new(form.items.map { |item| lower(item) }), form)
186
- when HashLit
187
- copy_position(
188
- HashLit.new(form.entries.map do |entry|
189
- entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
190
- end),
191
- form
192
- )
193
- else
194
- form
195
- end
196
- end
197
-
198
- def copy_position(target, source)
199
- return target unless target.respond_to?(:line=) && source.respond_to?(:line)
200
-
201
- target.line ||= source.line
202
- target.column ||= source.column
203
- target
142
+ def handle_import_macros(form)
143
+ args = form.rest
144
+ destructure = args[0]
145
+ module_arg = args[1]
146
+ unless destructure.is_a?(HashLit) || destructure.is_a?(Sym)
147
+ raise macro_error(:import_macros_destructure_invalid, form)
204
148
  end
205
-
206
- def lower_quasi(form)
207
- case form
208
- when AutoGensym then gensym_local_for(form.name)
209
- when Sym then List.new([Sym.new('quasi-sym'), form.name])
210
- when List then lower_quasi_list(form)
211
- when Vec then lower_quasi_vec(form)
212
- when HashLit then lower_quasi_hash(form)
213
- when Unquote then lower(form.form)
214
- when UnquoteSplice
215
- raise Error, Kapusta::Errors.format(:unquote_splice_outside_list)
216
- when Quasiquote
217
- raise Error, Kapusta::Errors.format(:nested_quasiquote)
218
- else
219
- form
220
- end
149
+ unless module_arg.is_a?(Symbol) || module_arg.is_a?(String)
150
+ raise macro_error(:import_macros_module_invalid, form)
221
151
  end
222
152
 
223
- def lower_quasi_list(list)
224
- items = list.items
225
- return List.new([Sym.new('quasi-list')]) if items.empty?
226
-
227
- if (tail_expr = splice_tail(items))
228
- head_items = items[0...-1].map { |item| lower_quasi(item) }
229
- return List.new([Sym.new('quasi-list-tail'), Vec.new(head_items), tail_expr])
230
- end
231
-
232
- lowered_items = items.map { |item| lower_quasi_item(item) }
233
- List.new([Sym.new('quasi-list'), *lowered_items])
153
+ module_label = MacroImporter.module_label(module_arg)
154
+ exports = macro_importer.load(module_arg, form)
155
+ if destructure.is_a?(HashLit)
156
+ register_imported_macros(destructure, exports, module_label, form)
157
+ else
158
+ register_whole_module(destructure, exports)
234
159
  end
160
+ end
235
161
 
236
- def lower_quasi_vec(vec)
237
- items = vec.items
238
- if (tail_expr = splice_tail(items))
239
- head_items = items[0...-1].map { |item| lower_quasi(item) }
240
- return List.new([Sym.new('quasi-vec-tail'), Vec.new(head_items), tail_expr])
241
- end
242
-
243
- lowered_items = items.map { |item| lower_quasi_item(item) }
244
- List.new([Sym.new('quasi-vec'), *lowered_items])
245
- end
162
+ def macro_importer
163
+ @macro_importer ||= MacroImporter.new(path: @path, loading: @loading, error_class: Error)
164
+ end
246
165
 
247
- def lower_quasi_hash(hash)
248
- parts = []
249
- hash.entries.each do |entry|
250
- next unless entry.is_a?(Array)
166
+ def register_imported_macros(destructure, exports, module_label, form)
167
+ destructure.pairs.each do |key, target|
168
+ raise macro_error(:import_macros_destructure_invalid, form) unless target.is_a?(Sym)
251
169
 
252
- key, value = entry
253
- parts << lower_quasi(key) << lower_quasi(value)
254
- end
255
- List.new([Sym.new('quasi-hash'), *parts])
170
+ proc_obj = exports[key] ||
171
+ raise(macro_error(:import_macros_macro_not_found, form,
172
+ macro: key.to_s.tr('_', '-'), module: module_label))
173
+ @macros[lookup_key(target.name)] = proc_obj
256
174
  end
175
+ end
257
176
 
258
- def lower_quasi_item(item)
259
- if item.is_a?(Unquote) && unpack_call?(item.form)
260
- inner = lower(item.form.items[1])
261
- List.new([Sym.new('.'), inner, 0])
262
- else
263
- lower_quasi(item)
264
- end
177
+ def register_whole_module(bind_sym, exports)
178
+ exports.each do |export_key, proc_obj|
179
+ macro_name = "#{bind_sym.name}.#{export_key.to_s.tr('_', '-')}"
180
+ @macros[lookup_key(macro_name)] = proc_obj
265
181
  end
182
+ end
266
183
 
267
- def splice_tail(items)
268
- last = items.last
269
- return unless last
270
- return lower(last.form) if last.is_a?(UnquoteSplice)
271
- return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
272
-
273
- nil
274
- end
184
+ def register(source_name, params, body)
185
+ proc_obj = compile_macro(source_name, params, body)
186
+ @macros[lookup_key(source_name)] = proc_obj
187
+ end
275
188
 
276
- def unpack_call?(form)
277
- form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
278
- end
189
+ def compile_macro(name, params, body)
190
+ macro_path = @path || "(macro #{name})"
191
+ MacroLowerer.compile(params:, body:, path: macro_path, error_class: Error)
192
+ end
279
193
 
280
- def gensym_local_for(prefix)
281
- @gensyms[prefix] ||= MacroExpander.fresh_local_gensym(prefix)
282
- end
194
+ def invoke_macro(key, args)
195
+ proc_obj = @macros[key]
196
+ proc_obj.call(*args)
283
197
  end
284
198
  end
285
199
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kapusta
4
+ module Compiler
5
+ module MacroGensym
6
+ @counter = 0
7
+
8
+ class << self
9
+ def fresh_gensym(prefix)
10
+ @counter += 1
11
+ GeneratedSym.new("#{prefix}_g#{@counter}", @counter)
12
+ end
13
+
14
+ def fresh_local_gensym(prefix)
15
+ @counter += 1
16
+ GeneratedSym.new("#{prefix}_local_#{@counter}", @counter)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'macro_lowerer'
4
+
5
+ module Kapusta
6
+ module Compiler
7
+ class MacroImporter
8
+ EXTENSIONS = %w[kapm kap fnlm fnl].freeze
9
+
10
+ def self.module_label(module_arg)
11
+ module_arg.is_a?(Symbol) ? module_arg.to_s.tr('_', '-') : module_arg.to_s
12
+ end
13
+
14
+ def initialize(path: nil, loading: nil, error_class: Kapusta::Error)
15
+ @path = path
16
+ @loading = loading || []
17
+ @error_class = error_class
18
+ end
19
+
20
+ def load(module_arg, import_form)
21
+ module_label = self.class.module_label(module_arg)
22
+ absolute_path = resolve_macro_module(module_arg) ||
23
+ raise(import_error(:import_macros_module_not_found, import_form, module: module_label))
24
+
25
+ raise import_error(:import_macros_cycle, import_form, module: module_label) if @loading.include?(absolute_path)
26
+
27
+ load_macro_module(absolute_path, module_label, import_form)
28
+ end
29
+
30
+ private
31
+
32
+ def resolve_macro_module(module_arg)
33
+ snake_stem = module_arg.to_s
34
+ kebab_stem = snake_stem.tr('_', '-')
35
+ [kebab_stem, snake_stem].uniq.each do |stem|
36
+ EXTENSIONS.each do |ext|
37
+ candidate = File.expand_path("#{stem}.#{ext}", base_dir)
38
+ return candidate if File.file?(candidate)
39
+ end
40
+ end
41
+ nil
42
+ end
43
+
44
+ def base_dir
45
+ return Dir.pwd unless @path && !@path.start_with?('(')
46
+
47
+ File.dirname(File.expand_path(@path))
48
+ end
49
+
50
+ def load_macro_module(absolute_path, module_label, import_form)
51
+ @loading.push(absolute_path)
52
+ begin
53
+ source = File.read(absolute_path)
54
+ forms = Reader.read_all(source)
55
+ rescue Kapusta::Error => e
56
+ raise e.with_defaults(path: absolute_path)
57
+ end
58
+ unless forms.last.is_a?(HashLit)
59
+ raise import_error(:import_macros_module_no_exports, import_form, module: module_label)
60
+ end
61
+
62
+ processed = forms.map { |form| MacroLowerer.lower_module_form(form, error_class: @error_class) }
63
+ wrapper = List.new([List.new([Sym.new('fn'), Vec.new([]), *processed])])
64
+ ruby = Compiler.compile_forms([wrapper], path: absolute_path)
65
+ result = TOPLEVEL_BINDING.eval(ruby, absolute_path, 1)
66
+
67
+ return result if result.is_a?(Hash)
68
+
69
+ raise import_error(:import_macros_module_no_exports, import_form, module: module_label)
70
+ ensure
71
+ @loading.pop
72
+ end
73
+
74
+ def import_error(code, form, **args)
75
+ line = form.respond_to?(:line) ? form.line : nil
76
+ column = form.respond_to?(:column) ? form.column : nil
77
+ @error_class.new(Kapusta::Errors.format(code, **args), path: @path, line:, column:)
78
+ end
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'macro_gensym'
4
+
5
+ module Kapusta
6
+ module Compiler
7
+ class MacroLowerer
8
+ FN_HEADS = %w[fn lambda λ].freeze
9
+
10
+ def self.compile(params:, body:, path:, error_class:)
11
+ callable = new(error_class:).callable_form(params, body)
12
+ ruby = Compiler.compile_forms([callable], path:)
13
+ TOPLEVEL_BINDING.eval(ruby, path, 1)
14
+ end
15
+
16
+ def self.lower_module_form(form, error_class:)
17
+ new(error_class:).lower_module_form(form)
18
+ end
19
+
20
+ def initialize(error_class:)
21
+ @error_class = error_class
22
+ @gensyms = {}
23
+ end
24
+
25
+ def callable_form(params, body)
26
+ List.new([Sym.new('fn'), params, *lowered_body_with_gensyms(body)])
27
+ end
28
+
29
+ def lower_module_form(form)
30
+ return lower_fn_form(form) if fn_form?(form)
31
+
32
+ lower(form)
33
+ end
34
+
35
+ def lower(form)
36
+ case form
37
+ when Quasiquote then copy_position(lower_quasi(form.form), form)
38
+ when Unquote, UnquoteSplice
39
+ raise @error_class, Kapusta::Errors.format(:unquote_outside_quasiquote)
40
+ when AutoGensym
41
+ raise @error_class, Kapusta::Errors.format(:auto_gensym_outside_quasiquote, name: form.name)
42
+ when List then copy_position(List.new(form.items.map { |item| lower(item) }), form)
43
+ when Vec then copy_position(Vec.new(form.items.map { |item| lower(item) }), form)
44
+ when HashLit
45
+ copy_position(
46
+ HashLit.new(form.entries.map do |entry|
47
+ entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
48
+ end),
49
+ form
50
+ )
51
+ else
52
+ form
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def lower_fn_form(form)
59
+ items = form.items
60
+ if items[1].is_a?(Sym) && items[2].is_a?(Vec)
61
+ name_sym = items[1]
62
+ params = items[2]
63
+ body = items[3..] || []
64
+ elsif items[1].is_a?(Vec)
65
+ name_sym = nil
66
+ params = items[1]
67
+ body = items[2..] || []
68
+ else
69
+ return form
70
+ end
71
+
72
+ head_items = name_sym ? [form.head, name_sym, params] : [form.head, params]
73
+ List.new(head_items + lowered_body_with_gensyms(body))
74
+ end
75
+
76
+ def lowered_body_with_gensyms(body)
77
+ lowered_body = body.map { |item| lower(item) }
78
+ wrap_gensyms(collected_gensyms, lowered_body)
79
+ end
80
+
81
+ def collected_gensyms
82
+ @gensyms.map { |prefix, sym| [sym, prefix] }
83
+ end
84
+
85
+ def wrap_gensyms(gensyms, body)
86
+ return body if gensyms.empty?
87
+
88
+ bindings = gensyms.flat_map { |sym, prefix| [sym, List.new([Sym.new('quasi-gensym'), prefix])] }
89
+ wrapped = body.length == 1 ? body[0] : List.new([Sym.new('do'), *body])
90
+ [List.new([Sym.new('let'), Vec.new(bindings), wrapped])]
91
+ end
92
+
93
+ def fn_form?(form)
94
+ form.is_a?(List) && form.head.is_a?(Sym) && FN_HEADS.include?(form.head.name)
95
+ end
96
+
97
+ def copy_position(target, source)
98
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
99
+
100
+ target.line ||= source.line
101
+ target.column ||= source.column
102
+ target
103
+ end
104
+
105
+ def lower_quasi(form)
106
+ case form
107
+ when AutoGensym then gensym_local_for(form.name)
108
+ when Sym then List.new([Sym.new('quasi-sym'), form.name])
109
+ when List then lower_quasi_list(form)
110
+ when Vec then lower_quasi_vec(form)
111
+ when HashLit then lower_quasi_hash(form)
112
+ when Unquote then lower(form.form)
113
+ when UnquoteSplice
114
+ raise @error_class, Kapusta::Errors.format(:unquote_splice_outside_list)
115
+ when Quasiquote
116
+ raise @error_class, Kapusta::Errors.format(:nested_quasiquote)
117
+ else
118
+ form
119
+ end
120
+ end
121
+
122
+ def lower_quasi_list(list)
123
+ items = list.items
124
+ return List.new([Sym.new('quasi-list')]) if items.empty?
125
+
126
+ if (tail_expr = splice_tail(items))
127
+ head_items = items[0...-1].map { |item| lower_quasi(item) }
128
+ return List.new([Sym.new('quasi-list-tail'), Vec.new(head_items), tail_expr])
129
+ end
130
+
131
+ lowered_items = items.map { |item| lower_quasi_item(item) }
132
+ List.new([Sym.new('quasi-list'), *lowered_items])
133
+ end
134
+
135
+ def lower_quasi_vec(vec)
136
+ items = vec.items
137
+ if (tail_expr = splice_tail(items))
138
+ head_items = items[0...-1].map { |item| lower_quasi(item) }
139
+ return List.new([Sym.new('quasi-vec-tail'), Vec.new(head_items), tail_expr])
140
+ end
141
+
142
+ lowered_items = items.map { |item| lower_quasi_item(item) }
143
+ List.new([Sym.new('quasi-vec'), *lowered_items])
144
+ end
145
+
146
+ def lower_quasi_hash(hash)
147
+ parts = []
148
+ hash.entries.each do |entry|
149
+ next unless entry.is_a?(Array)
150
+
151
+ key, value = entry
152
+ parts << lower_quasi(key) << lower_quasi(value)
153
+ end
154
+ List.new([Sym.new('quasi-hash'), *parts])
155
+ end
156
+
157
+ def lower_quasi_item(item)
158
+ if item.is_a?(Unquote) && unpack_call?(item.form)
159
+ inner = lower(item.form.items[1])
160
+ List.new([Sym.new('.'), inner, 0])
161
+ else
162
+ lower_quasi(item)
163
+ end
164
+ end
165
+
166
+ def splice_tail(items)
167
+ last = items.last
168
+ return unless last
169
+ return lower(last.form) if last.is_a?(UnquoteSplice)
170
+ return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
171
+
172
+ nil
173
+ end
174
+
175
+ def unpack_call?(form)
176
+ form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
177
+ end
178
+
179
+ def gensym_local_for(prefix)
180
+ @gensyms[prefix] ||= MacroGensym.fresh_local_gensym(prefix)
181
+ end
182
+ end
183
+ end
184
+ end