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.
@@ -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..].reduce(forms.first) do |memo, form|
108
- temp = thread_temp
109
- List.new([
110
- Sym.new('let'),
111
- Vec.new([temp, memo]),
112
- List.new([
113
- Sym.new('if'),
114
- List.new([Sym.new('='), temp, nil]),
115
- nil,
116
- thread_step(temp, form, position)
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)
@@ -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',
@@ -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