kapusta 0.1.5 → 0.2.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 +13 -0
- data/examples/bank-account.kap +21 -0
- data/examples/baseball-game.kap +11 -0
- data/examples/climbing-stairs.kap +13 -0
- data/examples/doto-hygiene.kap +5 -0
- data/examples/happy-number.kap +20 -0
- data/examples/length-of-last-word.kap +7 -0
- data/examples/maximum-subarray.kap +12 -0
- data/examples/move-zeroes.kap +13 -0
- data/examples/stack.kap +27 -10
- data/examples/two-sum-hash.kap +17 -0
- data/examples/use_bank_account.rb +13 -0
- data/examples/valid-parentheses-1.kap +19 -0
- data/examples/valid-parentheses-2.kap +8 -0
- data/lib/kapusta/ast.rb +33 -3
- data/lib/kapusta/compiler/emitter/bindings.rb +35 -25
- data/lib/kapusta/compiler/emitter/control_flow.rb +4 -4
- data/lib/kapusta/compiler/emitter/expressions.rb +13 -13
- data/lib/kapusta/compiler/emitter/interop.rb +91 -40
- data/lib/kapusta/compiler/emitter/patterns.rb +14 -11
- data/lib/kapusta/compiler/emitter/support.rb +13 -11
- data/lib/kapusta/compiler/emitter.rb +1 -5
- data/lib/kapusta/compiler/normalizer.rb +41 -17
- data/lib/kapusta/compiler/runtime.rb +0 -152
- data/lib/kapusta/env.rb +21 -6
- data/lib/kapusta/reader.rb +27 -3
- data/lib/kapusta/version.rb +1 -1
- data/lib/kapusta.rb +62 -1
- data/spec/cli_spec.rb +25 -2
- data/spec/examples_spec.rb +234 -87
- data/spec/reader_spec.rb +26 -0
- metadata +16 -7
data/lib/kapusta/env.rb
CHANGED
|
@@ -10,12 +10,13 @@ module Kapusta
|
|
|
10
10
|
end
|
|
11
11
|
|
|
12
12
|
def define(name, value)
|
|
13
|
-
@vars[name] = value
|
|
13
|
+
@vars[binding_key(name)] = value
|
|
14
14
|
end
|
|
15
15
|
|
|
16
16
|
def lookup(name)
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
key = binding_key(name)
|
|
18
|
+
if @vars.key?(key)
|
|
19
|
+
@vars[key]
|
|
19
20
|
elsif @parent
|
|
20
21
|
@parent.lookup(name)
|
|
21
22
|
else
|
|
@@ -23,8 +24,17 @@ module Kapusta
|
|
|
23
24
|
end
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
def lookup_if_defined(name)
|
|
28
|
+
key = binding_key(name)
|
|
29
|
+
if @vars.key?(key)
|
|
30
|
+
@vars[key]
|
|
31
|
+
else
|
|
32
|
+
@parent&.lookup_if_defined(name)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
26
36
|
def defined?(name)
|
|
27
|
-
@vars.key?(name) || @parent&.defined?(name)
|
|
37
|
+
@vars.key?(binding_key(name)) || @parent&.defined?(name)
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
def ruby_name_defined?(name)
|
|
@@ -36,8 +46,9 @@ module Kapusta
|
|
|
36
46
|
end
|
|
37
47
|
|
|
38
48
|
def set_existing!(name, value)
|
|
39
|
-
|
|
40
|
-
|
|
49
|
+
key = binding_key(name)
|
|
50
|
+
if @vars.key?(key)
|
|
51
|
+
@vars[key] = value
|
|
41
52
|
elsif @parent
|
|
42
53
|
@parent.set_existing!(name, value)
|
|
43
54
|
else
|
|
@@ -51,6 +62,10 @@ module Kapusta
|
|
|
51
62
|
|
|
52
63
|
private
|
|
53
64
|
|
|
65
|
+
def binding_key(name)
|
|
66
|
+
name.respond_to?(:binding_key) ? name.binding_key : name
|
|
67
|
+
end
|
|
68
|
+
|
|
54
69
|
def binding_ruby_name(value)
|
|
55
70
|
value.respond_to?(:ruby_name) ? value.ruby_name : value
|
|
56
71
|
end
|
data/lib/kapusta/reader.rb
CHANGED
|
@@ -8,6 +8,7 @@ module Kapusta
|
|
|
8
8
|
|
|
9
9
|
WHITESPACE = [' ', "\t", "\n", "\r", "\f", "\v", ','].freeze
|
|
10
10
|
DELIMS = ['(', ')', '[', ']', '{', '}', '"', ';'].freeze
|
|
11
|
+
CLOSING_DELIMS = [')', ']', '}'].freeze
|
|
11
12
|
|
|
12
13
|
def self.read_all(source, preserve_comments: false)
|
|
13
14
|
new(source, preserve_comments:).read_all
|
|
@@ -85,6 +86,7 @@ module Kapusta
|
|
|
85
86
|
when '{' then read_hash
|
|
86
87
|
when '"' then read_string
|
|
87
88
|
when '#' then read_hashfn
|
|
89
|
+
when *CLOSING_DELIMS then raise unexpected_closing_delim(peek)
|
|
88
90
|
else
|
|
89
91
|
read_atom
|
|
90
92
|
end
|
|
@@ -93,11 +95,12 @@ module Kapusta
|
|
|
93
95
|
end
|
|
94
96
|
|
|
95
97
|
def read_list
|
|
98
|
+
opening_position = source_position
|
|
96
99
|
advance
|
|
97
100
|
items = []
|
|
98
101
|
loop do
|
|
99
102
|
skip_ws
|
|
100
|
-
raise
|
|
103
|
+
raise unclosed_opening_delim('(', opening_position) if eof?
|
|
101
104
|
break if peek == ')'
|
|
102
105
|
|
|
103
106
|
items << read_next_item
|
|
@@ -107,11 +110,12 @@ module Kapusta
|
|
|
107
110
|
end
|
|
108
111
|
|
|
109
112
|
def read_vec
|
|
113
|
+
opening_position = source_position
|
|
110
114
|
advance
|
|
111
115
|
items = []
|
|
112
116
|
loop do
|
|
113
117
|
skip_ws
|
|
114
|
-
raise
|
|
118
|
+
raise unclosed_opening_delim('[', opening_position) if eof?
|
|
115
119
|
break if peek == ']'
|
|
116
120
|
|
|
117
121
|
items << read_next_item
|
|
@@ -121,12 +125,13 @@ module Kapusta
|
|
|
121
125
|
end
|
|
122
126
|
|
|
123
127
|
def read_hash
|
|
128
|
+
opening_position = source_position
|
|
124
129
|
advance
|
|
125
130
|
entries = []
|
|
126
131
|
pending = []
|
|
127
132
|
loop do
|
|
128
133
|
skip_ws
|
|
129
|
-
raise
|
|
134
|
+
raise unclosed_opening_delim('{', opening_position) if eof?
|
|
130
135
|
break if peek == '}'
|
|
131
136
|
|
|
132
137
|
item = read_next_item
|
|
@@ -218,6 +223,25 @@ module Kapusta
|
|
|
218
223
|
parse_atom(token)
|
|
219
224
|
end
|
|
220
225
|
|
|
226
|
+
def unexpected_closing_delim(char)
|
|
227
|
+
line, column = source_position
|
|
228
|
+
Error.new("unexpected closing delimiter '#{char}' at line #{line}, column #{column}")
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def unclosed_opening_delim(char, position)
|
|
232
|
+
line, column = position
|
|
233
|
+
Error.new("unclosed opening delimiter '#{char}' at line #{line}, column #{column}")
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def source_position
|
|
237
|
+
prefix = @src[0...@pos]
|
|
238
|
+
line = prefix.count("\n") + 1
|
|
239
|
+
last_newline = prefix.rindex("\n")
|
|
240
|
+
column = last_newline ? prefix.length - last_newline : prefix.length + 1
|
|
241
|
+
|
|
242
|
+
[line, column]
|
|
243
|
+
end
|
|
244
|
+
|
|
221
245
|
def parse_atom(token)
|
|
222
246
|
return true if token == 'true'
|
|
223
247
|
return false if token == 'false'
|
data/lib/kapusta/version.rb
CHANGED
data/lib/kapusta.rb
CHANGED
|
@@ -9,6 +9,8 @@ require_relative 'kapusta/env'
|
|
|
9
9
|
require_relative 'kapusta/compiler'
|
|
10
10
|
|
|
11
11
|
module Kapusta
|
|
12
|
+
@loaded_kapusta_features = {}
|
|
13
|
+
|
|
12
14
|
def self.eval(source, path: '(eval)', **_opts)
|
|
13
15
|
Compiler.run(source, path:)
|
|
14
16
|
end
|
|
@@ -22,10 +24,69 @@ module Kapusta
|
|
|
22
24
|
Compiler.compile(source, path:)
|
|
23
25
|
end
|
|
24
26
|
|
|
27
|
+
def self.require(feature, relative_to: nil)
|
|
28
|
+
feature = feature.to_s
|
|
29
|
+
local_path = resolve_require_path(feature, relative_to:)
|
|
30
|
+
|
|
31
|
+
return require_kapusta_file(local_path) if local_path&.end_with?('.kap')
|
|
32
|
+
return Kernel.require(local_path) if local_path
|
|
33
|
+
|
|
34
|
+
Kernel.require(feature)
|
|
35
|
+
end
|
|
36
|
+
|
|
25
37
|
def self.install!
|
|
26
38
|
@install ||= begin
|
|
27
|
-
require 'rubygems'
|
|
39
|
+
Kernel.require 'rubygems'
|
|
28
40
|
true
|
|
29
41
|
end
|
|
30
42
|
end
|
|
43
|
+
|
|
44
|
+
def self.resolve_require_path(feature, relative_to:)
|
|
45
|
+
return unless local_feature?(feature)
|
|
46
|
+
|
|
47
|
+
path =
|
|
48
|
+
if File.absolute_path?(feature)
|
|
49
|
+
feature
|
|
50
|
+
else
|
|
51
|
+
File.expand_path(feature, require_base_dir(relative_to))
|
|
52
|
+
end
|
|
53
|
+
existing_feature_path(path)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def self.local_feature?(feature)
|
|
57
|
+
feature.start_with?('./', '../') || File.absolute_path?(feature)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.require_base_dir(relative_to)
|
|
61
|
+
return Dir.pwd if relative_to.nil? || relative_to.start_with?('(')
|
|
62
|
+
|
|
63
|
+
File.dirname(File.expand_path(relative_to))
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.existing_feature_path(path)
|
|
67
|
+
candidates =
|
|
68
|
+
if File.extname(path).empty?
|
|
69
|
+
[path, "#{path}.kap", "#{path}.rb"]
|
|
70
|
+
else
|
|
71
|
+
[path]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
candidates.find { |candidate| File.file?(candidate) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def self.require_kapusta_file(path)
|
|
78
|
+
expanded = File.realpath(path)
|
|
79
|
+
return false if @loaded_kapusta_features[expanded]
|
|
80
|
+
|
|
81
|
+
@loaded_kapusta_features[expanded] = true
|
|
82
|
+
dofile(expanded)
|
|
83
|
+
$LOADED_FEATURES << expanded unless $LOADED_FEATURES.include?(expanded)
|
|
84
|
+
true
|
|
85
|
+
rescue StandardError, ScriptError
|
|
86
|
+
@loaded_kapusta_features.delete(expanded) if expanded
|
|
87
|
+
raise
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private_class_method :resolve_require_path, :local_feature?, :require_base_dir,
|
|
91
|
+
:existing_feature_path, :require_kapusta_file
|
|
31
92
|
end
|
data/spec/cli_spec.rb
CHANGED
|
@@ -39,7 +39,28 @@ RSpec.describe Kapusta::CLI do
|
|
|
39
39
|
|
|
40
40
|
stdout, stderr, status = Open3.capture3(RbConfig.ruby, output_path)
|
|
41
41
|
|
|
42
|
-
expected =
|
|
42
|
+
expected = <<~OUT
|
|
43
|
+
1
|
|
44
|
+
2
|
|
45
|
+
"Fizz"
|
|
46
|
+
4
|
|
47
|
+
"Buzz"
|
|
48
|
+
"Fizz"
|
|
49
|
+
7
|
|
50
|
+
8
|
|
51
|
+
"Fizz"
|
|
52
|
+
"Buzz"
|
|
53
|
+
11
|
|
54
|
+
"Fizz"
|
|
55
|
+
13
|
|
56
|
+
14
|
|
57
|
+
"FizzBuzz"
|
|
58
|
+
16
|
|
59
|
+
17
|
|
60
|
+
"Fizz"
|
|
61
|
+
19
|
|
62
|
+
"Buzz"
|
|
63
|
+
OUT
|
|
43
64
|
expect(status.success?).to eq(true), stderr
|
|
44
65
|
expect(stdout).to eq(expected)
|
|
45
66
|
end
|
|
@@ -63,7 +84,9 @@ RSpec.describe Kapusta::CLI do
|
|
|
63
84
|
described_class.start([path, 'Ada'])
|
|
64
85
|
end
|
|
65
86
|
|
|
66
|
-
expect(output).to eq(
|
|
87
|
+
expect(output).to eq(<<~OUT)
|
|
88
|
+
"Hello, Ada!"
|
|
89
|
+
OUT
|
|
67
90
|
end
|
|
68
91
|
|
|
69
92
|
it 'prints the version with -v' do
|