kapusta 0.7.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76f35530eebc269dfec62ee1f36f6cc11a252ee4590eeac961c9b47e4e49ca09
4
- data.tar.gz: a0ee60d21fb9a6066915d916e465bef09d9208c0469712673db35bea83efd7c4
3
+ metadata.gz: f64991d78295bfc4a633136af86607f2d9536f3e113ae9ea819a0c5708837427
4
+ data.tar.gz: 0d656fcf3afefbb2c89acad7a0fa4de8a57f8a484c2ce887e5bc70e7493c71ea
5
5
  SHA512:
6
- metadata.gz: 350e111e7cfe045723ea1982c18762727d64ea615219d904a79af2933874af5ce355076e242ed9c0bf5e91d135dbd4498b2b5ae107b05035f205e3ccb87bbb54
7
- data.tar.gz: 7e242c3fb28fb0d5d2839f7e3d6e5dca96dda90204e1a81024111e1fc5ef4f6c7abb2a6e3628e5894eac46ce9d1c10f21db80bcae63da3ec9b4874a747b1bba1
6
+ metadata.gz: a27e51a26db8069c998d7e0b586beb4fda1505c2f13f572b82bbbda92bc79dbe6e3d4e308547877a0e455cd56b5c9f1c226830ca2645664dae65b47b552dc3a1
7
+ data.tar.gz: e6e675b9be4492d7bf75e341915bfd5e022a733240a2b7c4139c2904a99577207a7b4d182208ad1706c31bcded3ebaf3a50e03d18f27eafc68373b96141ae8b0
data/bin/fennel-parity CHANGED
@@ -24,6 +24,9 @@ COMPATIBLE = %w[
24
24
  hashfn.kap
25
25
  leap-year.kap
26
26
  macros-dbg.kap
27
+ macros-import-helpers.kap
28
+ macros-import-whole.kap
29
+ macros-import.kap
27
30
  macros-multi.kap
28
31
  macros-swap.kap
29
32
  macros-thrice-if.kap
@@ -44,13 +47,15 @@ COMPATIBLE = %w[
44
47
  underscore-patterns.kap
45
48
  ].freeze
46
49
 
47
- def run(cmd, file, chdir: EXAMPLES)
48
- out, err, status = Open3.capture3(cmd, file, chdir:)
50
+ def run(cmd, file, chdir: EXAMPLES, env: {})
51
+ out, err, status = Open3.capture3(env, cmd, file, chdir:)
49
52
  [out, err, status]
50
53
  rescue StandardError => e
51
54
  ['', e.message, nil]
52
55
  end
53
56
 
57
+ FENNEL_ENV = { 'FENNEL_MACRO_PATH' => './?.kapm;./?.kap' }.freeze
58
+
54
59
  def strip_outer_quotes(line)
55
60
  if line.length >= 2 && line.start_with?('"') && line.end_with?('"')
56
61
  line[1..-2]
@@ -100,7 +105,7 @@ end
100
105
 
101
106
  def check_run(path)
102
107
  k_out, k_err, k_status = run(KAPUSTA, path)
103
- f_out, f_err, f_status = run('fennel', path)
108
+ f_out, f_err, f_status = run('fennel', path, env: FENNEL_ENV)
104
109
 
105
110
  return "kapusta exited #{k_status&.exitstatus}: #{k_err.strip}" if k_status.nil? || !k_status.success?
106
111
  return "fennel exited #{f_status&.exitstatus}: #{f_err.strip}" if f_status.nil? || !f_status.success?
@@ -132,7 +137,7 @@ end
132
137
  def check_error(name)
133
138
  path = File.join(EXAMPLES_ERRORS, name)
134
139
  _, _, k_status = run(KAPUSTA, path, chdir: EXAMPLES_ERRORS)
135
- _, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS)
140
+ _, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS, env: FENNEL_ENV)
136
141
 
137
142
  return [:fail, 'fennel unexpectedly succeeded'] if f_status&.success?
138
143
  return [:fail, "kapusta unexpectedly succeeded (exit #{k_status&.exitstatus})"] if k_status&.success?
