kapusta 0.9.0 → 0.11.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 +10 -2
- data/bin/check-all +17 -0
- data/bin/compile-examples +70 -0
- data/bin/fennel-parity +4 -38
- data/examples/account-lockout.kap +11 -0
- data/examples/accumulator.kap +2 -0
- data/examples/bank-account.kap +2 -0
- data/examples/bst-iterator.kap +52 -0
- data/examples/circle.kap +18 -0
- data/examples/convert-temperature.kap +14 -0
- data/examples/count-effects.kap +13 -0
- data/examples/counter.kap +2 -0
- data/examples/falling-drops.kap +12 -0
- data/examples/fennel-parity-examples.txt +40 -0
- data/examples/hit-counter.kap +2 -0
- data/examples/max-achievable.kap +8 -0
- data/examples/module-header.kap +4 -2
- data/examples/mruby-runtime-examples.txt +92 -0
- data/examples/number-of-1-bits.kap +13 -0
- data/examples/number-of-steps.kap +15 -0
- data/examples/parking-system.kap +2 -0
- data/examples/recent-counter.kap +17 -0
- data/examples/scopes.kap +2 -0
- data/examples/signal-harvest.kap +16 -0
- data/examples/stack.kap +2 -0
- data/examples/two-sum-hash.kap +11 -14
- data/examples/underscore-patterns.kap +1 -1
- data/examples/valid-parentheses-1.kap +2 -0
- data/lib/kapusta/cli.rb +11 -6
- data/lib/kapusta/compiler/emitter/bindings.rb +27 -2
- data/lib/kapusta/compiler/emitter/control_flow.rb +97 -14
- data/lib/kapusta/compiler/emitter/expressions.rb +1 -0
- data/lib/kapusta/compiler/emitter/interop.rb +4 -2
- data/lib/kapusta/compiler/emitter/patterns.rb +125 -0
- data/lib/kapusta/compiler/emitter/support.rb +63 -24
- data/lib/kapusta/compiler/emitter.rb +2 -1
- data/lib/kapusta/compiler/normalizer.rb +6 -0
- data/lib/kapusta/compiler.rb +15 -6
- data/lib/kapusta/errors.rb +6 -0
- data/lib/kapusta/formatter.rb +1 -2
- data/lib/kapusta/lsp/definition.rb +17 -0
- data/lib/kapusta/lsp/rename.rb +3 -1
- data/lib/kapusta/lsp/scope_walker.rb +40 -11
- data/lib/kapusta/lsp.rb +2 -1
- data/lib/kapusta/version.rb +1 -1
- data/lib/kapusta.rb +2 -2
- data/spec/cli_spec.rb +35 -0
- data/spec/examples_errors_spec.rb +20 -0
- data/spec/examples_spec.rb +136 -0
- data/spec/lsp_spec.rb +71 -3
- data/spec/spec_helper.rb +9 -0
- metadata +14 -1
data/lib/kapusta/errors.rb
CHANGED
|
@@ -10,6 +10,7 @@ module Kapusta
|
|
|
10
10
|
bad_set_target: 'bad set target: %{target}',
|
|
11
11
|
bad_shorthand: 'bad shorthand',
|
|
12
12
|
bind_table_dots: 'unable to bind table ...',
|
|
13
|
+
cannot_call_constant: 'cannot call constant %{name}; reference it without parentheses',
|
|
13
14
|
cannot_call_literal: 'cannot call literal value %{value}',
|
|
14
15
|
cannot_emit_form: 'cannot emit form: %{form}',
|
|
15
16
|
cannot_set_method_binding: 'cannot set method binding: %{name}',
|
|
@@ -24,6 +25,9 @@ module Kapusta
|
|
|
24
25
|
dot_no_args: 'expected table argument',
|
|
25
26
|
each_no_binding: 'expected binding table',
|
|
26
27
|
empty_call: 'expected a function, macro, or special to call',
|
|
28
|
+
end_outside_header: 'end outside class or module',
|
|
29
|
+
end_with_args: 'end takes no arguments',
|
|
30
|
+
unclosed_header: 'class or module not closed with (end)',
|
|
27
31
|
expected_var: 'expected var %{name}',
|
|
28
32
|
fn_no_params: 'expected parameters table',
|
|
29
33
|
global_arity: 'expected name and value',
|
|
@@ -51,6 +55,7 @@ module Kapusta
|
|
|
51
55
|
odd_forms_in_hash: 'odd number of forms in hash',
|
|
52
56
|
rest_not_last: 'expected rest argument before last parameter',
|
|
53
57
|
shadowed_special: 'local %{name} was overshadowed by a special form or macro',
|
|
58
|
+
target_requires_compile: '--target requires --compile',
|
|
54
59
|
tset_no_value: 'tset: expected table, key, and value arguments',
|
|
55
60
|
unclosed_delimiter: "unclosed opening delimiter '%{char}'",
|
|
56
61
|
undefined_symbol: 'undefined symbol: %{name}',
|
|
@@ -58,6 +63,7 @@ module Kapusta
|
|
|
58
63
|
unexpected_eof: 'unexpected eof',
|
|
59
64
|
unexpected_vararg: 'unexpected vararg',
|
|
60
65
|
unknown_special_form: 'unknown special form: %{name}',
|
|
66
|
+
unknown_target: 'unknown target %{target}; only mruby is supported',
|
|
61
67
|
unquote_outside_quasiquote: 'unquote outside quasiquote',
|
|
62
68
|
unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
|
|
63
69
|
unterminated_string: 'unterminated string',
|
data/lib/kapusta/formatter.rb
CHANGED
|
@@ -266,7 +266,7 @@ module Kapusta
|
|
|
266
266
|
raw_args = list_raw_rest(list)
|
|
267
267
|
|
|
268
268
|
case head_name
|
|
269
|
-
when 'fn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
|
|
269
|
+
when 'fn', 'defn', 'lambda', 'λ', 'macro' then render_fn(head_name, list, indent, top_level:)
|
|
270
270
|
when 'let' then render_let(list, indent)
|
|
271
271
|
when 'do', 'finally' then render_prefix_body_form(head_name, [], raw_args, indent)
|
|
272
272
|
when 'try' then render_try(list, indent)
|
|
@@ -657,7 +657,6 @@ module Kapusta
|
|
|
657
657
|
|
|
658
658
|
def render_let_bindings(bindings, indent)
|
|
659
659
|
return render(bindings, indent + '(let '.length, force_expand: true) if contains_comments?(bindings.items)
|
|
660
|
-
return render(bindings, indent + '(let '.length, layout: :pairwise) if bindings.items.length <= 2
|
|
661
660
|
|
|
662
661
|
hanging = render_hanging_pairwise_vec(bindings)
|
|
663
662
|
hanging || render(bindings, indent + '(let '.length, layout: :pairwise)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'rename'
|
|
4
|
+
require_relative '../reader'
|
|
5
|
+
require_relative 'scope_walker'
|
|
4
6
|
|
|
5
7
|
module Kapusta
|
|
6
8
|
class LSP
|
|
@@ -8,6 +10,9 @@ module Kapusta
|
|
|
8
10
|
module_function
|
|
9
11
|
|
|
10
12
|
def find(uri, text, line_zero, character, workspace_index:)
|
|
13
|
+
marker = end_marker_at(text, line_zero, character)
|
|
14
|
+
return location_for_binding(uri, marker.target) if marker&.target
|
|
15
|
+
|
|
11
16
|
target = Rename.locate(text, line_zero, character)
|
|
12
17
|
return unless target
|
|
13
18
|
|
|
@@ -23,6 +28,18 @@ module Kapusta
|
|
|
23
28
|
end
|
|
24
29
|
end
|
|
25
30
|
|
|
31
|
+
def end_marker_at(text, line_zero, character)
|
|
32
|
+
forms = Reader.read_all(text)
|
|
33
|
+
walker = ScopeWalker.analyze(forms)
|
|
34
|
+
line = line_zero + 1
|
|
35
|
+
col = character + 1
|
|
36
|
+
walker.end_markers.find do |m|
|
|
37
|
+
m.line == line && col >= m.column && col <= m.end_column
|
|
38
|
+
end
|
|
39
|
+
rescue Kapusta::Error
|
|
40
|
+
nil
|
|
41
|
+
end
|
|
42
|
+
|
|
26
43
|
def locations_for_macro(uri, binding, workspace_index)
|
|
27
44
|
return unless binding
|
|
28
45
|
|
data/lib/kapusta/lsp/rename.rb
CHANGED
|
@@ -344,7 +344,9 @@ module Kapusta
|
|
|
344
344
|
end
|
|
345
345
|
|
|
346
346
|
def rename_constant(target, new_name, workspace_index)
|
|
347
|
-
|
|
347
|
+
unless Identifier.valid_constant_segment?(new_name)
|
|
348
|
+
return error("class and module names must start with an uppercase letter (got #{new_name.inspect})")
|
|
349
|
+
end
|
|
348
350
|
|
|
349
351
|
prefix = target.segment_prefix
|
|
350
352
|
seg_index = target.segment_index
|
|
@@ -15,6 +15,7 @@ module Kapusta
|
|
|
15
15
|
bindings[name] || parent&.lookup(name)
|
|
16
16
|
end
|
|
17
17
|
end
|
|
18
|
+
EndMarker = Struct.new(:line, :column, :end_column, :target, keyword_init: true)
|
|
18
19
|
|
|
19
20
|
SKIPPED_HEADS = %w[macros quasi-sym quasi-list
|
|
20
21
|
quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym].freeze
|
|
@@ -26,6 +27,7 @@ module Kapusta
|
|
|
26
27
|
'global' => :walk_global,
|
|
27
28
|
'set' => :walk_set,
|
|
28
29
|
'fn' => :walk_fn,
|
|
30
|
+
'defn' => :walk_fn,
|
|
29
31
|
'lambda' => :walk_fn,
|
|
30
32
|
'λ' => :walk_fn,
|
|
31
33
|
'for' => :walk_for,
|
|
@@ -48,7 +50,7 @@ module Kapusta
|
|
|
48
50
|
'gvar' => :walk_sigil_form
|
|
49
51
|
}.freeze
|
|
50
52
|
|
|
51
|
-
attr_reader :bindings, :references, :root_scope
|
|
53
|
+
attr_reader :bindings, :references, :root_scope, :end_markers
|
|
52
54
|
|
|
53
55
|
def self.analyze(forms)
|
|
54
56
|
walker = new
|
|
@@ -59,6 +61,7 @@ module Kapusta
|
|
|
59
61
|
def initialize
|
|
60
62
|
@bindings = []
|
|
61
63
|
@references = []
|
|
64
|
+
@end_markers = []
|
|
62
65
|
@scope_seq = 0
|
|
63
66
|
@root_scope = make_scope(nil, :file)
|
|
64
67
|
@gvar_scope = make_scope(nil, :gvars)
|
|
@@ -67,17 +70,43 @@ module Kapusta
|
|
|
67
70
|
end
|
|
68
71
|
|
|
69
72
|
def walk_top(forms)
|
|
70
|
-
|
|
73
|
+
walk_form_run(forms, 0, @root_scope)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def walk_form_run(forms, start, scope, header_target: nil)
|
|
77
|
+
i = start
|
|
71
78
|
while i < forms.length
|
|
72
79
|
form = forms[i]
|
|
80
|
+
if end_form?(form)
|
|
81
|
+
record_end_marker(form, header_target) if header_target
|
|
82
|
+
return i + 1
|
|
83
|
+
end
|
|
84
|
+
|
|
73
85
|
if bodyless_header?(form)
|
|
74
|
-
walk_bodyless_header(form, forms
|
|
75
|
-
|
|
86
|
+
i = walk_bodyless_header(form, forms, i + 1, scope)
|
|
87
|
+
next
|
|
76
88
|
end
|
|
77
89
|
|
|
78
|
-
walk_form(form,
|
|
90
|
+
walk_form(form, scope)
|
|
79
91
|
i += 1
|
|
80
92
|
end
|
|
93
|
+
i
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def record_end_marker(form, target)
|
|
97
|
+
head = form.head
|
|
98
|
+
return unless head.is_a?(Sym) && head.respond_to?(:line) && head.line
|
|
99
|
+
|
|
100
|
+
@end_markers << EndMarker.new(
|
|
101
|
+
line: head.line,
|
|
102
|
+
column: head.column,
|
|
103
|
+
end_column: head.column + head.name.length,
|
|
104
|
+
target:
|
|
105
|
+
)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def end_form?(form)
|
|
109
|
+
form.is_a?(List) && !form.empty? && form.head.is_a?(Sym) && form.head.name == 'end'
|
|
81
110
|
end
|
|
82
111
|
|
|
83
112
|
def binding_at(line, column)
|
|
@@ -120,25 +149,25 @@ module Kapusta
|
|
|
120
149
|
end
|
|
121
150
|
end
|
|
122
151
|
|
|
123
|
-
def walk_bodyless_header(form,
|
|
152
|
+
def walk_bodyless_header(form, forms, body_start, scope)
|
|
124
153
|
case form.head.name
|
|
125
154
|
when 'module'
|
|
126
155
|
name_sym = form.items[1]
|
|
127
|
-
add_constant_binding(name_sym, scope, :module)
|
|
156
|
+
binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :module) : nil
|
|
128
157
|
body = form.items[2..] || []
|
|
129
158
|
inside_module_or_class do
|
|
130
159
|
if body.length == 1 && bodyless_header?(body[0])
|
|
131
|
-
walk_bodyless_header(body[0],
|
|
160
|
+
walk_bodyless_header(body[0], forms, body_start, scope)
|
|
132
161
|
else
|
|
133
|
-
|
|
162
|
+
walk_form_run(forms, body_start, scope, header_target: binding)
|
|
134
163
|
end
|
|
135
164
|
end
|
|
136
165
|
when 'class'
|
|
137
166
|
name_sym, supers, = split_class_args(form.items[1..] || [])
|
|
138
167
|
supers&.items&.each { |item| walk_form(item, scope) }
|
|
139
|
-
add_constant_binding(name_sym, scope, :class)
|
|
168
|
+
binding = name_sym.is_a?(Sym) ? add_constant_binding(name_sym, scope, :class) : nil
|
|
140
169
|
inside_class do
|
|
141
|
-
|
|
170
|
+
walk_form_run(forms, body_start, scope, header_target: binding)
|
|
142
171
|
end
|
|
143
172
|
end
|
|
144
173
|
end
|
data/lib/kapusta/lsp.rb
CHANGED
|
@@ -257,7 +257,8 @@ module Kapusta
|
|
|
257
257
|
new_name, workspace_index: @workspace_index)
|
|
258
258
|
if result[:error]
|
|
259
259
|
debug("rename error: #{result[:error].inspect}")
|
|
260
|
-
|
|
260
|
+
notify('window/showMessage', { type: 1, message: "Rename: #{result[:error][:message]}" })
|
|
261
|
+
reply(id, { documentChanges: [] })
|
|
261
262
|
else
|
|
262
263
|
edit = build_workspace_edit(result[:changes])
|
|
263
264
|
debug("rename ok: files=#{result[:changes].keys.length} edits=#{result[:changes].values.sum(&:length)}")
|
data/lib/kapusta/version.rb
CHANGED
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
|
|
|
@@ -92,6 +92,26 @@ 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 'end-outside-header.kap' do
|
|
96
|
+
expect(run_error_example('end-outside-header.kap'))
|
|
97
|
+
.to eq("end-outside-header.kap:1:1: end outside class or module\n")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
it 'end-with-args.kap' do
|
|
101
|
+
expect(run_error_example('end-with-args.kap'))
|
|
102
|
+
.to eq("end-with-args.kap:5:1: end takes no arguments\n")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it 'extra-end.kap' do
|
|
106
|
+
expect(run_error_example('extra-end.kap'))
|
|
107
|
+
.to eq("extra-end.kap:7:1: end outside class or module\n")
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
it 'unclosed-header.kap' do
|
|
111
|
+
expect(run_error_example('unclosed-header.kap'))
|
|
112
|
+
.to eq("unclosed-header.kap:1:1: class or module not closed with (end)\n")
|
|
113
|
+
end
|
|
114
|
+
|
|
95
115
|
it 'destructure-rest-as-table.kap' do
|
|
96
116
|
expect(run_error_example('destructure-rest-as-table.kap'))
|
|
97
117
|
.to eq("destructure-rest-as-table.kap:6:3: unable to bind table ...\n")
|
data/spec/examples_spec.rb
CHANGED
|
@@ -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
|
|
@@ -399,6 +487,10 @@ RSpec.describe 'examples' do
|
|
|
399
487
|
OUT
|
|
400
488
|
end
|
|
401
489
|
|
|
490
|
+
it 'signal-harvest.kap' do
|
|
491
|
+
expect(run_example('signal-harvest.kap')).to eq("40\ntrue\nfalse\n")
|
|
492
|
+
end
|
|
493
|
+
|
|
402
494
|
it 'shapes.kap' do
|
|
403
495
|
expect(run_example('shapes.kap')).to eq("78.5\n9\n8\n0\n")
|
|
404
496
|
end
|
|
@@ -437,6 +529,19 @@ RSpec.describe 'examples' do
|
|
|
437
529
|
expect(run_example('two-sum-hash.kap')).to eq("[0, 1]\n[1, 2]\nnil\n")
|
|
438
530
|
end
|
|
439
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
|
+
|
|
440
545
|
it 'baseball-game.kap' do
|
|
441
546
|
expect(run_example('baseball-game.kap')).to eq("30\n27\n")
|
|
442
547
|
end
|
|
@@ -468,6 +573,10 @@ RSpec.describe 'examples' do
|
|
|
468
573
|
OUT
|
|
469
574
|
end
|
|
470
575
|
|
|
576
|
+
it 'recent-counter.kap' do
|
|
577
|
+
expect(run_example('recent-counter.kap')).to eq("4\n5\n")
|
|
578
|
+
end
|
|
579
|
+
|
|
471
580
|
it 'reverse-integer.kap' do
|
|
472
581
|
expect(run_example('reverse-integer.kap')).to eq("321\n-321\n21\n0\n")
|
|
473
582
|
end
|
|
@@ -577,3 +686,30 @@ RSpec.describe 'examples' do
|
|
|
577
686
|
OUT
|
|
578
687
|
end
|
|
579
688
|
end
|
|
689
|
+
|
|
690
|
+
RSpec.describe 'mruby runtime examples' do
|
|
691
|
+
MRUBY_RUNTIME_EXAMPLES.each do |name|
|
|
692
|
+
it name do
|
|
693
|
+
path = File.join(EXAMPLES_DIR, name)
|
|
694
|
+
ruby = compile_example(name)
|
|
695
|
+
expected = run_example(name)
|
|
696
|
+
expect(run_compiled_source(ruby, path:)).to eq(expected)
|
|
697
|
+
mruby_stdout, _mruby_stderr, mruby_status = capture_mruby_source(ruby, path:)
|
|
698
|
+
|
|
699
|
+
if mruby_status.success? && mruby_stdout == expected
|
|
700
|
+
expect(run_mruby_source(ruby, path:)).to eq(expected)
|
|
701
|
+
else
|
|
702
|
+
mruby_ruby = compile_example(name, target: :mruby)
|
|
703
|
+
|
|
704
|
+
if mruby_ruby == ruby
|
|
705
|
+
expect(mruby_status).to be_success
|
|
706
|
+
else
|
|
707
|
+
expect(mruby_ruby).not_to match(/^\s*in\b/)
|
|
708
|
+
expect(mruby_ruby).not_to include('^(')
|
|
709
|
+
expect(run_compiled_source(mruby_ruby, path:)).to eq(expected)
|
|
710
|
+
run_mruby_source(mruby_ruby, path:)
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
end
|
|
714
|
+
end
|
|
715
|
+
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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'
|