kapusta 0.10.0 → 0.11.1

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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -0
  3. data/examples/accumulator.kap +2 -0
  4. data/examples/bank-account.kap +2 -0
  5. data/examples/bst-iterator.kap +52 -0
  6. data/examples/circle.kap +2 -0
  7. data/examples/counter.kap +2 -0
  8. data/examples/hit-counter.kap +2 -0
  9. data/examples/module-header.kap +4 -2
  10. data/examples/mruby-runtime-examples.txt +3 -0
  11. data/examples/parking-system.kap +2 -0
  12. data/examples/recent-counter.kap +17 -0
  13. data/examples/scopes.kap +2 -0
  14. data/examples/signal-harvest.kap +16 -0
  15. data/examples/stack.kap +2 -0
  16. data/examples/valid-parentheses-1.kap +2 -0
  17. data/lib/kapusta/compiler/emitter/bindings.rb +1 -4
  18. data/lib/kapusta/compiler/emitter/control_flow.rb +25 -30
  19. data/lib/kapusta/compiler/emitter/expressions.rb +2 -0
  20. data/lib/kapusta/compiler/emitter/interop.rb +23 -15
  21. data/lib/kapusta/compiler/emitter/patterns.rb +29 -52
  22. data/lib/kapusta/compiler/emitter/support.rb +106 -44
  23. data/lib/kapusta/compiler/macro_expander.rb +4 -12
  24. data/lib/kapusta/compiler/macro_lowerer.rb +4 -12
  25. data/lib/kapusta/compiler/normalizer.rb +9 -17
  26. data/lib/kapusta/compiler.rb +2 -2
  27. data/lib/kapusta/errors.rb +4 -0
  28. data/lib/kapusta/formatter.rb +1 -1
  29. data/lib/kapusta/lsp/definition.rb +17 -0
  30. data/lib/kapusta/lsp/rename.rb +3 -1
  31. data/lib/kapusta/lsp/scope_walker.rb +79 -46
  32. data/lib/kapusta/lsp/workspace_index.rb +2 -13
  33. data/lib/kapusta/lsp.rb +17 -16
  34. data/lib/kapusta/support.rb +8 -0
  35. data/lib/kapusta/version.rb +1 -1
  36. data/spec/examples_errors_spec.rb +25 -0
  37. data/spec/examples_spec.rb +21 -0
  38. data/spec/lsp_spec.rb +71 -3
  39. metadata +4 -1
@@ -35,7 +35,7 @@ module Kapusta
35
35
  end
36
36
 
37
37
  def remove(uri)
38
- path = uri_to_path(uri)
38
+ path = LSP.uri_to_path(uri)
39
39
  if path && File.file?(path)
40
40
  store(uri, File.read(path))
41
41
  else
@@ -178,7 +178,7 @@ module Kapusta
178
178
  end
179
179
 
180
180
  def resolve_module_uris(importing_uri, module_label)
181
- importing_path = uri_to_path(importing_uri)
181
+ importing_path = LSP.uri_to_path(importing_uri)
182
182
  return [] unless importing_path
183
183
 
184
184
  base_dir = File.dirname(importing_path)
@@ -209,17 +209,6 @@ module Kapusta
209
209
  def path_to_uri(path)
210
210
  "file://#{URI::DEFAULT_PARSER.escape(File.expand_path(path))}"
211
211
  end
212
-
213
- def uri_to_path(uri)
214
- return unless uri.is_a?(String)
215
-
216
- parsed = URI.parse(uri)
217
- return URI::DEFAULT_PARSER.unescape(parsed.path) if parsed.scheme == 'file'
218
-
219
- uri
220
- rescue URI::InvalidURIError
221
- nil
222
- end
223
212
  end
224
213
  end
225
214
  end
data/lib/kapusta/lsp.rb CHANGED
@@ -19,6 +19,17 @@ module Kapusta
19
19
  new(input:, output:, log:).run
20
20
  end
21
21
 
