kapusta 0.8.0 → 0.10.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -2
  3. data/bin/check-all +17 -0
  4. data/bin/compile-examples +70 -0
  5. data/bin/fennel-parity +4 -37
  6. data/examples/account-lockout.kap +11 -0
  7. data/examples/circle.kap +16 -0
  8. data/examples/convert-temperature.kap +14 -0
  9. data/examples/count-effects.kap +13 -0
  10. data/examples/falling-drops.kap +12 -0
  11. data/examples/fennel-parity-examples.txt +40 -0
  12. data/examples/hit-counter.kap +17 -0
  13. data/examples/max-achievable.kap +8 -0
  14. data/examples/mruby-runtime-examples.txt +89 -0
  15. data/examples/number-of-1-bits.kap +13 -0
  16. data/examples/number-of-steps.kap +15 -0
  17. data/examples/parking-system.kap +18 -0
  18. data/examples/thread-styles.kap +41 -0
  19. data/examples/two-sum-hash.kap +11 -14
  20. data/examples/underscore-patterns.kap +1 -1
  21. data/lib/kapusta/ast.rb +1 -1
  22. data/lib/kapusta/cli.rb +11 -6
  23. data/lib/kapusta/compiler/emitter/bindings.rb +27 -2
  24. data/lib/kapusta/compiler/emitter/control_flow.rb +97 -14
  25. data/lib/kapusta/compiler/emitter/interop.rb +2 -0
  26. data/lib/kapusta/compiler/emitter/patterns.rb +125 -0
  27. data/lib/kapusta/compiler/emitter/support.rb +9 -2
  28. data/lib/kapusta/compiler/emitter.rb +2 -1
  29. data/lib/kapusta/compiler/normalizer.rb +22 -12
  30. data/lib/kapusta/compiler.rb +13 -4
  31. data/lib/kapusta/errors.rb +3 -0
  32. data/lib/kapusta/formatter.rb +9 -2
  33. data/lib/kapusta/lsp/scope_walker.rb +55 -5
  34. data/lib/kapusta/reader.rb +28 -0
  35. data/lib/kapusta/version.rb +1 -1
  36. data/lib/kapusta.rb +2 -2
  37. data/spec/cli_spec.rb +35 -0
  38. data/spec/examples_spec.rb +128 -0
  39. data/spec/lsp_spec.rb +86 -0
  40. data/spec/spec_helper.rb +9 -0
  41. metadata +14 -1
@@ -95,6 +95,8 @@ module Kapusta
95
95
  when '#' then read_hashfn
96
96
  when '`' then read_quasiquote
97
97
  when ',' then read_unquote
98
+ when '@' then peek_at(@pos + 1) == '@' ? read_sigil(:cvar, 2) : read_sigil(:ivar, 1)
99
+ when '$' then read_sigil(:gvar, 1)
98
100
  when *CLOSING_DELIMS then raise unexpected_closing_delim(peek)
99
101
  else
100
102
  read_atom
@@ -117,6 +119,32 @@ module Kapusta
117
119
  Quasiquote.new(read_form)
118
120
  end
119
121
 
122
+ def read_sigil(kind, prefix_length)
123
+ return read_atom unless sigil_id_start?(peek_at(@pos + prefix_length))
124
+
125
+ position = source_position
126
+ prefix_length.times { advance }
127
+ name_position = source_position
128
+ name_start = @pos
129
+ advance until delim?(peek) || peek == '.'
130
+ inner = Sym.new(@src[name_start...@pos])
131
+ inner.line = name_position[0]
132
+ inner.column = name_position[1]
133
+ list = List.new([Sym.new(kind.to_s), inner])
134
+ list.sigil = kind
135
+ list.line = position[0]
136
+ list.column = position[1]
137
+ list
138
+ end
139
+
140
+ def peek_at(pos)
141
+ @src[pos]
142
+ end
143
+
144
+ def sigil_id_start?(char)
145
+ !char.nil? && char.match?(/[A-Za-z_]/)
146
+ end
147
+
120
148
  def read_unquote
121
149
  advance
122
150
  if peek == '@'
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.8.0'
4
+ VERSION = '0.10.0'
5
5
  end
data/lib/kapusta.rb CHANGED
@@ -23,8 +23,8 @@ module Kapusta
23
23
  self.eval(source, path:)
