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 +4 -4
- data/bin/fennel-parity +9 -4
- data/examples/import-helpers.kapm +9 -0
- data/examples/macros-import-helpers.kap +3 -0
- data/examples/macros-import-whole.kap +5 -0
- data/examples/macros-import.kap +6 -0
- data/examples/shared-macros.kapm +4 -0
- data/lib/kapusta/compiler/macro_expander.rb +54 -142
- data/lib/kapusta/compiler/macro_gensym.rb +21 -0
- data/lib/kapusta/compiler/macro_importer.rb +81 -0
- data/lib/kapusta/compiler/macro_lowerer.rb +184 -0
- data/lib/kapusta/errors.rb +6 -1
- data/lib/kapusta/lsp/definition.rb +67 -0
- data/lib/kapusta/lsp/diagnostics.rb +42 -0
- data/lib/kapusta/lsp/formatting.rb +30 -0
- data/lib/kapusta/lsp/identifier.rb +28 -0
- data/lib/kapusta/lsp/rename.rb +417 -0
- data/lib/kapusta/lsp/scope_walker.rb +643 -0
- data/lib/kapusta/lsp/workspace_index.rb +225 -0
- data/lib/kapusta/lsp.rb +102 -48
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +17 -1
- data/spec/examples_spec.rb +12 -0
- data/spec/lsp_spec.rb +535 -15
- metadata +16 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f64991d78295bfc4a633136af86607f2d9536f3e113ae9ea819a0c5708837427
|
|
4
|
+
data.tar.gz: 0d656fcf3afefbb2c89acad7a0fa4de8a57f8a484c2ce887e5bc70e7493c71ea
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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?
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
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
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
283
|
-
|
|
284
|
-
|
|
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
|
data/lib/kapusta/errors.rb
CHANGED
|
@@ -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
|
-
|
|
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',
|