22
+ def self.uri_to_path(uri)
23
+ return unless uri.is_a?(String)
24
+
25
+ parsed = URI.parse(uri)
26
+ return URI::DEFAULT_PARSER.unescape(parsed.path) if parsed.scheme == 'file'
27
+
28
+ uri
29
+ rescue URI::InvalidURIError
30
+ nil
31
+ end
32
+
22
33
  def initialize(input:, output:, log:)
23
34
  @input = input.binmode
24
35
  @output = output.binmode
@@ -150,8 +161,8 @@ module Kapusta
150
161
 
151
162
  def on_initialize(params)
152
163
  folders = params['workspaceFolders'] || []
153
- roots = folders.filter_map { |f| uri_to_path(f['uri']) }
154
- roots << uri_to_path(params['rootUri']) if params['rootUri']
164
+ roots = folders.filter_map { |f| LSP.uri_to_path(f['uri']) }
165
+ roots << LSP.uri_to_path(params['rootUri']) if params['rootUri']
155
166
  roots.compact!
156
167
  roots.uniq!
157
168
  debug("initialize: roots=#{roots.inspect}")
@@ -213,7 +224,7 @@ module Kapusta
213
224
  entry = @sources[uri]
214
225
  return [] unless entry
215
226
 
216
- Formatting.text_edits(entry[:text], uri_to_path(uri))
227
+ Formatting.text_edits(entry[:text], LSP.uri_to_path(uri))
217
228
  end
218
229
 
219
230
  def definition(params)
@@ -257,7 +268,8 @@ module Kapusta
257
268
  new_name, workspace_index: @workspace_index)
258
269
  if result[:error]
259
270
  debug("rename error: #{result[:error].inspect}")
260
- reply_error(id, result[:error][:code], result[:error][:message])
271
+ notify('window/showMessage', { type: 1, message: "Rename: #{result[:error][:message]}" })
272
+ reply(id, { documentChanges: [] })
261
273
  else
262
274
  edit = build_workspace_edit(result[:changes])
263
275
  debug("rename ok: files=#{result[:changes].keys.length} edits=#{result[:changes].values.sum(&:length)}")
@@ -282,23 +294,12 @@ module Kapusta
282
294
  end
283
295
 
284
296
  def publish_diagnostics(uri, text, version)
285
- diagnostics = Diagnostics.collect(text, uri_to_path(uri))
297
+ diagnostics = Diagnostics.collect(text, LSP.uri_to_path(uri))
286
298
  params = { uri:, diagnostics: }
287
299
  params[:version] = version unless version.nil?
288
300
  notify('textDocument/publishDiagnostics', params)
289
301
  end
290
302
 
291
- def uri_to_path(uri)
292
- return unless uri
293
-
294
- parsed = URI.parse(uri)
295
- return URI::DEFAULT_PARSER.unescape(parsed.path) if parsed.scheme == 'file'
296
-
297
- uri
298
- rescue URI::InvalidURIError
299
- uri
300
- end
301
-
302
303
  def log(message)
303
304
  @log.puts "kapusta-ls: #{message}"
304
305
  end
@@ -4,4 +4,12 @@ module Kapusta
4
4
  def self.kebab_to_snake(name)
5
5
  name.tr('-', '_')
6
6
  end
7
+
8
+ def self.copy_position(target, source)
9
+ return target unless target.respond_to?(:line=) && source.respond_to?(:line)
10
+
11
+ target.line ||= source.line
12
+ target.column ||= source.column
13
+ target
14
+ end
7
15
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.10.0'
4
+ VERSION = '0.11.1'
5
5
  end
@@ -92,6 +92,31 @@ RSpec.describe 'examples-errors' do
92
92
  .to eq("destructure-literal-table.kap:4:1: could not destructure literal\n")
93
93
  end
94
94
 
