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.
- checksums.yaml +4 -4
- data/README.md +24 -6
- data/bin/fennel-parity +11 -4
- data/examples/classify-wallet.kap +11 -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/power-of-three.kap +12 -0
- data/examples/shared-macros.kapm +4 -0
- data/exe/kapusta-ls +14 -0
- data/kapusta.gemspec +2 -2
- data/lib/kapusta/compiler/emitter/bindings.rb +38 -4
- data/lib/kapusta/compiler/emitter/collections.rb +51 -59
- data/lib/kapusta/compiler/emitter/control_flow.rb +24 -2
- data/lib/kapusta/compiler/emitter/expressions.rb +0 -2
- data/lib/kapusta/compiler/emitter/interop.rb +2 -1
- data/lib/kapusta/compiler/emitter/patterns.rb +52 -4
- data/lib/kapusta/compiler/emitter/support.rb +1 -1
- data/lib/kapusta/compiler/emitter.rb +1 -1
- data/lib/kapusta/compiler/lua_compat.rb +149 -0
- data/lib/kapusta/compiler/macro_expander.rb +55 -141
- 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 +4 -19
- data/lib/kapusta/compiler.rb +4 -2
- data/lib/kapusta/errors.rb +9 -3
- data/lib/kapusta/formatter.rb +4 -0
- 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 +312 -0
- data/lib/kapusta/reader.rb +0 -2
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_errors_spec.rb +142 -1
- data/spec/examples_spec.rb +12 -0
- data/spec/lsp_spec.rb +603 -0
- 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
|
-
|
|
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)]
|
|
@@ -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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
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
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
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
|
-
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
281
|
-
|
|
282
|
-
|
|
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
|