kapusta 0.1.3 → 0.1.5
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 +1 -0
- data/bin/compile-examples +24 -0
- data/examples/contains-duplicate.kap +7 -0
- data/lib/kapusta/compiler/emitter/bindings.rb +192 -41
- data/lib/kapusta/compiler/emitter/collections.rb +47 -49
- data/lib/kapusta/compiler/emitter/control_flow.rb +59 -18
- data/lib/kapusta/compiler/emitter/expressions.rb +24 -8
- data/lib/kapusta/compiler/emitter/interop.rb +123 -24
- data/lib/kapusta/compiler/emitter/patterns.rb +5 -8
- data/lib/kapusta/compiler/emitter/support.rb +108 -32
- data/lib/kapusta/compiler/normalizer.rb +1 -1
- data/lib/kapusta/compiler/runtime.rb +72 -71
- data/lib/kapusta/compiler.rb +2 -1
- data/lib/kapusta/env.rb +16 -0
- data/lib/kapusta/error.rb +5 -0
- data/lib/kapusta/formatter.rb +10 -20
- data/lib/kapusta/reader.rb +16 -12
- data/lib/kapusta/version.rb +1 -1
- data/lib/kapusta.rb +1 -0
- data/spec/cli_spec.rb +12 -21
- data/spec/examples_spec.rb +5 -1
- metadata +4 -1
|
@@ -4,6 +4,7 @@ module Kapusta
|
|
|
4
4
|
module Compiler
|
|
5
5
|
module Runtime
|
|
6
6
|
HELPER_DEPENDENCIES = {
|
|
7
|
+
stringify: %i[repr],
|
|
7
8
|
print_values: %i[stringify],
|
|
8
9
|
concat: %i[stringify],
|
|
9
10
|
method_path_value: %i[kebab_to_snake],
|
|
@@ -20,12 +21,12 @@ module Kapusta
|
|
|
20
21
|
|
|
21
22
|
HELPER_SOURCES = {
|
|
22
23
|
kebab_to_snake: <<~RUBY.chomp,
|
|
23
|
-
def
|
|
24
|
+
def kap_kebab_to_snake(name)
|
|
24
25
|
name.tr('-', '_')
|
|
25
26
|
end
|
|
26
27
|
RUBY
|
|
27
28
|
call: <<~'RUBY'.chomp,
|
|
28
|
-
def
|
|
29
|
+
def kap_call(callee, positional, kwargs = nil, block = nil)
|
|
29
30
|
raise "not callable: #{callee.inspect}" unless callee.respond_to?(:call)
|
|
30
31
|
|
|
31
32
|
if block
|
|
@@ -36,7 +37,7 @@ module Kapusta
|
|
|
36
37
|
end
|
|
37
38
|
RUBY
|
|
38
39
|
send_call: <<~RUBY.chomp,
|
|
39
|
-
def
|
|
40
|
+
def kap_send_call(receiver, method_name, positional, kwargs = nil, block = nil)
|
|
40
41
|
if block
|
|
41
42
|
if kwargs
|
|
42
43
|
receiver.public_send(method_name, *positional, **kwargs, &block)
|
|
@@ -51,7 +52,7 @@ module Kapusta
|
|
|
51
52
|
end
|
|
52
53
|
RUBY
|
|
53
54
|
invoke_self: <<~RUBY.chomp,
|
|
54
|
-
def
|
|
55
|
+
def kap_invoke_self(receiver, method_name, positional, kwargs = nil, block = nil)
|
|
55
56
|
if block
|
|
56
57
|
if kwargs
|
|
57
58
|
receiver.send(method_name, *positional, **kwargs, &block)
|
|
@@ -63,53 +64,50 @@ module Kapusta
|
|
|
63
64
|
end
|
|
64
65
|
end
|
|
65
66
|
RUBY
|
|
66
|
-
stringify: <<~
|
|
67
|
-
def
|
|
68
|
-
render = nil
|
|
69
|
-
render = lambda do |item|
|
|
70
|
-
case item
|
|
71
|
-
when nil then 'nil'
|
|
72
|
-
when true then 'true'
|
|
73
|
-
when false then 'false'
|
|
74
|
-
when String, Symbol then item.inspect
|
|
75
|
-
when Array
|
|
76
|
-
"[#{item.map { |child| render.call(child) }.join(', ')}]"
|
|
77
|
-
when Hash
|
|
78
|
-
"{#{item.map { |key, child| "#{render.call(key)}=>#{render.call(child)}" }.join(', ')}}"
|
|
79
|
-
else
|
|
80
|
-
item.inspect
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
67
|
+
stringify: <<~RUBY.chomp,
|
|
68
|
+
def kap_stringify(value)
|
|
84
69
|
case value
|
|
85
70
|
when nil then 'nil'
|
|
86
|
-
when
|
|
87
|
-
when false then 'false'
|
|
88
|
-
when Array, Hash then render.call(value)
|
|
71
|
+
when Array, Hash then kap_repr(value)
|
|
89
72
|
else value.to_s
|
|
90
73
|
end
|
|
91
74
|
end
|
|
92
75
|
RUBY
|
|
76
|
+
repr: <<~'RUBY'.chomp,
|
|
77
|
+
def kap_repr(value)
|
|
78
|
+
case value
|
|
79
|
+
when nil then 'nil'
|
|
80
|
+
when true, false then value.to_s
|
|
81
|
+
when String, Symbol then value.inspect
|
|
82
|
+
when Array
|
|
83
|
+
"[#{value.map { |item| kap_repr(item) }.join(', ')}]"
|
|
84
|
+
when Hash
|
|
85
|
+
"{#{value.map { |key, item| "#{kap_repr(key)}=>#{kap_repr(item)}" }.join(', ')}}"
|
|
86
|
+
else
|
|
87
|
+
value.inspect
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
RUBY
|
|
93
91
|
print_values: <<~'RUBY'.chomp,
|
|
94
|
-
def
|
|
95
|
-
$stdout.puts(values.map { |value|
|
|
92
|
+
def kap_print_values(*values)
|
|
93
|
+
$stdout.puts(values.map { |value| kap_stringify(value) }.join("\t"))
|
|
96
94
|
nil
|
|
97
95
|
end
|
|
98
96
|
RUBY
|
|
99
97
|
concat: <<~RUBY.chomp,
|
|
100
|
-
def
|
|
101
|
-
values.map { |value|
|
|
98
|
+
def kap_concat(values)
|
|
99
|
+
values.map { |value| kap_stringify(value) }.join
|
|
102
100
|
end
|
|
103
101
|
RUBY
|
|
104
102
|
get_path: <<~RUBY.chomp,
|
|
105
|
-
def
|
|
103
|
+
def kap_get_path(obj, keys)
|
|
106
104
|
keys.reduce(obj) { |acc, key| acc[key] }
|
|
107
105
|
end
|
|
108
106
|
RUBY
|
|
109
107
|
qget_path: <<~RUBY.chomp,
|
|
110
|
-
def
|
|
108
|
+
def kap_qget_path(obj, keys)
|
|
111
109
|
keys.each do |key|
|
|
112
|
-
return
|
|
110
|
+
return if obj.nil?
|
|
113
111
|
|
|
114
112
|
obj = obj[key]
|
|
115
113
|
end
|
|
@@ -117,64 +115,64 @@ module Kapusta
|
|
|
117
115
|
end
|
|
118
116
|
RUBY
|
|
119
117
|
set_path: <<~RUBY.chomp,
|
|
120
|
-
def
|
|
118
|
+
def kap_set_path(obj, keys, value)
|
|
121
119
|
target = obj
|
|
122
120
|
keys[0...-1].each { |key| target = target[key] }
|
|
123
121
|
target[keys.last] = value
|
|
124
122
|
end
|
|
125
123
|
RUBY
|
|
126
124
|
method_path_value: <<~RUBY.chomp,
|
|
127
|
-
def
|
|
128
|
-
segments.reduce(base) { |obj, segment| obj.public_send(
|
|
125
|
+
def kap_method_path_value(base, segments)
|
|
126
|
+
segments.reduce(base) { |obj, segment| obj.public_send(kap_kebab_to_snake(segment).to_sym) }
|
|
129
127
|
end
|
|
130
128
|
RUBY
|
|
131
129
|
set_method_path: <<~'RUBY'.chomp,
|
|
132
|
-
def
|
|
130
|
+
def kap_set_method_path(base, segments, value)
|
|
133
131
|
target = base
|
|
134
132
|
segments[0...-1].each do |segment|
|
|
135
|
-
target = target.public_send(
|
|
133
|
+
target = target.public_send(kap_kebab_to_snake(segment).to_sym)
|
|
136
134
|
end
|
|
137
|
-
setter = "#{
|
|
135
|
+
setter = "#{kap_kebab_to_snake(segments.last)}="
|
|
138
136
|
target.public_send(setter.to_sym, value)
|
|
139
137
|
end
|
|
140
138
|
RUBY
|
|
141
139
|
current_class_scope: <<~RUBY.chomp,
|
|
142
|
-
def
|
|
140
|
+
def kap_current_class_scope(receiver)
|
|
143
141
|
receiver.is_a?(Module) ? receiver : receiver.class
|
|
144
142
|
end
|
|
145
143
|
RUBY
|
|
146
144
|
get_ivar: <<~'RUBY'.chomp,
|
|
147
|
-
def
|
|
148
|
-
receiver.instance_variable_get("@#{
|
|
145
|
+
def kap_get_ivar(receiver, name)
|
|
146
|
+
receiver.instance_variable_get("@#{kap_kebab_to_snake(name)}")
|
|
149
147
|
end
|
|
150
148
|
RUBY
|
|
151
149
|
set_ivar: <<~'RUBY'.chomp,
|
|
152
|
-
def
|
|
153
|
-
receiver.instance_variable_set("@#{
|
|
150
|
+
def kap_set_ivar(receiver, name, value)
|
|
151
|
+
receiver.instance_variable_set("@#{kap_kebab_to_snake(name)}", value)
|
|
154
152
|
end
|
|
155
153
|
RUBY
|
|
156
154
|
get_cvar: <<~'RUBY'.chomp,
|
|
157
|
-
def
|
|
158
|
-
|
|
155
|
+
def kap_get_cvar(receiver, name)
|
|
156
|
+
kap_current_class_scope(receiver).class_variable_get("@@#{kap_kebab_to_snake(name)}")
|
|
159
157
|
end
|
|
160
158
|
RUBY
|
|
161
159
|
set_cvar: <<~'RUBY'.chomp,
|
|
162
|
-
def
|
|
163
|
-
|
|
160
|
+
def kap_set_cvar(receiver, name, value)
|
|
161
|
+
kap_current_class_scope(receiver).class_variable_set("@@#{kap_kebab_to_snake(name)}", value)
|
|
164
162
|
end
|
|
165
163
|
RUBY
|
|
166
164
|
get_gvar: <<~'RUBY'.chomp,
|
|
167
|
-
def
|
|
168
|
-
Kernel.eval("$#{
|
|
165
|
+
def kap_get_gvar(name)
|
|
166
|
+
Kernel.eval("$#{kap_kebab_to_snake(name)}", binding, __FILE__, __LINE__)
|
|
169
167
|
end
|
|
170
168
|
RUBY
|
|
171
169
|
set_gvar: <<~'RUBY'.chomp,
|
|
172
|
-
def
|
|
173
|
-
Kernel.eval("$#{
|
|
170
|
+
def kap_set_gvar(name, value)
|
|
171
|
+
Kernel.eval("$#{kap_kebab_to_snake(name)} = value", binding, __FILE__, __LINE__)
|
|
174
172
|
end
|
|
175
173
|
RUBY
|
|
176
174
|
ensure_module: <<~RUBY.chomp,
|
|
177
|
-
def
|
|
175
|
+
def kap_ensure_module(holder, path)
|
|
178
176
|
segments = path.split('.')
|
|
179
177
|
last = segments.pop
|
|
180
178
|
scope = holder.is_a?(Module) ? holder : Object
|
|
@@ -198,7 +196,7 @@ module Kapusta
|
|
|
198
196
|
end
|
|
199
197
|
RUBY
|
|
200
198
|
ensure_class: <<~RUBY.chomp,
|
|
201
|
-
def
|
|
199
|
+
def kap_ensure_class(holder, path, super_class)
|
|
202
200
|
segments = path.split('.')
|
|
203
201
|
last = segments.pop
|
|
204
202
|
scope = holder.is_a?(Module) ? holder : Object
|
|
@@ -222,14 +220,14 @@ module Kapusta
|
|
|
222
220
|
end
|
|
223
221
|
RUBY
|
|
224
222
|
destructure: <<~RUBY.chomp,
|
|
225
|
-
def
|
|
223
|
+
def kap_destructure(pattern, value)
|
|
226
224
|
bindings = {}
|
|
227
|
-
|
|
225
|
+
kap_destructure_into(pattern, value, bindings)
|
|
228
226
|
bindings
|
|
229
227
|
end
|
|
230
228
|
RUBY
|
|
231
229
|
destructure_into: <<~'RUBY'.chomp,
|
|
232
|
-
def
|
|
230
|
+
def kap_destructure_into(pattern, value, bindings)
|
|
233
231
|
case pattern[0]
|
|
234
232
|
when :sym
|
|
235
233
|
name = pattern[1]
|
|
@@ -241,18 +239,18 @@ module Kapusta
|
|
|
241
239
|
before = items[0...rest_idx]
|
|
242
240
|
rest_pattern = items[rest_idx][1]
|
|
243
241
|
before.each_with_index do |item, i|
|
|
244
|
-
|
|
242
|
+
kap_destructure_into(item, value ? value[i] : nil, bindings)
|
|
245
243
|
end
|
|
246
244
|
rest_value = value ? (value[rest_idx..] || []) : []
|
|
247
|
-
|
|
245
|
+
kap_destructure_into(rest_pattern, rest_value, bindings)
|
|
248
246
|
else
|
|
249
247
|
items.each_with_index do |item, i|
|
|
250
|
-
|
|
248
|
+
kap_destructure_into(item, value ? value[i] : nil, bindings)
|
|
251
249
|
end
|
|
252
250
|
end
|
|
253
251
|
when :hash
|
|
254
252
|
pattern[1].each do |key, subpattern|
|
|
255
|
-
|
|
253
|
+
kap_destructure_into(subpattern, value ? value[key] : nil, bindings)
|
|
256
254
|
end
|
|
257
255
|
when :ignore
|
|
258
256
|
nil
|
|
@@ -262,13 +260,13 @@ module Kapusta
|
|
|
262
260
|
end
|
|
263
261
|
RUBY
|
|
264
262
|
match_pattern: <<~RUBY.chomp,
|
|
265
|
-
def
|
|
263
|
+
def kap_match_pattern(pattern, value)
|
|
266
264
|
bindings = {}
|
|
267
|
-
[
|
|
265
|
+
[kap_match_pattern_into(pattern, value, bindings), bindings]
|
|
268
266
|
end
|
|
269
267
|
RUBY
|
|
270
268
|
match_pattern_into: <<~'RUBY'.chomp
|
|
271
|
-
def
|
|
269
|
+
def kap_match_pattern_into(pattern, value, bindings)
|
|
272
270
|
case pattern[0]
|
|
273
271
|
when :bind
|
|
274
272
|
name = pattern[1]
|
|
@@ -293,14 +291,14 @@ module Kapusta
|
|
|
293
291
|
return false if array.length < before.length
|
|
294
292
|
|
|
295
293
|
before.each_with_index do |item, i|
|
|
296
|
-
return false unless
|
|
294
|
+
return false unless kap_match_pattern_into(item, array[i], bindings)
|
|
297
295
|
end
|
|
298
|
-
|
|
296
|
+
kap_match_pattern_into(rest_pattern, array[rest_idx..], bindings)
|
|
299
297
|
else
|
|
300
298
|
return false unless array.length >= items.length
|
|
301
299
|
|
|
302
300
|
items.each_with_index do |item, i|
|
|
303
|
-
return false unless
|
|
301
|
+
return false unless kap_match_pattern_into(item, array[i], bindings)
|
|
304
302
|
end
|
|
305
303
|
true
|
|
306
304
|
end
|
|
@@ -309,7 +307,7 @@ module Kapusta
|
|
|
309
307
|
|
|
310
308
|
pattern[1].each do |key, subpattern|
|
|
311
309
|
return false unless value.key?(key)
|
|
312
|
-
return false unless
|
|
310
|
+
return false unless kap_match_pattern_into(subpattern, value[key], bindings)
|
|
313
311
|
end
|
|
314
312
|
true
|
|
315
313
|
when :lit
|
|
@@ -319,7 +317,7 @@ module Kapusta
|
|
|
319
317
|
when :or
|
|
320
318
|
pattern[1].any? do |option|
|
|
321
319
|
option_bindings = bindings.dup
|
|
322
|
-
next false unless
|
|
320
|
+
next false unless kap_match_pattern_into(option, value, option_bindings)
|
|
323
321
|
|
|
324
322
|
bindings.replace(option_bindings)
|
|
325
323
|
true
|
|
@@ -334,7 +332,7 @@ module Kapusta
|
|
|
334
332
|
module_function
|
|
335
333
|
|
|
336
334
|
def helper_name(name)
|
|
337
|
-
"
|
|
335
|
+
"kap_#{name}"
|
|
338
336
|
end
|
|
339
337
|
|
|
340
338
|
def helper_source(helpers)
|
|
@@ -366,11 +364,14 @@ module Kapusta
|
|
|
366
364
|
helper_methods = []
|
|
367
365
|
|
|
368
366
|
HELPER_SOURCES.each_key do |name|
|
|
369
|
-
helper_method = :"
|
|
370
|
-
|
|
367
|
+
helper_method = :"kap_#{name}"
|
|
368
|
+
body = instance_method(helper_method)
|
|
369
|
+
define_singleton_method(helper_method, body)
|
|
370
|
+
define_singleton_method(name, body)
|
|
371
371
|
helper_methods << helper_method
|
|
372
372
|
end
|
|
373
373
|
|
|
374
|
+
private_class_method(*helper_methods)
|
|
374
375
|
send(:private, *helper_methods)
|
|
375
376
|
end
|
|
376
377
|
end
|
data/lib/kapusta/compiler.rb
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'error'
|
|
3
4
|
require_relative 'compiler/runtime'
|
|
4
5
|
require_relative 'compiler/normalizer'
|
|
5
6
|
require_relative 'compiler/emitter'
|
|
6
7
|
|
|
7
8
|
module Kapusta
|
|
8
9
|
module Compiler
|
|
9
|
-
class Error <
|
|
10
|
+
class Error < Kapusta::Error; end
|
|
10
11
|
SPECIAL_FORMS = %w[
|
|
11
12
|
fn lambda λ let local var set if when unless case match
|
|
12
13
|
while for each do values
|
data/lib/kapusta/env.rb
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Kapusta
|
|
4
4
|
class Env
|
|
5
|
+
MethodBinding = Struct.new(:ruby_name)
|
|
6
|
+
|
|
5
7
|
def initialize(parent = nil)
|
|
6
8
|
@parent = parent
|
|
7
9
|
@vars = {}
|
|
@@ -25,6 +27,14 @@ module Kapusta
|
|
|
25
27
|
@vars.key?(name) || @parent&.defined?(name)
|
|
26
28
|
end
|
|
27
29
|
|
|
30
|
+
def ruby_name_defined?(name)
|
|
31
|
+
@vars.any? { |_source_name, value| binding_ruby_name(value) == name } || @parent&.ruby_name_defined?(name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def local_ruby_name_defined?(name)
|
|
35
|
+
@vars.any? { |_source_name, value| binding_ruby_name(value) == name }
|
|
36
|
+
end
|
|
37
|
+
|
|
28
38
|
def set_existing!(name, value)
|
|
29
39
|
if @vars.key?(name)
|
|
30
40
|
@vars[name] = value
|
|
@@ -38,5 +48,11 @@ module Kapusta
|
|
|
38
48
|
def child
|
|
39
49
|
Env.new(self)
|
|
40
50
|
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def binding_ruby_name(value)
|
|
55
|
+
value.respond_to?(:ruby_name) ? value.ruby_name : value
|
|
56
|
+
end
|
|
41
57
|
end
|
|
42
58
|
end
|
data/lib/kapusta/formatter.rb
CHANGED
|
@@ -177,15 +177,15 @@ module Kapusta
|
|
|
177
177
|
when Sym
|
|
178
178
|
form.name
|
|
179
179
|
when Vec
|
|
180
|
-
return
|
|
180
|
+
return if contains_comments?(form.items)
|
|
181
181
|
|
|
182
182
|
"[#{form.items.map { |item| flat_render(item) }.join(' ')}]"
|
|
183
183
|
when HashLit
|
|
184
|
-
return
|
|
184
|
+
return if contains_comments?(form.entries)
|
|
185
185
|
|
|
186
186
|
"{#{form.pairs.map { |key, value| flat_hash_pair(key, value) }.join(' ')}}"
|
|
187
187
|
when List
|
|
188
|
-
return
|
|
188
|
+
return if contains_comments?(form.items)
|
|
189
189
|
return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
|
|
190
190
|
|
|
191
191
|
"(#{form.items.map { |item| flat_render(item) }.join(' ')})"
|
|
@@ -548,13 +548,12 @@ module Kapusta
|
|
|
548
548
|
|
|
549
549
|
def render_hanging_pairwise_vec(vec)
|
|
550
550
|
pairs = vec.items.each_slice(2).to_a
|
|
551
|
-
|
|
552
|
-
left, right = pair
|
|
553
|
-
return nil unless pair.length == 2
|
|
551
|
+
return unless pairs.all? { |pair| pair.length == 2 }
|
|
554
552
|
|
|
553
|
+
rendered_pairs = pairs.map do |left, right|
|
|
555
554
|
render_binding_pair(left, right)
|
|
556
555
|
end
|
|
557
|
-
return
|
|
556
|
+
return if rendered_pairs.any?(&:nil?)
|
|
558
557
|
|
|
559
558
|
lines = ["[#{rendered_pairs.first}"]
|
|
560
559
|
continuation = ' ' * '(let ['.length
|
|
@@ -609,7 +608,7 @@ module Kapusta
|
|
|
609
608
|
def render_pair(left, right, indent)
|
|
610
609
|
left_rendered = flat_render(left) || render(left, indent)
|
|
611
610
|
right_rendered = flat_render(right) || render(right, indent)
|
|
612
|
-
return
|
|
611
|
+
return unless single_line?(left_rendered) && single_line?(right_rendered)
|
|
613
612
|
|
|
614
613
|
pair = "#{left_rendered} #{right_rendered}"
|
|
615
614
|
fits?(pair, indent) ? pair : nil
|
|
@@ -617,12 +616,12 @@ module Kapusta
|
|
|
617
616
|
|
|
618
617
|
def render_binding_pair(left, right)
|
|
619
618
|
left_rendered = flat_render(left)
|
|
620
|
-
return
|
|
619
|
+
return unless left_rendered
|
|
621
620
|
|
|
622
621
|
right_rendered = render(right, '(let ['.length + left_rendered.length + 1)
|
|
623
622
|
first_line, *rest = right_rendered.lines(chomp: true)
|
|
624
623
|
pair = "#{left_rendered} #{first_line}"
|
|
625
|
-
return
|
|
624
|
+
return unless pair.length <= MAX_WIDTH
|
|
626
625
|
|
|
627
626
|
return pair if rest.empty?
|
|
628
627
|
|
|
@@ -675,15 +674,6 @@ module Kapusta
|
|
|
675
674
|
end
|
|
676
675
|
end
|
|
677
676
|
|
|
678
|
-
def fn_body(form)
|
|
679
|
-
args = list_rest(form)
|
|
680
|
-
if args[0].is_a?(Sym) && args[1].is_a?(Vec)
|
|
681
|
-
args.drop(2)
|
|
682
|
-
else
|
|
683
|
-
args.drop(1)
|
|
684
|
-
end
|
|
685
|
-
end
|
|
686
|
-
|
|
687
677
|
def consecutive_requires?(previous, current)
|
|
688
678
|
require_form?(previous) && require_form?(current)
|
|
689
679
|
end
|
|
@@ -834,6 +824,6 @@ module Kapusta
|
|
|
834
824
|
puts 'Formats Kapusta source using the built-in Kapusta reader and pretty-printer.'
|
|
835
825
|
end
|
|
836
826
|
|
|
837
|
-
class Error <
|
|
827
|
+
class Error < Kapusta::Error; end
|
|
838
828
|
end
|
|
839
829
|
end
|
data/lib/kapusta/reader.rb
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative 'error'
|
|
4
|
+
|
|
3
5
|
module Kapusta
|
|
4
6
|
class Reader
|
|
7
|
+
class Error < Kapusta::Error; end
|
|
8
|
+
|
|
5
9
|
WHITESPACE = [' ', "\t", "\n", "\r", "\f", "\v", ','].freeze
|
|
6
10
|
DELIMS = ['(', ')', '[', ']', '{', '}', '"', ';'].freeze
|
|
7
11
|
|
|
@@ -61,7 +65,7 @@ module Kapusta
|
|
|
61
65
|
|
|
62
66
|
def read_next_item
|
|
63
67
|
skip_ws
|
|
64
|
-
raise 'unexpected eof' if eof?
|
|
68
|
+
raise Error, 'unexpected eof' if eof?
|
|
65
69
|
|
|
66
70
|
return read_comment if @preserve_comments && peek == ';'
|
|
67
71
|
|
|
@@ -70,7 +74,7 @@ module Kapusta
|
|
|
70
74
|
|
|
71
75
|
def read_form
|
|
72
76
|
skip_ws
|
|
73
|
-
raise 'unexpected eof' if eof?
|
|
77
|
+
raise Error, 'unexpected eof' if eof?
|
|
74
78
|
|
|
75
79
|
return read_comment if @preserve_comments && peek == ';'
|
|
76
80
|
|
|
@@ -93,7 +97,7 @@ module Kapusta
|
|
|
93
97
|
items = []
|
|
94
98
|
loop do
|
|
95
99
|
skip_ws
|
|
96
|
-
raise 'unclosed (' if eof?
|
|
100
|
+
raise Error, 'unclosed (' if eof?
|
|
97
101
|
break if peek == ')'
|
|
98
102
|
|
|
99
103
|
items << read_next_item
|
|
@@ -107,7 +111,7 @@ module Kapusta
|
|
|
107
111
|
items = []
|
|
108
112
|
loop do
|
|
109
113
|
skip_ws
|
|
110
|
-
raise 'unclosed [' if eof?
|
|
114
|
+
raise Error, 'unclosed [' if eof?
|
|
111
115
|
break if peek == ']'
|
|
112
116
|
|
|
113
117
|
items << read_next_item
|
|
@@ -122,7 +126,7 @@ module Kapusta
|
|
|
122
126
|
pending = []
|
|
123
127
|
loop do
|
|
124
128
|
skip_ws
|
|
125
|
-
raise 'unclosed {' if eof?
|
|
129
|
+
raise Error, 'unclosed {' if eof?
|
|
126
130
|
break if peek == '}'
|
|
127
131
|
|
|
128
132
|
item = read_next_item
|
|
@@ -139,7 +143,7 @@ module Kapusta
|
|
|
139
143
|
end
|
|
140
144
|
advance
|
|
141
145
|
|
|
142
|
-
raise 'odd number of forms in hash' unless pending.empty?
|
|
146
|
+
raise Error, 'odd number of forms in hash' unless pending.empty?
|
|
143
147
|
|
|
144
148
|
HashLit.new(entries)
|
|
145
149
|
end
|
|
@@ -168,7 +172,7 @@ module Kapusta
|
|
|
168
172
|
buffer << advance
|
|
169
173
|
end
|
|
170
174
|
end
|
|
171
|
-
raise 'unterminated string' if eof?
|
|
175
|
+
raise Error, 'unterminated string' if eof?
|
|
172
176
|
|
|
173
177
|
advance
|
|
174
178
|
buffer
|
|
@@ -209,7 +213,7 @@ module Kapusta
|
|
|
209
213
|
start = @pos
|
|
210
214
|
advance until delim?(peek)
|
|
211
215
|
token = @src[start...@pos]
|
|
212
|
-
raise 'empty token' if token.empty?
|
|
216
|
+
raise Error, 'empty token' if token.empty?
|
|
213
217
|
|
|
214
218
|
parse_atom(token)
|
|
215
219
|
end
|
|
@@ -217,9 +221,9 @@ module Kapusta
|
|
|
217
221
|
def parse_atom(token)
|
|
218
222
|
return true if token == 'true'
|
|
219
223
|
return false if token == 'false'
|
|
220
|
-
return
|
|
221
|
-
return token
|
|
222
|
-
return token
|
|
224
|
+
return if token == 'nil'
|
|
225
|
+
return Integer(token, 10) if token.match?(/\A-?\d+\z/)
|
|
226
|
+
return Float(token) if token.match?(/\A-?\d+\.\d+\z/)
|
|
223
227
|
|
|
224
228
|
if token.start_with?(':') && token.length > 1
|
|
225
229
|
Kapusta.kebab_to_snake(token[1..]).to_sym
|
|
@@ -230,7 +234,7 @@ module Kapusta
|
|
|
230
234
|
|
|
231
235
|
def normalize_hash_pair(item, value)
|
|
232
236
|
if item.is_a?(Sym) && item.name == ':'
|
|
233
|
-
raise 'bad shorthand' unless value.is_a?(Sym)
|
|
237
|
+
raise Error, 'bad shorthand' unless value.is_a?(Sym)
|
|
234
238
|
|
|
235
239
|
key = Kapusta.kebab_to_snake(value.name).to_sym
|
|
236
240
|
[key, value]
|
data/lib/kapusta/version.rb
CHANGED
data/lib/kapusta.rb
CHANGED
data/spec/cli_spec.rb
CHANGED
|
@@ -26,17 +26,23 @@ ensure
|
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
RSpec.describe Kapusta::CLI do
|
|
29
|
-
it 'compiles a .kap file to
|
|
29
|
+
it 'compiles a .kap file to runnable Ruby with --compile' do
|
|
30
30
|
path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
ruby = capture_stdout do
|
|
33
33
|
described_class.start(['--compile', path])
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
Dir.mktmpdir do |dir|
|
|
37
|
+
output_path = File.join(dir, 'fizzbuzz.rb')
|
|
38
|
+
File.write(output_path, ruby)
|
|
39
|
+
|
|
40
|
+
stdout, stderr, status = Open3.capture3(RbConfig.ruby, output_path)
|
|
41
|
+
|
|
42
|
+
expected = "1\n2\nFizz\n4\nBuzz\nFizz\n7\n8\nFizz\nBuzz\n11\nFizz\n13\n14\nFizzBuzz\n16\n17\nFizz\n19\nBuzz\n"
|
|
43
|
+
expect(status.success?).to eq(true), stderr
|
|
44
|
+
expect(stdout).to eq(expected)
|
|
45
|
+
end
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
it 'rejects extra positional arguments in compile mode' do
|
|
@@ -50,21 +56,6 @@ RSpec.describe Kapusta::CLI do
|
|
|
50
56
|
expect(error_output).to include('usage: kapusta')
|
|
51
57
|
end
|
|
52
58
|
|
|
53
|
-
it 'emits standalone Ruby that runs with plain ruby' do
|
|
54
|
-
source_path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
|
|
55
|
-
|
|
56
|
-
Dir.mktmpdir do |dir|
|
|
57
|
-
output_path = File.join(dir, 'fizzbuzz.rb')
|
|
58
|
-
ruby = Kapusta.compile(File.read(source_path), path: source_path)
|
|
59
|
-
File.write(output_path, ruby)
|
|
60
|
-
|
|
61
|
-
stdout, stderr, status = Open3.capture3(RbConfig.ruby, output_path)
|
|
62
|
-
|
|
63
|
-
expect(status.success?).to eq(true), stderr
|
|
64
|
-
expect(stdout).to include("FizzBuzz\n")
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
59
|
it 'passes remaining arguments through to the Kapusta program' do
|
|
69
60
|
path = File.expand_path('../examples/greet.kap', __dir__)
|
|
70
61
|
|
data/spec/examples_spec.rb
CHANGED
|
@@ -63,6 +63,10 @@ RSpec.describe 'examples' do
|
|
|
63
63
|
expect(run_example('counter.kap')).to eq("12\n")
|
|
64
64
|
end
|
|
65
65
|
|
|
66
|
+
it 'contains-duplicate.kap' do
|
|
67
|
+
expect(run_example('contains-duplicate.kap')).to eq("true\nfalse\ntrue\n")
|
|
68
|
+
end
|
|
69
|
+
|
|
66
70
|
it 'doto.kap' do
|
|
67
71
|
expect(run_example('doto.kap')).to eq("1, 2, 3\n")
|
|
68
72
|
end
|
|
@@ -302,6 +306,6 @@ end
|
|
|
302
306
|
RSpec.describe 'errors' do
|
|
303
307
|
it 'raises on unclosed list' do
|
|
304
308
|
expect { Kapusta.eval('(fn hello [name] (.. "Hi " name "!")') }
|
|
305
|
-
.to raise_error(/unclosed \(/)
|
|
309
|
+
.to raise_error(Kapusta::Reader::Error, /unclosed \(/)
|
|
306
310
|
end
|
|
307
311
|
end
|