95
+ it 'defn-outside-header.kap' do
96
+ expect(run_error_example('defn-outside-header.kap'))
97
+ .to eq("defn-outside-header.kap:1:1: defn outside class or module\n")
98
+ end
99
+
100
+ it 'end-outside-header.kap' do
101
+ expect(run_error_example('end-outside-header.kap'))
102
+ .to eq("end-outside-header.kap:1:1: end outside class or module\n")
103
+ end
104
+
105
+ it 'end-with-args.kap' do
106
+ expect(run_error_example('end-with-args.kap'))
107
+ .to eq("end-with-args.kap:5:1: end takes no arguments\n")
108
+ end
109
+
110
+ it 'extra-end.kap' do
111
+ expect(run_error_example('extra-end.kap'))
112
+ .to eq("extra-end.kap:7:1: end outside class or module\n")
113
+ end
114
+
115
+ it 'unclosed-header.kap' do
116
+ expect(run_error_example('unclosed-header.kap'))
117
+ .to eq("unclosed-header.kap:1:1: class or module not closed with (end)\n")
118
+ end
119
+
95
120
  it 'destructure-rest-as-table.kap' do
96
121
  expect(run_error_example('destructure-rest-as-table.kap'))
97
122
  .to eq("destructure-rest-as-table.kap:6:3: unable to bind table ...\n")
@@ -487,6 +487,10 @@ RSpec.describe 'examples' do
487
487
  OUT
488
488
  end
489
489
 
490
+ it 'signal-harvest.kap' do
491
+ expect(run_example('signal-harvest.kap')).to eq("40\ntrue\nfalse\n")
492
+ end
493
+
490
494
  it 'shapes.kap' do
491
495
  expect(run_example('shapes.kap')).to eq("78.5\n9\n8\n0\n")
492
496
  end
@@ -525,6 +529,19 @@ RSpec.describe 'examples' do
525
529
  expect(run_example('two-sum-hash.kap')).to eq("[0, 1]\n[1, 2]\nnil\n")
526
530
  end
527
531
 
532
+ it 'bst-iterator.kap' do
533
+ expect(run_example('bst-iterator.kap')).to eq(<<~OUT)
534
+ 3
535
+ 7
536
+ true
537
+ 9
538
+ true
539
+ 15
540
+ 20
541
+ false
542
+ OUT
543
+ end
544
+
528
545
  it 'baseball-game.kap' do
529
546
  expect(run_example('baseball-game.kap')).to eq("30\n27\n")
530
547
  end
@@ -556,6 +573,10 @@ RSpec.describe 'examples' do
556
573
  OUT
557
574
  end
558
575
 
576
+ it 'recent-counter.kap' do
577
+ expect(run_example('recent-counter.kap')).to eq("4\n5\n")
578
+ end
579
+
559
580
  it 'reverse-integer.kap' do
560
581
  expect(run_example('reverse-integer.kap')).to eq("321\n-321\n21\n0\n")
561
582
  end
data/spec/lsp_spec.rb CHANGED
@@ -268,7 +268,8 @@ RSpec.describe Kapusta::LSP do
268
268
  frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'foo'), new_name: 'bar')
269
269
  )
270
270
 
271
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
271
+ message = responses.find { |m| m['method'] == 'window/showMessage' }
272
+ expect(message['params']['message']).to include('already defined')
272
273
  end
273
274
  end
274
275
 
@@ -282,7 +283,8 @@ RSpec.describe Kapusta::LSP do
282
283
  frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'Foo'), new_name: 'Bar')
283
284
  )
284
285
 
285
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
286
+ message = responses.find { |m| m['method'] == 'window/showMessage' }
287
+ expect(message['params']['message']).to include('already defined')
286
288
  end
287
289
  end
288
290
 
@@ -304,6 +306,71 @@ RSpec.describe Kapusta::LSP do
304
306
  )
305
307
  end
306
308
 
