kapusta 0.12.0 → 0.13.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d5e4a7dc8e3d044580b58064b4632706c2115964847d674c23bbe39a625e0b6e
4
- data.tar.gz: b17a395b81ab568829022273e5df9ee5a186c052d6a1e4c843df631dfe1d6f76
3
+ metadata.gz: b547075d7dcb73121e61dc17577cb8262265eaffb6ee0d74b88a74ce83c8e09a
4
+ data.tar.gz: 877f490eca3f0d4419e38b4619f77470aff18ddd8fede555f2963beb8db5994f
5
5
  SHA512:
6
- metadata.gz: d3e8ca629130a695a67a1c210ab06869518e2d232084e01a8cb6d180094e2b10b0cbc3f65c191a9a7a6b1e70727f1563f28e3637264cc7776a58c2e16e554991
7
- data.tar.gz: 71695c0adf8e37705c9b9b85263e495578bcdd0d442c2d56ad09b1a6386640003164adf98d932c499472ae81d5c3d4a909898b907417e6d6463384b855b2fa33
6
+ metadata.gz: cddc220382f4c7f59788f4624f9e86f55bc58fe2b3fa6978f4e6d8bebaa870ff73be57786ecf2db94f14d6db460b3f485b6f82195e7fd21e255c7a7612ab50c2
7
+ data.tar.gz: 6c653e095b1c3d406f95e7f1659b615a2a59eaa21ef77e709cfc2dfc398d3526fb493f5054cb541acd36b203e690dbc28626e951e3e8f9d5e27366c0d5885834
data/README.md CHANGED
@@ -87,6 +87,7 @@ def ack(m, n)
87
87
  ack(m - 1, ack(m, n - 1))
88
88
  end
89
89
  end
90
+
90
91
  p ack(2, 3)
91
92
  p ack(3, 3)
