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
|
@@ -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
|
|
@@ -104,19 +104,29 @@ module Kapusta
|
|
|
104
104
|
end
|
|
105
105
|
|
|
106
106
|
def thread_short(forms, position)
|
|
107
|
-
forms[1..]
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
107
|
+
steps = forms[1..]
|
|
108
|
+
return forms.first if steps.empty?
|
|
109
|
+
|
|
110
|
+
prev_temp = thread_temp
|
|
111
|
+
binding_items = [prev_temp, forms.first]
|
|
112
|
+
body = nil
|
|
113
|
+
last_index = steps.length - 1
|
|
114
|
+
steps.each_with_index do |form, i|
|
|
115
|
+
guarded = List.new([
|
|
116
|
+
Sym.new('if'),
|
|
117
|
+
List.new([Sym.new('='), prev_temp, nil]),
|
|
118
|
+
nil,
|
|
119
|
+
thread_step(prev_temp, form, position)
|
|
120
|
+
])
|
|
121
|
+
if i == last_index
|
|
122
|
+
body = guarded
|
|
123
|
+
else
|
|
124
|
+
temp = thread_temp
|
|
125
|
+
binding_items.push(temp, guarded)
|
|
126
|
+
prev_temp = temp
|
|
127
|
+
end
|
|
119
128
|
end
|
|
129
|
+
List.new([Sym.new('let'), Vec.new(binding_items), body])
|
|
120
130
|
end
|
|
121
131
|
|
|
122
132
|
def thread_step(memo, form, position)
|
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',
|
data/lib/kapusta/formatter.rb
CHANGED
|
@@ -178,7 +178,7 @@ module Kapusta
|
|
|
178
178
|
|
|
179
179
|
case form
|
|
180
180
|
when Comment then form.text
|
|
181
|
-
when List then render_list(form, indent, top_level:)
|
|
181
|
+
when List then form.sigil ? render_sigil(form) : render_list(form, indent, top_level:)
|
|
182
182
|
when Vec then render_vec(form, indent, layout:, top_level:, force_expand:)
|
|
183
183
|
when HashLit then render_hash(form, indent)
|
|
184
184
|
when Quasiquote then render_prefix('`', form.form, indent, force_expand:)
|
|
@@ -189,6 +189,13 @@ module Kapusta
|
|
|
189
189
|
end
|
|
190
190
|
end
|
|
191
191
|
|
|
192
|
+
SIGIL_PREFIXES = { ivar: '@', cvar: '@@', gvar: '$' }.freeze
|
|
193
|
+
private_constant :SIGIL_PREFIXES
|
|
194
|
+
|
|
195
|
+
def render_sigil(list)
|
|
196
|
+
"#{SIGIL_PREFIXES.fetch(list.sigil)}#{list.items[1].name}"
|
|
197
|
+
end
|
|
198
|
+
|
|
192
199
|
def render_prefix(prefix, inner, indent, force_expand: false)
|
|
193
200
|
rendered = render(inner, indent + prefix.length, force_expand:)
|
|
194
201
|
lines = rendered.lines(chomp: true)
|
|
@@ -221,6 +228,7 @@ module Kapusta
|
|
|
221
228
|
|
|
222
229
|
"{#{rendered.join(' ')}}"
|
|
223
230
|
when List
|
|
231
|
+
return render_sigil(form) if form.sigil
|
|
224
232
|
return if contains_comments?(form.items)
|
|
225
233
|
return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
|
|
226
234
|
return if multiline_in_source?(form)
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'rename'
|
|
4
|
+
|
|
5
|
+
module Kapusta
|
|
6
|
+
class LSP
|
|
7
|
+
module Definition
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def find(uri, text, line_zero, character, workspace_index:)
|
|
11
|
+
target = Rename.locate(text, line_zero, character)
|
|
12
|
+
return unless target
|
|
13
|
+
|
|
14
|
+
case target.kind
|
|
15
|
+
when :local, :toplevel_fn, :constant
|
|
16
|
+
location_for_binding(uri, target.binding) if target.binding
|
|
17
|
+
when :macro
|
|
18
|
+
locations_for_macro(uri, target.binding, workspace_index)
|
|
19
|
+
when :free_toplevel
|
|
20
|
+
locations_for_toplevel(target.name, workspace_index)
|
|
21
|
+
when :free_constant
|
|
22
|
+
locations_for_constant(target.segment_prefix, workspace_index)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def locations_for_macro(uri, binding, workspace_index)
|
|
27
|
+
return unless binding
|
|
28
|
+
|
|
29
|
+
case binding.kind
|
|
30
|
+
when :macro
|
|
31
|
+
location_for_binding(uri, binding)
|
|
32
|
+
when :macro_import
|
|
33
|
+
def_uri, def_binding = workspace_index.find_macro_definition(
|
|
34
|
+
uri, binding.import_module, binding.import_key
|
|
35
|
+
)
|
|
36
|
+
location_for_binding(def_uri, def_binding) if def_uri && def_binding
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def location_for_binding(uri, binding)
|
|
41
|
+
{ uri:, range: binding_range(binding) }
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def locations_for_toplevel(name, workspace_index)
|
|
45
|
+
defs = workspace_index.toplevel_fn_definitions(name)
|
|
46
|
+
return if defs.empty?
|
|
47
|
+
|
|
48
|
+
defs.map { |uri, b| { uri:, range: binding_range(b) } }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def locations_for_constant(prefix, workspace_index)
|
|
52
|
+
defs = workspace_index.constant_definitions_with_prefix(prefix)
|
|
53
|
+
return if defs.empty?
|
|
54
|
+
|
|
55
|
+
defs.map { |uri, b| { uri:, range: binding_range(b) } }
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def binding_range(binding)
|
|
59
|
+
line = binding.line - 1
|
|
60
|
+
{
|
|
61
|
+
start: { line:, character: binding.column - 1 },
|
|
62
|
+
end: { line:, character: binding.end_column - 1 }
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Kapusta
|
|
4
|
+
class LSP
|
|
5
|
+
module Diagnostics
|
|
6
|
+
SEVERITY_ERROR = 1
|
|
7
|
+
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def collect(text, path)
|
|
11
|
+
Kapusta.compile(text, path: path || '(buffer)')
|
|
12
|
+
[]
|
|
13
|
+
rescue Kapusta::Error => e
|
|
14
|
+
[diagnostic_from(e, text)]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def diagnostic_from(error, text)
|
|
18
|
+
line = [(error.line || 1) - 1, 0].max
|
|
19
|
+
column = [(error.column || 1) - 1, 0].max
|
|
20
|
+
|
|
21
|
+
{
|
|
22
|
+
range: {
|
|
23
|
+
start: { line:, character: column },
|
|
24
|
+
end: { line:, character: column + token_length(text, line, column) }
|
|
25
|
+
},
|
|
26
|
+
severity: SEVERITY_ERROR,
|
|
27
|
+
source: 'kapusta-ls',
|
|
28
|
+
message: error.reason
|
|
29
|
+
}
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def token_length(text, line, column)
|
|
33
|
+
source_line = text.lines[line]
|
|
34
|
+
return 1 unless source_line
|
|
35
|
+
|
|
36
|
+
tail = source_line[column..] || ''
|
|
37
|
+
match = tail.match(/\A[^\s()\[\]{}";`,]+/)
|
|
38
|
+
match && match[0].length.positive? ? match[0].length : 1
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../formatter'
|
|
4
|
+
|
|
5
|
+
module Kapusta
|
|
6
|
+
class LSP
|
|
7
|
+
module Formatting
|
|
8
|
+
module_function
|
|
9
|
+
|
|
10
|
+
def text_edits(text, path)
|
|
11
|
+
formatted = Kapusta::Formatter.format(text, path:)
|
|
12
|
+
return [] if formatted == text
|
|
13
|
+
|
|
14
|
+
[{ range: full_range(text), newText: formatted }]
|
|
15
|
+
rescue Kapusta::Error
|
|
16
|
+
[]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def full_range(text)
|
|
20
|
+
lines = text.split("\n", -1)
|
|
21
|
+
end_line = [lines.length - 1, 0].max
|
|
22
|
+
end_character = lines.last ? lines.last.length : 0
|
|
23
|
+
{
|
|
24
|
+
start: { line: 0, character: 0 },
|
|
25
|
+
end: { line: end_line, character: end_character }
|
|
26
|
+
}
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../compiler'
|
|
4
|
+
|
|
5
|
+
module Kapusta
|
|
6
|
+
class LSP
|
|
7
|
+
module Identifier
|
|
8
|
+
DELIM_CHARS = '()[]{}";`,'
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def valid_local?(name)
|
|
13
|
+
return false if name.nil? || name.empty?
|
|
14
|
+
return false if name.match?(/\s/)
|
|
15
|
+
return false if name.match?(/[#{Regexp.escape(DELIM_CHARS)}]/o)
|
|
16
|
+
return false if name.match?(/\A-?\d/)
|
|
17
|
+
return false if name.include?('.')
|
|
18
|
+
return false if Kapusta::Compiler::SPECIAL_FORMS.include?(name)
|
|
19
|
+
|
|
20
|
+
true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def valid_constant_segment?(segment)
|
|
24
|
+
!segment.nil? && segment.match?(/\A[A-Z]\w*\z/)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|