@@ -0,0 +1,9 @@
1
+ (local FACTOR 10)
2
+
3
+ (fn double [x]
4
+ (* x 2))
5
+
6
+ (fn scaled [x]
7
+ `(* ,x ,(double FACTOR)))
8
+
9
+ {: scaled}
@@ -0,0 +1,3 @@
1
+ (import-macros {: scaled} :import-helpers)
2
+
3
+ (print (scaled 3))
@@ -0,0 +1,5 @@
1
+ (import-macros mine :shared-macros)
2
+
3
+ (var n 5)
4
+ (mine.inc-by n 2)
5
+ (print n)
@@ -0,0 +1,6 @@
1
+ (import-macros {: inc-by} :shared-macros)
2
+
3
+ (var counter 0)
4
+ (inc-by counter 5)
5
+ (inc-by counter 3)
6
+ (print counter)
@@ -0,0 +1,4 @@
1
+ (fn inc-by [target n]
2
+ `(set ,target (+ ,target ,n)))
3
+
4
+ {: inc-by}
@@ -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)]
@@ -90,7 +92,8 @@ module Kapusta
90
92
  register_macros_form(list.rest)
91
93
  return List.new([Sym.new('do')])
92
94
  when 'import-macros'
93
- raise macro_error(:import_macros_unsupported, list)
95
+ handle_import_macros(list)
96
+ return List.new([Sym.new('do')])
94
97
  end
95
98
 
96
99
  key = lookup_key(name)
@@ -136,152 +139,61 @@ module Kapusta
136
139
  value.is_a?(List) && value.head.is_a?(Sym) && %w[fn lambda λ].include?(value.head.name)
137
140
  end
138
141
 
139
- def register(source_name, params, body)
140
- proc_obj = compile_macro(source_name, params, body)
141
- @macros[lookup_key(source_name)] = proc_obj
142
- end
143
-
144
- def compile_macro(name, params, body)
145
- lowering = Lowering.new
146
- lowered_body = body.map { |form| lowering.lower(form) }
147
- gensym_locals = lowering.collected_gensyms
148
-
149
- wrapped =
150
- if gensym_locals.empty?
151
- List.new([Sym.new('fn'), params, *lowered_body])
152
- else
153
- let_bindings = gensym_locals.flat_map do |gensym_sym, prefix|
154
- [gensym_sym, List.new([Sym.new('quasi-gensym'), prefix])]
155
- end
156
- inner = lowered_body.length == 1 ? lowered_body.first : List.new([Sym.new('do'), *lowered_body])
157
- List.new([Sym.new('fn'), params, List.new([Sym.new('let'), Vec.new(let_bindings), inner])])
158
- end
159
-
160
- macro_path = @path || "(macro #{name})"
161
- ruby = Compiler.compile_forms([wrapped], path: macro_path)
162
- TOPLEVEL_BINDING.eval(ruby, macro_path, 1)
163
- end
164
-
165
- def invoke_macro(key, args)
166
- proc_obj = @macros[key]
167
- proc_obj.call(*args)
168
- end
169
-
170
- class Lowering
171
- def initialize
172
- @gensyms = {}
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)
173
148
  end
174
-
175
- def collected_gensyms
176
- @gensyms.map { |prefix, sym| [sym, prefix] }
149
+ unless module_arg.is_a?(Symbol) || module_arg.is_a?(String)
150
+ raise macro_error(:import_macros_module_invalid, form)
177
151
  end
178
152
 
179
- def lower(form)
180
- case form
181
- when Quasiquote then copy_position(lower_quasi(form.form), form)
182
- when Unquote, UnquoteSplice
183
- raise Error, Kapusta::Errors.format(:unquote_outside_quasiquote)
184
- when AutoGensym
185
- raise Error, Kapusta::Errors.format(:auto_gensym_outside_quasiquote, name: form.name)
186
- when List then copy_position(List.new(form.items.map { |item| lower(item) }), form)
187
- when Vec then copy_position(Vec.new(form.items.map { |item| lower(item) }), form)
188
- when HashLit
189
- copy_position(
190
- HashLit.new(form.entries.map do |entry|
191
- entry.is_a?(Array) ? [lower(entry[0]), lower(entry[1])] : entry
192
- end),
193
- form
194
- )
195
- else
196
- form
197
- end
198
- end
199
-
200
- def copy_position(target, source)
201
- return target unless target.respond_to?(:line=) && source.respond_to?(:line)
202
-
203
- target.line ||= source.line
204
- target.column ||= source.column
205
- target
206
- end
207
-
208
- def lower_quasi(form)
209
- case form
210
- when AutoGensym then gensym_local_for(form.name)
211
- when Sym then List.new([Sym.new('quasi-sym'), form.name])
212
- when List then lower_quasi_list(form)
213
- when Vec then lower_quasi_vec(form)
214
- when HashLit then lower_quasi_hash(form)
215
- when Unquote then lower(form.form)
216
- when UnquoteSplice
217
- raise Error, Kapusta::Errors.format(:unquote_splice_outside_list)
218
- when Quasiquote
219
- raise Error, Kapusta::Errors.format(:nested_quasiquote)
220
- else
221
- form
222
- end
223
- end
224
-
225
- def lower_quasi_list(list)
226
- items = list.items
227
- return List.new([Sym.new('quasi-list')]) if items.empty?
228
-
229
- if (tail_expr = splice_tail(items))
230
- head_items = items[0...-1].map { |item| lower_quasi(item) }
231
- return List.new([Sym.new('quasi-list-tail'), Vec.new(head_items), tail_expr])
232
- end
233
-
234
- lowered_items = items.map { |item| lower_quasi_item(item) }
235
- 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)
236
159
  end
160
+ end
237
161
 
238
- def lower_quasi_vec(vec)
239
- items = vec.items
240
- if (tail_expr = splice_tail(items))
241
- head_items = items[0...-1].map { |item| lower_quasi(item) }
242
- return List.new([Sym.new('quasi-vec-tail'), Vec.new(head_items), tail_expr])
243
- end
244
-
245
- lowered_items = items.map { |item| lower_quasi_item(item) }
246
- List.new([Sym.new('quasi-vec'), *lowered_items])
247
- end
162
+ def macro_importer
163
+ @macro_importer ||= MacroImporter.new(path: @path, loading: @loading, error_class: Error)
164
+ end
248
165
 
249
- def lower_quasi_hash(hash)
250
- parts = []
251
- hash.entries.each do |entry|
252
- 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)
253
169
 
254
- key, value = entry
255
- parts << lower_quasi(key) << lower_quasi(value)
256
- end
257
- 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
258
174
  end
175
+ end
259
176
 
260
- def lower_quasi_item(item)
261
- if item.is_a?(Unquote) && unpack_call?(item.form)
262
- inner = lower(item.form.items[1])
263
- List.new([Sym.new('.'), inner, 0])
264
- else
265
- lower_quasi(item)
266
- 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
267
181
  end
182
+ end
268
183
 
269
- def splice_tail(items)
270
- last = items.last
271
- return unless last
272
- return lower(last.form) if last.is_a?(UnquoteSplice)
273
- return lower(last.form.items[1]) if last.is_a?(Unquote) && unpack_call?(last.form)
274
-
275
- nil
276
- 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
277
188
 
278
- def unpack_call?(form)
279
- form.is_a?(List) && form.head.is_a?(Sym) && form.head.name == 'unpack'
280
- 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
281
193
 
282
- def gensym_local_for(prefix)
283
- @gensyms[prefix] ||= MacroExpander.fresh_local_gensym(prefix)
284
- end
194
+ def invoke_macro(key, args)
195
+ proc_obj = @macros[key]
196
+ proc_obj.call(*args)
285
197
  end
286
198
  end
287
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
@@ -30,7 +30,12 @@ module Kapusta
30
30
  global_non_symbol_name: 'unable to bind %{type} %{value}',
31
31
  icollect_no_iterator: 'expected iterator binding table',
32
32
  if_no_body: 'expected condition and body',
33
- import_macros_unsupported: 'import-macros is not yet supported',
33
+ import_macros_cycle: 'import-macros cycle detected for module %{module}',
34
+ import_macros_destructure_invalid: 'import-macros expects a hash literal as first argument',
35
+ import_macros_macro_not_found: 'import-macros: macro %{macro} not exported by module %{module}',
36
+ import_macros_module_invalid: 'import-macros expects a symbol or string module name',
37
+ import_macros_module_no_exports: 'import-macros: module %{module} has no export table',
38
+ import_macros_module_not_found: 'import-macros: module %{module} not found',
34
39
  invalid_class_name: 'invalid class name: %{name}',
35
40
  invalid_module_name: 'invalid module name: %{name}',
36
41
  let_no_body: 'expected body expression',