92
93
  ```
@@ -15,7 +15,7 @@
15
15
 
16
16
  (fn initialize [root]
17
17
  (set @stack [])
18
- (self.push-left root))
18
+ (push-left root))
19
19
 
20
20
  (fn push-left [node]
21
21
  (var n node)
@@ -27,7 +27,7 @@
27
27
  (fn next []
28
28
  (let [stack @stack
29
29
  node (stack.pop)]
30
- (self.push-left (node.right))
30
+ (push-left (node.right))
31
31
  (node.val)))
32
32
 
33
33
  (fn has-next? []
@@ -2,4 +2,4 @@
2
2
  (fn initialize [name] (set (ivar name) name))
3
3
  (fn name [] (ivar name))
4
4
  (fn kingdom [] "animalia")
5
- (fn label [] (.. (self.name) " the animal")))
5
+ (fn label [] (.. name " the animal")))
@@ -1,7 +1,7 @@
1
1
  (require "./zoo-animal-1")
2
2
 
3
3
  (class Zoo.Dog [Zoo.Animal]
4
- (fn label [] (.. (self.name) " the dog"))
4
+ (fn label [] (.. name " the dog"))
5
5
  (fn bark [] "woof"))
6
6
 
7
7
  (let [dog (Zoo.Dog.new "Poppy")]
@@ -81,9 +81,21 @@ module Kapusta
81
81
  end
82
82
 
83
83
  def emit_definition_form(form, env, current_scope)
84
- return [emit_method_definition(form, env), env] unless current_scope == :toplevel
84
+ return emit_toplevel_method_definition(form, env) if current_scope == :toplevel
85
85
 
86
- emit_toplevel_method_definition(form, env)
86
+ code = emit_method_definition(form, env)
87
+ register_self_method_binding(form, env)
88
+ [code, env]
89
+ end
90
+
91
+ def register_self_method_binding(form, env)
92
+ name_sym = form.items[1]
93
+ return unless name_sym.is_a?(Sym) && !name_sym.dotted?
94
+
95
+ ruby_name = Kapusta.kebab_to_snake(name_sym.name)
96
+ return unless direct_method_name?(ruby_name)
97
+
98
+ env.define(name_sym.name, Env::SelfMethodBinding.new(ruby_name))
87
99
  end
88
100
 
89
101
  def emit_toplevel_method_definition(form, env)
@@ -234,7 +246,7 @@ module Kapusta
234
246
 
235
247
  binding = env.lookup_if_defined(name)
236
248
  return false if binding.nil?
237
- return false if method_binding?(binding)
249
+ return false if callable_method_binding?(binding)
238
250
  return false if constant_binding?(binding)
239
251
 
240
252
  true
@@ -385,7 +397,7 @@ module Kapusta
385
397
  binding = env.lookup_if_defined(target.name)
386
398
  ruby_name =
387
399
  if binding
388
- emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
400
+ emit_error!(:cannot_set_method_binding, name: target.name) if callable_method_binding?(binding)
389
401
 
390
402
  binding
391
403
  else
@@ -429,7 +441,7 @@ module Kapusta
429
441
  end
430
442
  else
431
443
  binding = env.lookup(target.name)
432
- emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
444
+ emit_error!(:cannot_set_method_binding, name: target.name) if callable_method_binding?(binding)
433
445
  emit_error!(:expected_var, name: target.name) unless mutable_binding?(env, target.name)
434
446
 
435
447
  emit_assignment(binding, value_code)
@@ -80,13 +80,17 @@ module Kapusta
80
80
  end
81
81
 
82
82
  def emit_module_expr(args, env)
83
- body = emit_sequence(args[1..], env, :module, allow_method_definitions: true, result: false).first
83
+ body = with_class_body do
84
+ emit_sequence(args[1..], env.child, :module, allow_method_definitions: true, result: false).first
85
+ end
84
86
  emit_module_wrapper(args[0], body)
85
87
  end
86
88
 
87
89
  def emit_class_expr(args, env)
88
90
  name_sym, supers, body_forms = split_class_args(args)
89
- body = emit_sequence(body_forms, env, :class, allow_method_definitions: true, result: false).first
91
+ body = with_class_body do
92
+ emit_sequence(body_forms, env.child, :class, allow_method_definitions: true, result: false).first
93
+ end
90
94
  emit_class_wrapper(name_sym, supers, env,
91
95
  body)
92
96
  end
@@ -298,7 +302,7 @@ module Kapusta
298
302
  end
299
303
 
300
304
  def emit_bound_call(binding, args, env, current_scope)
301
- return emit_self_method_binding_call(binding, args, env, current_scope) if method_binding?(binding)
305
+ return emit_self_method_binding_call(binding, args, env, current_scope) if callable_method_binding?(binding)
302
306
 
303
307
  emit_error!(:cannot_call_constant, name: binding) if constant_binding?(binding)
304
308
 
@@ -438,10 +442,15 @@ module Kapusta
438
442
  return emit_multisym_value(sym, env) if sym.dotted?
439
443
  return 'ARGV' if name == 'ARGV'
440
444
  return name if name.match?(/\A[A-Z]/)
445
+ return Kapusta.kebab_to_snake(name) if implicit_self_method?(name)
441
446
 
442
447
  emit_error!(:undefined_symbol, name:)
443
448
  end
444
449
 
450
+ def implicit_self_method?(name)
451
+ in_class_body? && direct_method_name?(Kapusta.kebab_to_snake(name))
452
+ end
453
+
445
454
  def emit_gvar(sym)
446
455
  "$#{global_name(sym.name)}"
447
456
  end
@@ -41,29 +41,29 @@ module Kapusta
41
41
 
42
42
  def emit_form_run(forms, start, env, current_scope, header_form: nil)
43
43
  i = start
44
- codes = []
44
+ entries = []
45
45
  while i < forms.length
46
46
  form = forms[i]
47
47
  if end_form?(form)
48
48
  validate_end_form!(form)
49
49
  with_current_form(form) { emit_error!(:end_outside_header) } unless header_form
50
- return [codes.join("\n"), i + 1]
50
+ return [join_definition_aware(entries, current_scope), i + 1]
51
51
  end
52
52
 
53
53
  if bodyless_header?(form)
54
54
  header_code, i = emit_bodyless_header(form, forms, i + 1, env)
55
- codes << header_code
55
+ entries << [form, header_code]
56
56
  next
57
57
  end
58
58
 
59
59
  code, env = emit_form_in_sequence(form, env, current_scope,
60
60
  allow_method_definitions: true,
61
61
  result_needed: false)
62
- codes << code
62
+ entries << [form, code]
63
63
  i += 1
64
64
  end
65
65
  with_current_form(header_form) { emit_error!(:unclosed_header) } if header_form
66
- [codes.join("\n"), i]
66
+ [join_definition_aware(entries, current_scope), i]
67
67
  end
68
68
 
69
69
  def end_form?(form)
@@ -109,12 +109,16 @@ module Kapusta
109
109
  inner_code, next_i = emit_bodyless_header(inner[0], forms, body_start, env)
110
110
  [emit_direct_module_header(name_sym, inner_code) || emit_module_wrapper(name_sym, inner_code), next_i]
111
111
  else
112
- body, next_i = emit_form_run(forms, body_start, env, :module, header_form: form)
112
+ body, next_i = with_class_body do
113
+ emit_form_run(forms, body_start, env.child, :module, header_form: form)
114
+ end
113
115
  [emit_direct_module_header(name_sym, body) || emit_module_wrapper(name_sym, body), next_i]
114
116
  end
115
117
  else
116
118
  name_sym, supers, = split_class_args(form.items[1..])
117
- body, next_i = emit_form_run(forms, body_start, env, :class, header_form: form)
119
+ body, next_i = with_class_body do
120
+ emit_form_run(forms, body_start, env.child, :class, header_form: form)
121
+ end
118
122
  code = emit_direct_class_header(name_sym, supers, body, env) ||
119
123
  emit_class_wrapper(name_sym, supers, env, body)
120
124
  [code, next_i]
@@ -170,14 +174,41 @@ module Kapusta
170
174
 
171
175
  def emit_sequence(forms, env, current_scope, allow_method_definitions:, result: true)
172
176
  current_env = env
173
- codes = []
177
+ entries = []
174
178
  forms.each_with_index do |form, index|
175
179
  code, current_env = emit_form_in_sequence(form, current_env, current_scope,
176
180
  allow_method_definitions:,
177
181
  result_needed: result && index == forms.length - 1)
178
- codes << code unless code.empty?
182
+ entries << [form, code] unless code.empty?
183
+ end
184
+ [join_definition_aware(entries, current_scope), current_env]
185
+ end
186
+
187
+ def join_definition_aware(entries, current_scope)
188
+ return '' if entries.empty?
189
+
190
+ result = +entries.first[1]
191
+ entries.each_cons(2) do |(prev_form, _), (curr_form, curr_code)|
192
+ sep = blank_between_definitions?(prev_form, curr_form, current_scope) ? "\n\n" : "\n"
193
+ result << sep << curr_code
194
+ end
195
+ result
196
+ end
197
+
198
+ def blank_between_definitions?(prev_form, curr_form, current_scope)
199
+ return false unless %i[toplevel module class].include?(current_scope)
200
+
201
+ definition_form?(prev_form) || definition_form?(curr_form)
202
+ end
203
+
204
+ def definition_form?(form)
205
+ return false unless form.is_a?(List) && !form.empty? && form.head.is_a?(Sym)
206
+
207
+ case form.head.name
208
+ when 'defn', 'class', 'module' then true
209
+ when 'fn', 'lambda', 'λ' then form.items[1].is_a?(Sym)
210
+ else false
179
211
  end
180
- [codes.join("\n"), current_env]
181
212
  end
182
213
 
183
214
  def sequence_statement_form?(form)
@@ -313,8 +344,30 @@ module Kapusta
313
344
  binding.is_a?(Env::MethodBinding)
314
345
  end
315
346
 
347
+ def in_class_body?
348
+ @class_body_depth.positive?
349
+ end
350
+
351
+ def with_class_body
352
+ @class_body_depth += 1
353
+ yield
354
+ ensure
355
+ @class_body_depth -= 1
356
+ end
357
+
358
+ def self_method_binding?(binding)
359
+ binding.is_a?(Env::SelfMethodBinding)
360
+ end
361
+
362
+ def callable_method_binding?(binding)
363
+ method_binding?(binding) || self_method_binding?(binding)
364
+ end
365
+
316
366
  def binding_value_code(binding)
317
- method_binding?(binding) ? "method(#{binding.ruby_name.to_sym.inspect})" : binding
367
+ return "method(#{binding.ruby_name.to_sym.inspect})" if method_binding?(binding)
368
+ return binding.ruby_name if self_method_binding?(binding)
369
+
370
+ binding
318
371
  end
319
372
 
320
373
  def parse_counted_for_bindings(bindings, env, current_scope)
@@ -28,6 +28,7 @@ module Kapusta
28
28
  @path = path
29
29
  @target = target
30
30
  @temp_index = 0
31
+ @class_body_depth = 0
31
32
  end
32
33
 
33
34
  def emit_file(forms)
data/lib/kapusta/env.rb CHANGED
@@ -3,6 +3,7 @@
3
3
  module Kapusta
4
4
  class Env
5
5
  MethodBinding = Struct.new(:ruby_name)
6
+ SelfMethodBinding = Struct.new(:ruby_name)
6
7
 
7
8
  def initialize(parent = nil)
8
9
  @parent = parent
@@ -17,7 +17,7 @@ module Kapusta
17
17
  return unless target
18
18
 
19
19
  case target.kind
20
- when :local, :toplevel_fn, :constant
20
+ when :local, :toplevel_fn, :constant, :method
21
21
  location_for_binding(uri, target.binding) if target.binding
22
22
  when :macro
23
23
  locations_for_macro(uri, target.binding, workspace_index)
@@ -32,6 +32,8 @@ module Kapusta
32
32
  case target.kind
33
33
  when :local
34
34
  rename_local(uri, target, new_name)
35
+ when :method
36
+ rename_method(uri, target, new_name)
35
37
  when :toplevel_fn, :free_toplevel
36
38
  return error('cross-file rename requires a workspace') unless workspace_index
37
39
 
@@ -128,8 +130,6 @@ module Kapusta
128
130
  end
129
131
 
130
132
  if binding
131
- return if binding.kind == :method
132
-
133
133
  return constant_target(walker, binding, seg) if %i[module class].include?(binding.kind)
134
134
 
135
135
  return local_target(walker, binding, seg)
@@ -138,7 +138,6 @@ module Kapusta
138
138
  if reference
139
139
  target = reference.target
140
140
  if target
141
- return if target.kind == :method
142
141
  return constant_target(walker, target, seg, sym:) if %i[module class].include?(target.kind)
143
142
 
144
143
  return local_target(walker, target, seg, sym:)
@@ -164,6 +163,7 @@ module Kapusta
164
163
  kind = case binding.kind
165
164
  when :toplevel_fn then :toplevel_fn
166
165
  when :macro, :macro_import then :macro
166
+ when :method then :method
167
167
  else :local
168
168
  end
169
169
  Target.new(
@@ -207,6 +207,22 @@ module Kapusta
207
207
  }
208
208
  end
209
209
 
210
+ def rename_method(uri, target, new_name)
211
+ return error("invalid identifier: #{new_name}") unless Identifier.valid_local?(new_name)
212
+ return error('cannot resolve binding') unless target.binding
213
+
214
+ walker = target.walker
215
+ binding = target.binding
216
+
217
+ occs = [binding]
218
+ walker.references.each do |r|
219
+ occs << r if r.target.equal?(binding)
220
+ end
221
+
222
+ edits = occs.map { |o| text_edit_first_segment(o, new_name) }
223
+ { changes: { uri => edits } }
224
+ end
225
+
210
226
  def rename_local(uri, target, new_name)
211
227
  return error("invalid identifier: #{new_name}") unless Identifier.valid_local?(new_name)
212
228
  return error('cannot resolve binding') unless target.binding
@@ -76,6 +76,17 @@ module Kapusta
76
76
 
77
77
  def walk_top(forms)
78
78
  walk_form_run(forms, 0, @root_scope)
79
+ resolve_late_references
80
+ end
81
+
82
+ def resolve_late_references
83
+ @references.each do |r|
84
+ next if r.target
85
+ next unless r.scope
86
+
87
+ target = r.scope.lookup(r.name)
88
+ r.target = target if target
89
+ end
79
90
  end
80
91
 
81
92
  def walk_form_run(forms, start, scope, header_target: nil)
@@ -164,7 +175,8 @@ module Kapusta
164
175
  if body.length == 1 && bodyless_header?(body[0])
165
176
  walk_bodyless_header(body[0], forms, body_start, scope)
166
177
  else
167
- walk_form_run(forms, body_start, scope, header_target: binding)
178
+ body_scope = make_scope(scope, :module)
179
+ walk_form_run(forms, body_start, body_scope, header_target: binding)
168
180
  end
169
181
  end
170
182
  when 'class'
@@ -172,7 +184,8 @@ module Kapusta
172
184
  supers&.items&.each { |item| walk_form(item, scope) }
173
185
  binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :class) : nil
174
186
  inside_class do
175
- walk_form_run(forms, body_start, scope, header_target: binding)
187
+ body_scope = make_scope(scope, :class)
188
+ walk_form_run(forms, body_start, body_scope, header_target: binding)
176
189
  end
177
190
  end
178
191
  end
@@ -406,7 +419,7 @@ module Kapusta
406
419
  else
407
420
  (scope == @root_scope ? :toplevel_fn : :fn_local)
408
421
  end
409
- binding = add_binding(name_sym, scope, kind, lexical: kind != :method)
422
+ binding = add_binding(name_sym, scope, kind, lexical: true)
410
423
  fn_scope.bindings[name_sym.name] = binding unless kind == :method
411
424
  end
412
425
  bind_param_vec(params, fn_scope)
@@ -558,10 +571,11 @@ module Kapusta
558
571
  add_constant_binding(name_sym, scope, kind) if name_sym.is_a?(Sym)
559
572
 
560
573
  body = list.items[body_start..] || []
574
+ body_scope = make_scope(scope, kind)
561
575
  if kind == :class
562
- inside_class { body.each { |form| walk_form(form, scope) } }
576
+ inside_class { body.each { |form| walk_form(form, body_scope) } }
563
577
  else
564
- inside_module_or_class { body.each { |form| walk_form(form, scope) } }
578
+ inside_module_or_class { body.each { |form| walk_form(form, body_scope) } }
565
579
  end
566
580
  end
567
581
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.12.0'
4
+ VERSION = '0.13.0'
5
5
  end
data/spec/lsp_spec.rb CHANGED
@@ -234,7 +234,7 @@ RSpec.describe Kapusta::LSP do
234
234
  end
235
235
  end
236
236
 
237
- it 'does not offer rename for method definitions after a bodyless class header' do
237
+ it 'offers rename for method definitions after a bodyless class header' do
238
238
  text = "(class Counter)\n(fn initialize [start] start)\n"
239
239
  responses = run(
240
240
  frame_initialize,
@@ -242,7 +242,60 @@ RSpec.describe Kapusta::LSP do
242
242
  frame_prepare_rename(uri: 'file:///x.kap', **cursor_at(text, 'initialize'))
243
243
  )
244
244
 
245
- expect(result_for(responses)['result']).to be_nil
245
+ expect(result_for(responses)['result']).to include('placeholder' => 'initialize')
246
+ end
247
+
248
+ it 'renames a class method and its bare-self call sites within a file' do
249
+ text = "(class Greeter)\n" \
250
+ "(fn initialize [name] (set @name name))\n" \
251
+ "(fn greeting [] (.. \"Hello, \" (label)))\n" \
252
+ "(fn label [] @name)\n"
253
+ responses = run(
254
+ frame_initialize,
255
+ frame_did_open('file:///x.kap', text),
256
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'label'), new_name: 'who')
257
+ )
258
+ edits = result_for(responses)['result']['documentChanges'].first['edits']
259
+
260
+ expect(edits.map { |e| e['newText'] }).to all(eq('who'))
261
+ expect(edits.map { |e| [e['range']['start']['line'], e['range']['start']['character']] })
262
+ .to contain_exactly([2, 31], [3, 4])
263
+ end
264
+
265
+ it 'jumps from a bare-self method call to the class method definition' do
266
+ text = "(class Greeter)\n" \
267
+ "(fn label [] 1)\n" \
268
+ "(fn greeting [] (label))\n"
269
+ responses = run(
270
+ frame_initialize,
271
+ frame_did_open('file:///x.kap', text),
272
+ frame_definition(uri: 'file:///x.kap', line: 2, character: 17)
273
+ )
274
+ result = result_for(responses)['result']
275
+
276
+ expect(result).to eq(
277
+ 'uri' => 'file:///x.kap',
278
+ 'range' => {
279
+ 'start' => { 'line' => 1, 'character' => 4 },
280
+ 'end' => { 'line' => 1, 'character' => 9 }
281
+ }
282
+ )
283
+ end
284
+
285
+ it 'renames a class method when the cursor is on a bare-self call site' do
286
+ text = "(class Greeter)\n" \
287
+ "(fn label [] 1)\n" \
288
+ "(fn greeting [] (label))\n"
289
+ responses = run(
290
+ frame_initialize,
291
+ frame_did_open('file:///x.kap', text),
292
+ frame_rename(uri: 'file:///x.kap', line: 2, character: 17, new_name: 'who')
293
+ )
294
+ edits = result_for(responses)['result']['documentChanges'].first['edits']
295
+
296
+ expect(edits.map { |e| e['newText'] }).to all(eq('who'))
297
+ expect(edits.map { |e| [e['range']['start']['line'], e['range']['start']['character']] })
298
+ .to contain_exactly([1, 4], [2, 17])
246
299
  end
247
300
 
248
301
  it 'does not let class methods shadow top-level function references' do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kapusta
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov