kapusta 0.7.0 → 0.9.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/README.md +1 -1
- data/bin/fennel-parity +10 -4
- data/examples/hit-counter.kap +17 -0
- 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/parking-system.kap +18 -0
- data/examples/shared-macros.kapm +4 -0
- data/examples/thread-styles.kap +41 -0
- data/lib/kapusta/ast.rb +1 -1
- 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/compiler/normalizer.rb +22 -12
- data/lib/kapusta/errors.rb +6 -1
- data/lib/kapusta/formatter.rb +9 -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 +693 -0
- data/lib/kapusta/lsp/workspace_index.rb +225 -0
- data/lib/kapusta/lsp.rb +102 -48
- data/lib/kapusta/reader.rb +28 -0
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +17 -1
- data/spec/examples_spec.rb +25 -0
- data/spec/lsp_spec.rb +621 -15
- metadata +19 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 520158590f1244ac17f70b58ded945f66be00060f6da89cebb366fefaf27b62e
|
|
4
|
+
data.tar.gz: 6773e0faa320f23cb15106e710f380a39093c9fc6e56cead8941c28e0ccf16c5
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 814d37c473287d2d1e54568c7d21c22d88703cd89da761100c1f20950e47cb18eb2ac4807aa596878ded72eb7049a6083d2b9de46bc76ce2ea55787f6993f61f
|
|
7
|
+
data.tar.gz: 3a15fc05522e617cf46e8709a55dc8497252503f2b995dffaa4f8995fc34a54e9bcc2a711cd501884203bf0113c7924f159377459de8f2524e9c371c018d8238
|
data/README.md
CHANGED
|
@@ -104,7 +104,7 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
|
|
|
104
104
|
Kapusta-specific additions:
|
|
105
105
|
|
|
106
106
|
- `module` and `class` for Ruby host structure, including file-header forms
|
|
107
|
-
- `ivar`
|
|
107
|
+
- `ivar` or `@var`) / `cvar` or `@@var` / `gvar` or `$var`
|
|
108
108
|
- `try` / `catch` / `finally` plus `raise` for exceptions
|
|
109
109
|
- `(ruby "...")` raw host escape hatch
|
|
110
110
|
- a trailing symbol-keyed hash is emitted as Ruby keyword arguments
|
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
|
|
@@ -40,17 +43,20 @@ COMPATIBLE = %w[
|
|
|
40
43
|
shapes.kap
|
|
41
44
|
squares.kap
|
|
42
45
|
sum.kap
|
|
46
|
+
thread-styles.kap
|
|
43
47
|
tic-tac-toe.kap
|
|
44
48
|
underscore-patterns.kap
|
|
45
49
|
].freeze
|
|
46
50
|
|
|
47
|
-
def run(cmd, file, chdir: EXAMPLES)
|
|
48
|
-
out, err, status = Open3.capture3(cmd, file, chdir:)
|
|
51
|
+
def run(cmd, file, chdir: EXAMPLES, env: {})
|
|
52
|
+
out, err, status = Open3.capture3(env, cmd, file, chdir:)
|
|
49
53
|
[out, err, status]
|
|
50
54
|
rescue StandardError => e
|
|
51
55
|
['', e.message, nil]
|
|
52
56
|
end
|
|
53
57
|
|
|
58
|
+
FENNEL_ENV = { 'FENNEL_MACRO_PATH' => './?.kapm;./?.kap' }.freeze
|
|
59
|
+
|
|
54
60
|
def strip_outer_quotes(line)
|
|
55
61
|
if line.length >= 2 && line.start_with?('"') && line.end_with?('"')
|
|
56
62
|
line[1..-2]
|
|
@@ -100,7 +106,7 @@ end
|
|
|
100
106
|
|
|
101
107
|
def check_run(path)
|
|
102
108
|
k_out, k_err, k_status = run(KAPUSTA, path)
|
|
103
|
-
f_out, f_err, f_status = run('fennel', path)
|
|
109
|
+
f_out, f_err, f_status = run('fennel', path, env: FENNEL_ENV)
|
|
104
110
|
|
|
105
111
|
return "kapusta exited #{k_status&.exitstatus}: #{k_err.strip}" if k_status.nil? || !k_status.success?
|
|
106
112
|
return "fennel exited #{f_status&.exitstatus}: #{f_err.strip}" if f_status.nil? || !f_status.success?
|
|
@@ -132,7 +138,7 @@ end
|
|
|
132
138
|
def check_error(name)
|
|
133
139
|
path = File.join(EXAMPLES_ERRORS, name)
|
|
134
140
|
_, _, k_status = run(KAPUSTA, path, chdir: EXAMPLES_ERRORS)
|
|
135
|
-
_, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS)
|
|
141
|
+
_, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS, env: FENNEL_ENV)
|
|
136
142
|
|
|
137
143
|
return [:fail, 'fennel unexpectedly succeeded'] if f_status&.success?
|
|
138
144
|
return [:fail, "kapusta unexpectedly succeeded (exit #{k_status&.exitstatus})"] if k_status&.success?
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
(class HitCounter)
|
|
2
|
+
|
|
3
|
+
(set @@total 0)
|
|
4
|
+
|
|
5
|
+
(fn initialize [name] (set @name name))
|
|
6
|
+
|
|
7
|
+
(fn hit []
|
|
8
|
+
(set @@total (+ @@total 1))
|
|
9
|
+
(set $last-hitter @name)
|
|
10
|
+
@@total)
|
|
11
|
+
|
|
12
|
+
(let [a (HitCounter.new "alice")
|
|
13
|
+
b (HitCounter.new "bob")]
|
|
14
|
+
(print (a.hit))
|
|
15
|
+
(print (b.hit))
|
|
16
|
+
(print (a.hit))
|
|
17
|
+
(print $last-hitter))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
(class ParkingSystem)
|
|
2
|
+
|
|
3
|
+
(fn initialize [big medium small]
|
|
4
|
+
(set @big big)
|
|
5
|
+
(set @medium medium)
|
|
6
|
+
(set @small small))
|
|
7
|
+
|
|
8
|
+
(fn add-car [car-type]
|
|
9
|
+
(if (and (= car-type 1) (> @big 0)) (do (set @big (- @big 1)) true)
|
|
10
|
+
(and (= car-type 2) (> @medium 0)) (do (set @medium (- @medium 1)) true)
|
|
11
|
+
(and (= car-type 3) (> @small 0)) (do (set @small (- @small 1)) true)
|
|
12
|
+
false))
|
|
13
|
+
|
|
14
|
+
(let [parking (ParkingSystem.new 1 1 0)]
|
|
15
|
+
(print (parking.add-car 1))
|
|
16
|
+
(print (parking.add-car 2))
|
|
17
|
+
(print (parking.add-car 3))
|
|
18
|
+
(print (parking.add-car 1)))
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
(fn positive? [n] (> n 0))
|
|
2
|
+
(fn square [n] (* n n))
|
|
3
|
+
(fn add [x y] (+ x y))
|
|
4
|
+
(fn mul [x y] (* x y))
|
|
5
|
+
(fn nonzero [n] (if (= n 0) nil n))
|
|
6
|
+
(fn non-empty [s] (if (= s "") nil s))
|
|
7
|
+
(fn wrap [s] (.. ">>" s "<<"))
|
|
8
|
+
(fn shout [s] (.. s "!"))
|
|
9
|
+
|
|
10
|
+
(fn keep [pred xs]
|
|
11
|
+
(icollect [_ x (ipairs xs)]
|
|
12
|
+
(when (pred x) x)))
|
|
13
|
+
|
|
14
|
+
(fn map [f xs]
|
|
15
|
+
(icollect [_ x (ipairs xs)]
|
|
16
|
+
(f x)))
|
|
17
|
+
|
|
18
|
+
(fn join [sep xs]
|
|
19
|
+
(var s "")
|
|
20
|
+
(each [_ x (ipairs xs)]
|
|
21
|
+
(if (= s "")
|
|
22
|
+
(set s (.. x))
|
|
23
|
+
(set s (.. s sep x))))
|
|
24
|
+
s)
|
|
25
|
+
|
|
26
|
+
(let [scores [-2 3 -1 4 0 5]
|
|
27
|
+
report (->> scores
|
|
28
|
+
(keep positive?)
|
|
29
|
+
(map square)
|
|
30
|
+
(join ", "))
|
|
31
|
+
adjusted (-> 7 (add 3) (mul 2) (square))
|
|
32
|
+
ok (-?> "hello" (non-empty) (wrap) (shout))
|
|
33
|
+
bad (-?> "" (non-empty) (wrap) (shout))
|
|
34
|
+
live (-?>> 5 (nonzero) (mul 3) (add 1))
|
|
35
|
+
dead (-?>> 0 (nonzero) (mul 3) (add 1))]
|
|
36
|
+
(print report)
|
|
37
|
+
(print adjusted)
|
|
38
|
+
(print ok)
|
|
39
|
+
(print bad)
|
|
40
|
+
(print live)
|
|
41
|
+
(print dead))
|
data/lib/kapusta/ast.rb
CHANGED
|
@@ -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
|