24
24
  end
25
25
 
26
- def self.compile(source, path: '(eval)', **_opts)
27
- Compiler.compile(source, path:)
26
+ def self.compile(source, path: '(eval)', target: nil, **_opts)
27
+ Compiler.compile(source, path:, target:)
28
28
  end
29
29
 
30
30
  def self.require(feature, relative_to: nil)
data/spec/cli_spec.rb CHANGED
@@ -66,6 +66,19 @@ RSpec.describe Kapusta::CLI do
66
66
  end
67
67
  end
68
68
 
69
+ it 'compiles case and match forms for mruby with --target=mruby' do
70
+ path = File.expand_path('../examples/match.kap', __dir__)
71
+
72
+ ruby = capture_stdout do
73
+ described_class.start(['--compile', '--target=mruby', path])
74
+ end
75
+
76
+ expect(ruby).not_to match(/^\s*in\b/)
77
+ expect(ruby).not_to include('^(')
78
+ expect(ruby).to include("case\n")
79
+ expect(ruby).to include('when ')
80
+ end
81
+
69
82
  it 'rejects extra positional arguments in compile mode' do
70
83
  path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
71
84
 
@@ -77,6 +90,28 @@ RSpec.describe Kapusta::CLI do
77
90
  expect(error_output).to include('usage: kapusta')
78
91
  end
79
92
 
93
+ it 'rejects unsupported targets' do
94
+ path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
95
+
96
+ error_output = capture_stderr do
97
+ expect { described_class.start(['--compile', '--target=mri', path]) }
98
+ .to raise_error(SystemExit)
99
+ end
100
+
101
+ expect(error_output).to include('unknown target "mri"; only mruby is supported')
102
+ end
103
+
104
+ it 'rejects target without compile mode' do
105
+ path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
106
+
107
+ error_output = capture_stderr do
108
+ expect { described_class.start(['--target=mruby', path]) }
109
+ .to raise_error(SystemExit)
110
+ end
111
+
112
+ expect(error_output).to include('--target requires --compile')
113
+ end
114
+
80
115
  it 'passes remaining arguments through to the Kapusta program' do
81
116
  path = File.expand_path('../examples/greet.kap', __dir__)
82
117
 
@@ -2,10 +2,21 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'fileutils'
5
+ require 'open3'
5
6
  require 'stringio'
7
+ require 'tempfile'
6
8
 
7
9
  EXAMPLES_DIR = File.expand_path('../examples', __dir__)
8
10
 
11
+ def example_list(name)
12
+ File.readlines(File.join(EXAMPLES_DIR, name), chomp: true)
13
+ .reject(&:empty?)
14
+ .map { |example| "#{example}.kap" }
15
+ .freeze
16
+ end
17
+
18
+ MRUBY_RUNTIME_EXAMPLES = example_list('mruby-runtime-examples.txt')
19
+
9
20
  def run_example(name, argv: [])
10
21
  previous_argv = ARGV.dup
11
22
  previous_stdout = $stdout
@@ -27,6 +38,39 @@ ensure
27
38
  $stdout = previous_stdout
28
39
  end
29
40
 
41
+ def compile_example(name, target: nil)
42
+ path = File.join(EXAMPLES_DIR, name)
43
+ Kapusta.compile(File.read(path), path:, target:)
44
+ end
45
+
46
+ def run_compiled_source(source, path:)
47
+ previous_argv = ARGV.dup
48
+ previous_stdout = $stdout
49
+ ARGV.replace([])
50
+ $stdout = StringIO.new
51
+ TOPLEVEL_BINDING.eval(source, path, 1)
52
+ $stdout.string
53
+ ensure
54
+ $stdout = previous_stdout
55
+ ARGV.replace(previous_argv)
56
+ end
57
+
58
+ def run_mruby_source(source, path:)
59
+ stdout, stderr, status = capture_mruby_source(source, path:)
60
+
61
+ raise "mruby failed for #{path}:\n#{stderr}" unless status.success?
62
+
63
+ stdout
64
+ end
65
+
66
+ def capture_mruby_source(source, path:)
67
+ Tempfile.create([File.basename(path, '.kap'), '.rb']) do |file|
68
+ file.write(source)
69
+ file.close
70
+ Open3.capture3('mruby', file.path)
71
+ end
72
+ end
73
+
30
74
  RSpec.describe 'examples' do
31
75
  it 'ackermann.kap' do
32
76
  expect(run_example('ackermann.kap')).to eq("9\n61\n")
@@ -36,6 +80,18 @@ RSpec.describe 'examples' do
36
80
  expect(run_example('accumulator.kap')).to eq("22\n")
37
81
  end
38
82
 
83
+ it 'account-lockout.kap' do
84
+ expect(run_example('account-lockout.kap')).to eq(<<~OUT)
85
+ :ok
86
+ :locked
87
+ :locked
88
+ OUT
89
+ end
90
+
91
+ it 'circle.kap' do
92
+ expect(run_example('circle.kap')).to eq("78.53975\n31.4159\n")
93
+ end
94
+
39
95
  it 'anagram.kap' do
40
96
  expect(run_example('anagram.kap')).to eq("true\ntrue\nfalse\n")
41
97
  end
@@ -107,6 +163,26 @@ RSpec.describe 'examples' do
107
163
  expect(run_example('happy-number.kap')).to eq("true\nfalse\ntrue\n")
108
164
  end
109
165
 
166
+ it 'number-of-1-bits.kap' do
167
+ expect(run_example('number-of-1-bits.kap')).to eq("3\n1\n31\n")
168
+ end
169
+
170
+ it 'number-of-steps.kap' do
171
+ expect(run_example('number-of-steps.kap')).to eq("6\n4\n12\n")
172
+ end
173
+
174
+ it 'convert-temperature.kap' do
175
+ expect(run_example('convert-temperature.kap')).to eq("309.65\n97.7\n395.26\n251.798\n")
176
+ end
177
+
178
+ it 'count-effects.kap' do
179
+ expect(run_example('count-effects.kap')).to eq("1\n2\n")
180
+ end
181
+
182
+ it 'max-achievable.kap' do
183
+ expect(run_example('max-achievable.kap')).to eq("6\n7\n10\n")
184
+ end
185
+
110
186
  it 'move-zeroes.kap' do
111
187
  expect(run_example('move-zeroes.kap')).to eq(<<~OUT)
112
188
  [1, 3, 12, 0, 0]
@@ -368,6 +444,18 @@ RSpec.describe 'examples' do
368
444
  OUT
369
445
  end
370
446
 
447
+ it 'underscore-patterns.kap on mruby keeps loose nil and strict fallback separate' do
448
+ path = File.join(EXAMPLES_DIR, 'underscore-patterns.kap')
449
+ ruby = compile_example('underscore-patterns.kap', target: :mruby)
450
+
451
+ expect(run_mruby_source(ruby, path:)).to eq(<<~OUT)
452
+ 5
453
+ nil
454
+ 5
455
+ "fallback"
456
+ OUT
457
+ end
458
+
371
459
  it 'scopes.kap' do
372
460
  expect(run_example('scopes.kap')).to eq("5\n9\n9\n9\n")
373
461
  end
@@ -563,4 +651,44 @@ RSpec.describe 'examples' do
563
651
  it 'macros-import-whole.kap' do
564
652
  expect(run_example('macros-import-whole.kap')).to eq("7\n")
565
653
  end
654
+
655
+ it 'parking-system.kap' do
656
+ expect(run_example('parking-system.kap')).to eq("true\ntrue\nfalse\nfalse\n")
657
+ end
658
+
659
+ it 'hit-counter.kap' do
660
+ expect(run_example('hit-counter.kap')).to eq(<<~OUT)
661
+ 1
662
+ 2
663
+ 3
664
+ "alice"
665
+ OUT
666
+ end
667
+ end
668
+
669
+ RSpec.describe 'mruby runtime examples' do
670
+ MRUBY_RUNTIME_EXAMPLES.each do |name|
671
+ it name do
672
+ path = File.join(EXAMPLES_DIR, name)
673
+ ruby = compile_example(name)
674
+ expected = run_example(name)
675
+ expect(run_compiled_source(ruby, path:)).to eq(expected)
676
+ mruby_stdout, _mruby_stderr, mruby_status = capture_mruby_source(ruby, path:)
677
+
678
+ if mruby_status.success? && mruby_stdout == expected
679
+ expect(run_mruby_source(ruby, path:)).to eq(expected)
680
+ else
681
+ mruby_ruby = compile_example(name, target: :mruby)
682
+
683
+ if mruby_ruby == ruby
684
+ expect(mruby_status).to be_success
685
+ else
686
+ expect(mruby_ruby).not_to match(/^\s*in\b/)
687
+ expect(mruby_ruby).not_to include('^(')
688
+ expect(run_compiled_source(mruby_ruby, path:)).to eq(expected)
689
+ run_mruby_source(mruby_ruby, path:)
690
+ end
691
+ end
692
+ end
693
+ end
566
694
  end
data/spec/lsp_spec.rb CHANGED
@@ -587,6 +587,92 @@ RSpec.describe Kapusta::LSP do
587
587
  end
588
588
  end
589
589
 
590
+ it 'renames an @ivar across methods within a class' do
591
+ text = "(class C)\n(fn one [] (set @counter 1))\n(fn two [] @counter)\n"
592
+ responses = run(
593
+ frame_initialize,
594
+ frame_did_open('file:///x.kap', text),
595
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'counter'), new_name: 'total')
596
+ )
597
+ changes = result_for(responses)['result']['documentChanges']
598
+
599
+ expect(changes.length).to eq(1)
600
+ edits = changes.first['edits']
601
+ expect(edits.map { |e| e['newText'] }).to eq(%w[total total])
602
+ expect(edits.map { |e| [e['range']['start']['line'], e['range']['start']['character']] })
603
+ .to contain_exactly([1, 17], [2, 12])
604
+ end
605
+
606
+ it 'renames @ivar without touching a same-named fn parameter or value reference' do
607
+ text = "(class C)\n(fn init [val] (set @counter val))\n"
608
+ responses = run(
609
+ frame_initialize,
610
+ frame_did_open('file:///x.kap', text),
611
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'counter'), new_name: 'total')
612
+ )
613
+ changes = result_for(responses)['result']['documentChanges']
614
+
615
+ expect(changes.length).to eq(1)
616
+ edits = changes.first['edits']
617
+ expect(edits.length).to eq(1)
618
+ expect(edits.first['newText']).to eq('total')
619
+ expect(edits.first['range']['start']).to eq('line' => 1, 'character' => 21)
620
+ end
621
+
622
+ it 'renames a $gvar within a file' do
623
+ text = "(set $last 1)\n(print $last)\n"
624
+ responses = run(
625
+ frame_initialize,
626
+ frame_did_open('file:///x.kap', text),
627
+ frame_rename(uri: 'file:///x.kap', **cursor_at(text, 'last'), new_name: 'latest')
628
+ )
629
+ changes = result_for(responses)['result']['documentChanges']
630
+
631
+ expect(changes.length).to eq(1)
632
+ edits = changes.first['edits']
633
+ expect(edits.map { |e| e['newText'] }).to eq(%w[latest latest])
634
+ end
635
+
636
+ it 'keeps @x and @@x in separate sigil namespaces' do
637
+ text = "(class C)\n(set @@flag 1)\n(fn show [] (set @flag 2))\n"
638
+ ivar_position = { line: 2, character: 18 }
639
+ responses = run(
640
+ frame_initialize,
641
+ frame_did_open('file:///x.kap', text),
642
+ frame_rename(uri: 'file:///x.kap', **ivar_position, new_name: 'mark')
643
+ )
644
+ changes = result_for(responses)['result']['documentChanges']
645
+
646
+ expect(changes.length).to eq(1)
647
+ edits = changes.first['edits']
648
+ expect(edits.length).to eq(1)
649
+ expect(edits.first['newText']).to eq('mark')
650
+ expect(edits.first['range']['start']).to eq('line' => 2, 'character' => 18)
651
+ end
652
+
653
+ it 'jumps to the first @@cvar binding from a later use site' do
654
+ text = "(class C)\n(set @@total 0)\n(fn add [] (set @@total (+ @@total 1)))\n"
655
+ use_index = text.rindex('@@total') + 2
656
+ prefix = text[0...use_index]
657
+ last_nl = prefix.rindex("\n")
658
+ pos = { line: prefix.count("\n"), character: last_nl ? use_index - last_nl - 1 : use_index }
659
+
660
+ responses = run(
661
+ frame_initialize,
662
+ frame_did_open('file:///x.kap', text),
663
+ frame_definition(uri: 'file:///x.kap', **pos)
664
+ )
665
+ result = result_for(responses)['result']
666
+
667
+ expect(result).to eq(
668
+ 'uri' => 'file:///x.kap',
669
+ 'range' => {
670
+ 'start' => { 'line' => 1, 'character' => 7 },
671
+ 'end' => { 'line' => 1, 'character' => 12 }
672
+ }
673
+ )
674
+ end
675
+
590
676
  it 'escapes file URIs built during workspace scans' do
591
677
  Dir.mktmpdir do |dir|
592
678
  nested = File.join(dir, 'space dir')
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,15 @@
3
3
  require 'bundler/setup'
4
4
  require 'kapusta'
5
5
 
6
+ module SilenceConstantRedefinitionWarnings
7
+ def warn(message, category: nil)
8
+ return if /already initialized constant|previous definition of/.match?(message)
9
+
10
+ super
11
+ end
12
+ end
13
+ Warning.singleton_class.prepend(SilenceConstantRedefinitionWarnings)
14
+
6
15
  RSpec.configure do |config|
7
16
  config.disable_monkey_patching!
8
17
  config.example_status_persistence_file_path = '.rspec_status'
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.8.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -26,6 +26,7 @@ files:
26
26
  - bin/console
27
27
  - bin/fennel-parity
28
28
  - bin/setup
29
+ - examples/account-lockout.kap
29
30
  - examples/accumulator.kap
30
31
  - examples/ackermann.kap
31
32
  - examples/anagram.kap
@@ -38,9 +39,12 @@ files:
38
39
  - examples/block-sort.kap
39
40
  - examples/blocks-and-kwargs.kap
40
41
  - examples/calc.kap
42
+ - examples/circle.kap
41
43
  - examples/classify-wallet.kap
42
44
  - examples/climbing-stairs.kap
43
45
  - examples/contains-duplicate.kap
46
+ - examples/convert-temperature.kap
47
+ - examples/count-effects.kap
44
48
  - examples/counter.kap
45
49
  - examples/describe.kap
46
50
  - examples/destructure.kap
@@ -50,6 +54,8 @@ files:
50
54
  - examples/even-squares.kap
51
55
  - examples/exceptions.kap
52
56
  - examples/factorial.kap
57
+ - examples/falling-drops.kap
58
+ - examples/fennel-parity-examples.txt
53
59
  - examples/fib.kap
54
60
  - examples/files.kap
55
61
  - examples/fizzbuzz.kap
@@ -57,6 +63,7 @@ files:
57
63
  - examples/greet.kap
58
64
  - examples/happy-number.kap
59
65
  - examples/hashfn.kap
66
+ - examples/hit-counter.kap
60
67
  - examples/import-helpers.kapm
61
68
  - examples/kwargs.kap
62
69
  - examples/leap-year.kap
@@ -73,14 +80,19 @@ files:
73
80
  - examples/majority-element.kap
74
81
  - examples/manhattan-distance.kap
75
82
  - examples/match.kap
83
+ - examples/max-achievable.kap
76
84
  - examples/maximum-subarray.kap
77
85
  - examples/min-max.kap
78
86
  - examples/module-header.kap
79
87
  - examples/move-zeroes.kap
88
+ - examples/mruby-runtime-examples.txt
89
+ - examples/number-of-1-bits.kap
90
+ - examples/number-of-steps.kap
80
91
  - examples/or-patterns.kap
81
92
  - examples/packet-router.kap
82
93
  - examples/palindrome.kap
83
94
  - examples/pangram.kap
95
+ - examples/parking-system.kap
84
96
  - examples/pcall.kap
85
97
  - examples/pipeline.kap
86
98
  - examples/pivot-index.kap
@@ -103,6 +115,7 @@ files:
103
115
  - examples/stack.kap
104
116
  - examples/subtract-product-sum.kap
105
117
  - examples/sum.kap
118
+ - examples/thread-styles.kap
106
119
  - examples/threading.kap
107
120
  - examples/tic-tac-toe.kap
108
121
  - examples/tset.kap