309
+ it 'rejects renaming a class to a lowercase name with a clear message' do
310
+ text = "(class Accumulator)\n\n(end)\n"
311
+ responses = run(
312
+ frame_initialize,
313
+ frame_did_open('file:///x.kap', text),
314
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'Accumulator'), new_name: 'fff')
315
+ )
316
+
317
+ response = result_for(responses)
318
+ expect(response['error']).to be_nil
319
+ expect(response['result']).to eq('documentChanges' => [])
320
+
321
+ show_message = responses.find { |m| m['method'] == 'window/showMessage' }
322
+ expect(show_message).not_to be_nil
323
+ expect(show_message['params']['type']).to eq(1)
324
+ expect(show_message['params']['message']).to include('uppercase letter')
325
+ end
326
+
327
+ it 'renames a class declared with a bodyless header closed by (end) and its usages after (end)' do
328
+ text = "(class Accumulator)\n\n(fn add! [n] n)\n\n(end)\n\n(let [acc (Accumulator.new 10)]\n (acc.add! 5))\n"
329
+ with_workspace('a.kap' => text) do |root_uri, uri|
330
+ responses = run(
331
+ frame_initialize([root_uri]),
332
+ frame_did_open(uri['a.kap'], text),
333
+ frame_rename(uri: uri['a.kap'], **cursor_at(text, 'Accumulator'), new_name: 'Foo')
334
+ )
335
+ result = result_for(responses)['result']
336
+
337
+ expect(result).not_to be_nil
338
+ edits = result['documentChanges'].first['edits']
339
+ expect(edits.map { |e| e['range']['start']['line'] }).to contain_exactly(0, 6)
340
+ expect(edits.map { |e| e['newText'] }).to all(eq('Foo'))
341
+ end
342
+ end
343
+
344
+ it 'jumps from (end) to the class header that opened the file scope' do
345
+ text = "(class Foo)\n\n(fn hi [] 1)\n\n(end)\n"
346
+ responses = run(
347
+ frame_initialize,
348
+ frame_did_open('file:///x.kap', text),
349
+ frame_definition(uri: 'file:///x.kap', **cursor_at(text, 'end'))
350
+ )
351
+ result = result_for(responses)['result']
352
+
353
+ expect(result).to eq(
354
+ 'uri' => 'file:///x.kap',
355
+ 'range' => {
356
+ 'start' => { 'line' => 0, 'character' => 7 },
357
+ 'end' => { 'line' => 0, 'character' => 10 }
358
+ }
359
+ )
360
+ end
361
+
362
+ it 'jumps from (end) to the matching module header for nested headers' do
363
+ text = "(module Outer)\n\n(module Inner)\n(fn self.go [] 1)\n(end)\n\n(end)\n"
364
+ responses = run(
365
+ frame_initialize,
366
+ frame_did_open('file:///x.kap', text),
367
+ frame_definition(uri: 'file:///x.kap', line: 4, character: 1)
368
+ )
369
+ result = result_for(responses)['result']
370
+
371
+ expect(result['range']['start']).to eq('line' => 2, 'character' => 8)
372
+ end
373
+
307
374
  it 'jumps to a top-level fn definition across files' do
308
375
  text_a = "(fn greet [n] (print n))\n"
309
376
  text_b = "(greet 42)\n"
@@ -583,7 +650,8 @@ RSpec.describe Kapusta::LSP do
583
650
  frame_rename(uri: uri['a.kap'], **cursor_at(text_a, 'swap!'), new_name: 'flip!')
584
651
  )
585
652
 
586
- expect(result_for(responses).dig('error', 'message')).to include('already defined')
653
+ message = responses.find { |m| m['method'] == 'window/showMessage' }
654
+ expect(message['params']['message']).to include('already defined')
587
655
  end
588
656
  end
589
657
 
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.10.0
4
+ version: 0.11.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -38,6 +38,7 @@ files:
38
38
  - examples/binary-to-decimal.kap
39
39
  - examples/block-sort.kap
40
40
  - examples/blocks-and-kwargs.kap
41
+ - examples/bst-iterator.kap
41
42
  - examples/calc.kap
42
43
  - examples/circle.kap
43
44
  - examples/classify-wallet.kap
@@ -101,6 +102,7 @@ files:
101
102
  - examples/power-of-three.kap
102
103
  - examples/primes.kap
103
104
  - examples/raindrops.kap
105
+ - examples/recent-counter.kap
104
106
  - examples/record.kap
105
107
  - examples/regex.kap
106
108
  - examples/reverse-integer.kap
@@ -110,6 +112,7 @@ files:
110
112
  - examples/scopes.kap
111
113
  - examples/shapes.kap
112
114
  - examples/shared-macros.kapm
115
+ - examples/signal-harvest.kap
113
116
  - examples/single-number.kap
114
117
  - examples/squares.kap
115
118
  - examples/stack.kap