kapusta 0.1.2 → 0.1.3
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/examples/anonymous-greeter.kap +4 -0
- data/examples/binary-to-decimal.kap +7 -0
- data/examples/or-patterns.kap +8 -0
- data/examples/packet-router.kap +26 -0
- data/examples/tic-tac-toe.kap +18 -0
- data/examples/underscore-patterns.kap +14 -0
- data/lib/kapusta/compiler/emitter/collections.rb +21 -50
- data/lib/kapusta/compiler/emitter/control_flow.rb +28 -50
- data/lib/kapusta/compiler/emitter/expressions.rb +2 -1
- data/lib/kapusta/compiler/emitter/patterns.rb +133 -2
- data/lib/kapusta/compiler/emitter/support.rb +41 -0
- data/lib/kapusta/compiler/runtime.rb +20 -5
- data/lib/kapusta/formatter.rb +9 -7
- data/lib/kapusta/version.rb +1 -1
- data/spec/examples_spec.rb +24 -0
- data/spec/formatter_spec.rb +20 -0
- metadata +8 -3
- data/kapfmt +0 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2398c1c908306ac808914717a67e74f8085ee6048deb9c9dd665f4dd8c39cc2f
|
|
4
|
+
data.tar.gz: 1022c2df415418d26f1b84b1b16830cc525829182a82e93c1a450d0dd4b5bdd2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 6a31f0bab47d9c5c0d40720c52422717e0d04a9c03bb03a115ac62c7f541226dc3d9b7e9aa052bf55d574e1193ecab82e97b0610d43bbe1702038024486a1953
|
|
7
|
+
data.tar.gz: f20bf20a890d1b23f3ec7d2d113a3340c6255132a367a655cddab52ded9e2276bd236439d242ca0ea46f857096a5f5885620dd9a104f8b0f337c9aba75e50366
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
(fn inbox-line [user event]
|
|
2
|
+
(match event
|
|
3
|
+
[:score user points] (.. "score:" points)
|
|
4
|
+
[:profile user ?city] (if city (.. "city:" city) "city:nil")
|
|
5
|
+
_ "other"))
|
|
6
|
+
|
|
7
|
+
(fn score-delta [user event]
|
|
8
|
+
(case event
|
|
9
|
+
(where (or [:bonus (= user) points] [:score (= user) points])
|
|
10
|
+
(> points 0)
|
|
11
|
+
(< points 10))
|
|
12
|
+
points
|
|
13
|
+
_ 0))
|
|
14
|
+
|
|
15
|
+
(fn packet-kind [packet]
|
|
16
|
+
(case packet
|
|
17
|
+
[:ping seq] (.. "ping:" seq)
|
|
18
|
+
[:pong seq] (.. "pong:" seq)
|
|
19
|
+
_ "other"))
|
|
20
|
+
|
|
21
|
+
(print (inbox-line "Ada" [:score "Ada" 9]))
|
|
22
|
+
(print (inbox-line "Ada" [:score "Lin" 7]))
|
|
23
|
+
(print (inbox-line "Ada" [:profile "Ada" nil]))
|
|
24
|
+
(print (score-delta "Ada" [:bonus "Ada" 5]))
|
|
25
|
+
(print (score-delta "Ada" [:score "Lin" 5]))
|
|
26
|
+
(print (packet-kind [:ping 7 :fast]))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
(fn winner [board]
|
|
2
|
+
(case board
|
|
3
|
+
[["X" "X" "X"] _ _] "X"
|
|
4
|
+
[_ ["O" "O" "O"] _] "O"
|
|
5
|
+
[["X" _ _] [_ "X" _] [_ _ "X"]] "X"
|
|
6
|
+
[["O" _ _] ["O" _ _] ["O" _ _]] "O"
|
|
7
|
+
_ "draw"))
|
|
8
|
+
|
|
9
|
+
(each
|
|
10
|
+
[
|
|
11
|
+
_
|
|
12
|
+
board
|
|
13
|
+
(ipairs [
|
|
14
|
+
[["X" "X" "X"] ["O" "" ""] ["" "O" ""]]
|
|
15
|
+
[["O" "X" "X"] ["O" "" "X"] ["O" "" ""]]
|
|
16
|
+
[["X" "O" ""] ["" "X" "O"] ["" "" "X"]]
|
|
17
|
+
[["X" "O" "X"] ["O" "X" "O"] ["O" "X" "O"]]])]
|
|
18
|
+
(print (winner board)))
|
|
@@ -50,44 +50,19 @@ module Kapusta
|
|
|
50
50
|
|
|
51
51
|
def emit_fcollect(args, env, current_scope)
|
|
52
52
|
result_var = temp('result')
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
finish_code = emit_expr(bindings[2], env, current_scope)
|
|
59
|
-
step_code = '1'
|
|
60
|
-
until_form = nil
|
|
61
|
-
i = 3
|
|
62
|
-
while i < bindings.length
|
|
63
|
-
if bindings[i].is_a?(Sym) && bindings[i].name == '&until'
|
|
64
|
-
until_form = bindings[i + 1]
|
|
65
|
-
i += 2
|
|
66
|
-
else
|
|
67
|
-
step_code = emit_expr(bindings[i], env, current_scope)
|
|
68
|
-
i += 1
|
|
53
|
+
parsed = parse_counted_for_bindings(args[0].items, env, current_scope)
|
|
54
|
+
body_code, = emit_sequence(args[1..], parsed[:loop_env], current_scope, allow_method_definitions: false)
|
|
55
|
+
collecting_body = <<~RUBY.chomp
|
|
56
|
+
__kap_value = begin
|
|
57
|
+
#{indent(body_code)}
|
|
69
58
|
end
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
finish_var = temp('finish')
|
|
74
|
-
step_var = temp('step')
|
|
75
|
-
cmp_var = temp('cmp')
|
|
59
|
+
#{result_var} << __kap_value unless __kap_value.nil?
|
|
60
|
+
RUBY
|
|
61
|
+
loop_code = emit_counted_loop(**parsed, current_scope:, body_code: collecting_body)
|
|
76
62
|
<<~RUBY.chomp
|
|
77
63
|
(-> do
|
|
78
64
|
#{result_var} = []
|
|
79
|
-
#{
|
|
80
|
-
#{finish_var} = #{finish_code}
|
|
81
|
-
#{step_var} = #{step_code}
|
|
82
|
-
#{cmp_var} = #{step_var} >= 0 ? :<= : :>=
|
|
83
|
-
while #{ruby_name}.public_send(#{cmp_var}, #{finish_var})
|
|
84
|
-
#{until_code}
|
|
85
|
-
__kap_value = begin
|
|
86
|
-
#{indent(body_code)}
|
|
87
|
-
end
|
|
88
|
-
#{result_var} << __kap_value unless __kap_value.nil?
|
|
89
|
-
#{ruby_name} += #{step_var}
|
|
90
|
-
end
|
|
65
|
+
#{indent(loop_code)}
|
|
91
66
|
#{result_var}
|
|
92
67
|
end).call
|
|
93
68
|
RUBY
|
|
@@ -124,26 +99,22 @@ module Kapusta
|
|
|
124
99
|
loop_env = env.child
|
|
125
100
|
loop_env.define(acc_name.name, acc_var)
|
|
126
101
|
loop_env.define(loop_name.name, loop_var)
|
|
127
|
-
start_code = emit_expr(bindings[3], env, current_scope)
|
|
128
|
-
finish_code = emit_expr(bindings[4], env, current_scope)
|
|
129
|
-
step_code = bindings[5] ? emit_expr(bindings[5], env, current_scope) : '1'
|
|
130
102
|
body_code, = emit_sequence(args[1..], loop_env, current_scope, allow_method_definitions: false)
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
103
|
+
accumulating_body = "#{acc_var} = begin\n#{indent(body_code)}\nend"
|
|
104
|
+
loop_code = emit_counted_loop(
|
|
105
|
+
ruby_name: loop_var,
|
|
106
|
+
start_code: emit_expr(bindings[3], env, current_scope),
|
|
107
|
+
finish_code: emit_expr(bindings[4], env, current_scope),
|
|
108
|
+
step_code: bindings[5] ? emit_expr(bindings[5], env, current_scope) : '1',
|
|
109
|
+
until_form: nil,
|
|
110
|
+
loop_env:,
|
|
111
|
+
current_scope:,
|
|
112
|
+
body_code: accumulating_body
|
|
113
|
+
)
|
|
134
114
|
<<~RUBY.chomp
|
|
135
115
|
(-> do
|
|
136
116
|
#{acc_var} = #{emit_expr(bindings[1], env, current_scope)}
|
|
137
|
-
#{
|
|
138
|
-
#{finish_var} = #{finish_code}
|
|
139
|
-
#{step_var} = #{step_code}
|
|
140
|
-
#{cmp_var} = #{step_var} >= 0 ? :<= : :>=
|
|
141
|
-
while #{loop_var}.public_send(#{cmp_var}, #{finish_var})
|
|
142
|
-
#{acc_var} = begin
|
|
143
|
-
#{indent(body_code)}
|
|
144
|
-
end
|
|
145
|
-
#{loop_var} += #{step_var}
|
|
146
|
-
end
|
|
117
|
+
#{indent(loop_code)}
|
|
147
118
|
#{acc_var}
|
|
148
119
|
end).call
|
|
149
120
|
RUBY
|
|
@@ -26,9 +26,9 @@ module Kapusta
|
|
|
26
26
|
RUBY
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def emit_case(args, env, current_scope)
|
|
29
|
+
def emit_case(args, env, current_scope, mode)
|
|
30
30
|
value_var = temp('case_value')
|
|
31
|
-
body = build_case_clauses(value_var, args[1..], env, current_scope)
|
|
31
|
+
body = build_case_clauses(value_var, args[1..], env, current_scope, mode)
|
|
32
32
|
<<~RUBY.chomp
|
|
33
33
|
(-> do
|
|
34
34
|
#{value_var} = #{emit_expr(args[0], env, current_scope)}
|
|
@@ -37,31 +37,32 @@ module Kapusta
|
|
|
37
37
|
RUBY
|
|
38
38
|
end
|
|
39
39
|
|
|
40
|
-
def build_case_clauses(value_var, clauses, env, current_scope)
|
|
40
|
+
def build_case_clauses(value_var, clauses, env, current_scope, mode)
|
|
41
41
|
return 'nil' if clauses.empty?
|
|
42
42
|
|
|
43
43
|
pattern = clauses[0]
|
|
44
44
|
body = clauses[1]
|
|
45
|
-
else_code = build_case_clauses(value_var, clauses[2..], env, current_scope)
|
|
46
|
-
emit_case_clause(value_var, pattern, body, else_code, env, current_scope)
|
|
45
|
+
else_code = build_case_clauses(value_var, clauses[2..], env, current_scope, mode)
|
|
46
|
+
emit_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
-
def emit_case_clause(value_var, pattern, body, else_code, env, current_scope)
|
|
49
|
+
def emit_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
|
|
50
50
|
if where_pattern?(pattern)
|
|
51
|
-
emit_guarded_case_clause(value_var, pattern, body, else_code, env, current_scope)
|
|
51
|
+
emit_guarded_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
|
|
52
52
|
else
|
|
53
|
-
emit_simple_case_clause(value_var, pattern, body, else_code, env, current_scope)
|
|
53
|
+
emit_simple_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
|
|
54
54
|
end
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
-
def emit_simple_case_clause(value_var, pattern, body, else_code, env, current_scope)
|
|
57
|
+
def emit_simple_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
|
|
58
58
|
match_var = temp('match')
|
|
59
59
|
bindings_var = temp('bindings')
|
|
60
|
+
plan = pattern_match_plan(pattern, env, mode:, allow_pins: false)
|
|
60
61
|
arm_env = env.child
|
|
61
|
-
assign_code, arm_env = emit_bindings_from_match(
|
|
62
|
+
assign_code, arm_env = emit_bindings_from_match(plan[:bindings], bindings_var, arm_env)
|
|
62
63
|
body_code = emit_expr(body, arm_env, current_scope)
|
|
63
64
|
<<~RUBY.chomp
|
|
64
|
-
#{match_var} = #{runtime_call(:match_pattern,
|
|
65
|
+
#{match_var} = #{runtime_call(:match_pattern, plan[:pattern], value_var)}
|
|
65
66
|
if #{match_var}[0]
|
|
66
67
|
#{bindings_var} = #{match_var}[1]
|
|
67
68
|
#{assign_code}
|
|
@@ -72,17 +73,18 @@ module Kapusta
|
|
|
72
73
|
RUBY
|
|
73
74
|
end
|
|
74
75
|
|
|
75
|
-
def emit_guarded_case_clause(value_var, pattern, body, else_code, env, current_scope)
|
|
76
|
+
def emit_guarded_case_clause(value_var, pattern, body, else_code, env, current_scope, mode)
|
|
76
77
|
inner = pattern.items[1]
|
|
77
|
-
|
|
78
|
+
guards = pattern.items[2..]
|
|
78
79
|
match_var = temp('match')
|
|
79
80
|
bindings_var = temp('bindings')
|
|
81
|
+
plan = pattern_match_plan(inner, env, mode:, allow_pins: mode == :case)
|
|
80
82
|
arm_env = env.child
|
|
81
|
-
assign_code, arm_env = emit_bindings_from_match(
|
|
82
|
-
guard_code =
|
|
83
|
+
assign_code, arm_env = emit_bindings_from_match(plan[:bindings], bindings_var, arm_env)
|
|
84
|
+
guard_code = emit_case_guards(guards, arm_env, current_scope)
|
|
83
85
|
body_code = emit_expr(body, arm_env, current_scope)
|
|
84
86
|
<<~RUBY.chomp
|
|
85
|
-
#{match_var} = #{runtime_call(:match_pattern,
|
|
87
|
+
#{match_var} = #{runtime_call(:match_pattern, plan[:pattern], value_var)}
|
|
86
88
|
if #{match_var}[0]
|
|
87
89
|
#{bindings_var} = #{match_var}[1]
|
|
88
90
|
#{assign_code}
|
|
@@ -97,6 +99,12 @@ module Kapusta
|
|
|
97
99
|
RUBY
|
|
98
100
|
end
|
|
99
101
|
|
|
102
|
+
def emit_case_guards(guards, env, current_scope)
|
|
103
|
+
return 'true' if guards.empty?
|
|
104
|
+
|
|
105
|
+
guards.map { |guard| parenthesize(emit_expr(guard, env, current_scope)) }.join(' && ')
|
|
106
|
+
end
|
|
107
|
+
|
|
100
108
|
def emit_while(args, env, current_scope)
|
|
101
109
|
body_code, = emit_sequence(args[1..], env, current_scope, allow_method_definitions: false)
|
|
102
110
|
<<~RUBY.chomp
|
|
@@ -110,42 +118,12 @@ module Kapusta
|
|
|
110
118
|
end
|
|
111
119
|
|
|
112
120
|
def emit_for(args, env, current_scope)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
finish_code = emit_expr(bindings[2], env, current_scope)
|
|
117
|
-
step_code = '1'
|
|
118
|
-
until_form = nil
|
|
119
|
-
i = 3
|
|
120
|
-
while i < bindings.length
|
|
121
|
-
if bindings[i].is_a?(Sym) && bindings[i].name == '&until'
|
|
122
|
-
until_form = bindings[i + 1]
|
|
123
|
-
i += 2
|
|
124
|
-
else
|
|
125
|
-
step_code = emit_expr(bindings[i], env, current_scope)
|
|
126
|
-
i += 1
|
|
127
|
-
end
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
loop_env = env.child
|
|
131
|
-
ruby_name = temp(sanitize_local(name.name))
|
|
132
|
-
loop_env.define(name.name, ruby_name)
|
|
133
|
-
body_code, = emit_sequence(args[1..], loop_env, current_scope, allow_method_definitions: false)
|
|
134
|
-
until_code = until_form ? "break if #{emit_expr(until_form, loop_env, current_scope)}" : nil
|
|
135
|
-
cmp_var = temp('cmp')
|
|
136
|
-
step_var = temp('step')
|
|
137
|
-
finish_var = temp('finish')
|
|
121
|
+
parsed = parse_counted_for_bindings(args[0].items, env, current_scope)
|
|
122
|
+
body_code, = emit_sequence(args[1..], parsed[:loop_env], current_scope, allow_method_definitions: false)
|
|
123
|
+
loop_code = emit_counted_loop(**parsed, current_scope:, body_code:)
|
|
138
124
|
<<~RUBY.chomp
|
|
139
125
|
(-> do
|
|
140
|
-
#{
|
|
141
|
-
#{finish_var} = #{finish_code}
|
|
142
|
-
#{step_var} = #{step_code}
|
|
143
|
-
#{cmp_var} = #{step_var} >= 0 ? :<= : :>=
|
|
144
|
-
while #{ruby_name}.public_send(#{cmp_var}, #{finish_var})
|
|
145
|
-
#{until_code}
|
|
146
|
-
#{indent(body_code)}
|
|
147
|
-
#{ruby_name} += #{step_var}
|
|
148
|
-
end
|
|
126
|
+
#{indent(loop_code)}
|
|
149
127
|
nil
|
|
150
128
|
end).call
|
|
151
129
|
RUBY
|
|
@@ -51,7 +51,8 @@ module Kapusta
|
|
|
51
51
|
when 'local', 'var' then emit_local_expr(args, env, current_scope)
|
|
52
52
|
when 'set' then emit_set_expr(args, env, current_scope)
|
|
53
53
|
when 'if' then emit_if(args, env, current_scope)
|
|
54
|
-
when 'case'
|
|
54
|
+
when 'case' then emit_case(args, env, current_scope, :case)
|
|
55
|
+
when 'match' then emit_case(args, env, current_scope, :match)
|
|
55
56
|
when 'while' then emit_while(args, env, current_scope)
|
|
56
57
|
when 'for' then emit_for(args, env, current_scope)
|
|
57
58
|
when 'each' then emit_each(args, env, current_scope)
|
|
@@ -28,10 +28,10 @@ module Kapusta
|
|
|
28
28
|
end
|
|
29
29
|
end
|
|
30
30
|
|
|
31
|
-
def emit_bindings_from_match(
|
|
31
|
+
def emit_bindings_from_match(binding_names, bindings_var, env)
|
|
32
32
|
current_env = env
|
|
33
33
|
lines = []
|
|
34
|
-
|
|
34
|
+
binding_names.each do |name|
|
|
35
35
|
ruby_name = temp(sanitize_local(name))
|
|
36
36
|
current_env.define(name, ruby_name)
|
|
37
37
|
lines << "#{ruby_name} = #{bindings_var}.fetch(#{name.inspect})"
|
|
@@ -39,6 +39,118 @@ module Kapusta
|
|
|
39
39
|
[lines.join("\n"), current_env]
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
+
def pattern_match_plan(pattern, env, mode:, allow_pins:)
|
|
43
|
+
state = { bound_names: {}, binding_names: [] }
|
|
44
|
+
{
|
|
45
|
+
pattern: emit_match_pattern(pattern, env, mode:, allow_pins:, state:),
|
|
46
|
+
bindings: state[:binding_names]
|
|
47
|
+
}
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def emit_match_pattern(pattern, env, mode:, allow_pins:, state:)
|
|
51
|
+
case pattern
|
|
52
|
+
when Sym
|
|
53
|
+
emit_symbol_match_pattern(pattern, env, mode:, state:)
|
|
54
|
+
when Vec
|
|
55
|
+
emit_sequence_match_pattern(pattern.items, env, mode:, allow_pins:, state:)
|
|
56
|
+
when HashLit
|
|
57
|
+
pairs = pattern.pairs.map do |key, value|
|
|
58
|
+
"[#{key.inspect}, #{emit_match_pattern(value, env, mode:, allow_pins:, state:)}]"
|
|
59
|
+
end
|
|
60
|
+
"[:hash, [#{pairs.join(', ')}]]"
|
|
61
|
+
when List
|
|
62
|
+
if pin_pattern?(pattern)
|
|
63
|
+
emit_pin_match_pattern(pattern, env, mode:, allow_pins:)
|
|
64
|
+
elsif or_pattern?(pattern)
|
|
65
|
+
emit_or_match_pattern(pattern, env, mode:, allow_pins:, state:)
|
|
66
|
+
elsif where_pattern?(pattern)
|
|
67
|
+
raise Error, '`where` is only valid as a case/match clause head'
|
|
68
|
+
else
|
|
69
|
+
emit_sequence_match_pattern(pattern.items, env, mode:, allow_pins:, state:)
|
|
70
|
+
end
|
|
71
|
+
when nil
|
|
72
|
+
'[:lit, nil]'
|
|
73
|
+
when Symbol, String, Numeric, true, false
|
|
74
|
+
"[:lit, #{pattern.inspect}]"
|
|
75
|
+
else
|
|
76
|
+
raise Error, "bad pattern: #{pattern.inspect}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def emit_symbol_match_pattern(pattern, env, mode:, state:)
|
|
81
|
+
name = pattern.name
|
|
82
|
+
|
|
83
|
+
if name == '_'
|
|
84
|
+
'[:wild]'
|
|
85
|
+
elsif nil_allowing_pattern_name?(name)
|
|
86
|
+
bind_name = name.start_with?('?') ? name.delete_prefix('?') : name
|
|
87
|
+
emit_named_match_pattern(bind_name, env, mode:, state:, allow_nil: true, prefer_pin: false)
|
|
88
|
+
else
|
|
89
|
+
emit_named_match_pattern(name, env, mode:, state:, allow_nil: false, prefer_pin: true)
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def emit_named_match_pattern(name, env, mode:, state:, allow_nil:, prefer_pin:)
|
|
94
|
+
if state[:bound_names].key?(name)
|
|
95
|
+
"[:ref, #{name.inspect}]"
|
|
96
|
+
elsif prefer_pin && mode == :match && env.defined?(name)
|
|
97
|
+
"[:pin, #{env.lookup(name)}]"
|
|
98
|
+
else
|
|
99
|
+
state[:bound_names][name] = true
|
|
100
|
+
state[:binding_names] << name
|
|
101
|
+
"[:bind, #{name.inspect}, #{allow_nil}]"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def emit_sequence_match_pattern(items, env, mode:, allow_pins:, state:)
|
|
106
|
+
parts = []
|
|
107
|
+
i = 0
|
|
108
|
+
while i < items.length
|
|
109
|
+
if rest_pattern_marker?(items, i)
|
|
110
|
+
parts << "[:rest, #{emit_match_pattern(items[i + 1], env, mode:, allow_pins:, state:)}]"
|
|
111
|
+
i += 2
|
|
112
|
+
else
|
|
113
|
+
parts << emit_match_pattern(items[i], env, mode:, allow_pins:, state:)
|
|
114
|
+
i += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
"[:vec, [#{parts.join(', ')}]]"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def emit_pin_match_pattern(pattern, env, mode:, allow_pins:)
|
|
121
|
+
raise Error, 'pin patterns are only supported inside `case` guards' unless allow_pins && mode == :case
|
|
122
|
+
|
|
123
|
+
name_sym = pattern.items[1]
|
|
124
|
+
raise Error, "bad pin pattern: #{pattern.inspect}" unless name_sym.is_a?(Sym)
|
|
125
|
+
raise Error, "cannot pin undefined name: #{name_sym.name}" unless env.defined?(name_sym.name)
|
|
126
|
+
|
|
127
|
+
"[:pin, #{env.lookup(name_sym.name)}]"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def emit_or_match_pattern(pattern, env, mode:, allow_pins:, state:)
|
|
131
|
+
initial_names = state[:binding_names].length
|
|
132
|
+
initial_bound = state[:bound_names].dup
|
|
133
|
+
canonical_names = nil
|
|
134
|
+
variants = pattern.items[1..].map do |subpattern|
|
|
135
|
+
alt_state = {
|
|
136
|
+
bound_names: initial_bound.dup,
|
|
137
|
+
binding_names: state[:binding_names].dup
|
|
138
|
+
}
|
|
139
|
+
compiled = emit_match_pattern(subpattern, env, mode:, allow_pins:, state: alt_state)
|
|
140
|
+
alt_names = alt_state[:binding_names][initial_names..]
|
|
141
|
+
canonical_names ||= alt_names
|
|
142
|
+
raise Error, 'all `or` patterns must bind the same names' if canonical_names.sort != alt_names.sort
|
|
143
|
+
|
|
144
|
+
compiled
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
canonical_names.each do |name|
|
|
148
|
+
state[:bound_names][name] = true
|
|
149
|
+
state[:binding_names] << name
|
|
150
|
+
end
|
|
151
|
+
"[:or, [#{variants.join(', ')}]]"
|
|
152
|
+
end
|
|
153
|
+
|
|
42
154
|
def emit_pattern(pattern)
|
|
43
155
|
case pattern
|
|
44
156
|
when Sym
|
|
@@ -99,6 +211,25 @@ module Kapusta
|
|
|
99
211
|
def where_pattern?(pattern)
|
|
100
212
|
pattern.is_a?(List) && pattern.head.is_a?(Sym) && pattern.head.name == 'where'
|
|
101
213
|
end
|
|
214
|
+
|
|
215
|
+
def pin_pattern?(pattern)
|
|
216
|
+
pattern.is_a?(List) &&
|
|
217
|
+
pattern.items.length == 2 &&
|
|
218
|
+
pattern.head.is_a?(Sym) &&
|
|
219
|
+
pattern.head.name == '='
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def or_pattern?(pattern)
|
|
223
|
+
pattern.is_a?(List) && pattern.head.is_a?(Sym) && pattern.head.name == 'or'
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def nil_allowing_pattern_name?(name)
|
|
227
|
+
name.length > 1 && (name.start_with?('?') || name.start_with?('_'))
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
def rest_pattern_marker?(items, index)
|
|
231
|
+
items[index].is_a?(Sym) && items[index].name == '&'
|
|
232
|
+
end
|
|
102
233
|
end
|
|
103
234
|
end
|
|
104
235
|
end
|
|
@@ -156,6 +156,47 @@ module Kapusta
|
|
|
156
156
|
"#{runtime_helper(name)}(#{rendered_args.join(', ')})"
|
|
157
157
|
end
|
|
158
158
|
|
|
159
|
+
def parse_counted_for_bindings(bindings, env, current_scope)
|
|
160
|
+
name_sym = bindings[0]
|
|
161
|
+
ruby_name = temp(sanitize_local(name_sym.name))
|
|
162
|
+
loop_env = env.child
|
|
163
|
+
loop_env.define(name_sym.name, ruby_name)
|
|
164
|
+
start_code = emit_expr(bindings[1], env, current_scope)
|
|
165
|
+
finish_code = emit_expr(bindings[2], env, current_scope)
|
|
166
|
+
step_code = '1'
|
|
167
|
+
until_form = nil
|
|
168
|
+
i = 3
|
|
169
|
+
while i < bindings.length
|
|
170
|
+
if bindings[i].is_a?(Sym) && bindings[i].name == '&until'
|
|
171
|
+
until_form = bindings[i + 1]
|
|
172
|
+
i += 2
|
|
173
|
+
else
|
|
174
|
+
step_code = emit_expr(bindings[i], env, current_scope)
|
|
175
|
+
i += 1
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
{ ruby_name:, loop_env:, start_code:, finish_code:, step_code:, until_form: }
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def emit_counted_loop(ruby_name:, start_code:, finish_code:, step_code:,
|
|
182
|
+
until_form:, loop_env:, current_scope:, body_code:)
|
|
183
|
+
finish_var = temp('finish')
|
|
184
|
+
step_var = temp('step')
|
|
185
|
+
cmp_var = temp('cmp')
|
|
186
|
+
until_code = until_form ? "break if #{emit_expr(until_form, loop_env, current_scope)}" : nil
|
|
187
|
+
<<~RUBY.chomp
|
|
188
|
+
#{ruby_name} = #{start_code}
|
|
189
|
+
#{finish_var} = #{finish_code}
|
|
190
|
+
#{step_var} = #{step_code}
|
|
191
|
+
#{cmp_var} = #{step_var} >= 0 ? :<= : :>=
|
|
192
|
+
while #{ruby_name}.public_send(#{cmp_var}, #{finish_var})
|
|
193
|
+
#{until_code}
|
|
194
|
+
#{indent(body_code)}
|
|
195
|
+
#{ruby_name} += #{step_var}
|
|
196
|
+
end
|
|
197
|
+
RUBY
|
|
198
|
+
end
|
|
199
|
+
|
|
159
200
|
def sanitize_local(name)
|
|
160
201
|
base = Kapusta.kebab_to_snake(name)
|
|
161
202
|
base = base.gsub('?', '_q').gsub('!', '_bang')
|
|
@@ -270,9 +270,16 @@ module Kapusta
|
|
|
270
270
|
match_pattern_into: <<~'RUBY'.chomp
|
|
271
271
|
def __kap_match_pattern_into(pattern, value, bindings)
|
|
272
272
|
case pattern[0]
|
|
273
|
-
when :
|
|
273
|
+
when :bind
|
|
274
274
|
name = pattern[1]
|
|
275
|
-
|
|
275
|
+
allow_nil = pattern[2]
|
|
276
|
+
return false if value.nil? && !allow_nil
|
|
277
|
+
|
|
278
|
+
bindings[name] = value
|
|
279
|
+
true
|
|
280
|
+
when :ref
|
|
281
|
+
bindings.key?(pattern[1]) && bindings[pattern[1]] == value
|
|
282
|
+
when :wild
|
|
276
283
|
true
|
|
277
284
|
when :vec
|
|
278
285
|
return false unless value.is_a?(Array) || value.respond_to?(:to_ary)
|
|
@@ -290,7 +297,7 @@ module Kapusta
|
|
|
290
297
|
end
|
|
291
298
|
__kap_match_pattern_into(rest_pattern, array[rest_idx..], bindings)
|
|
292
299
|
else
|
|
293
|
-
return false unless array.length
|
|
300
|
+
return false unless array.length >= items.length
|
|
294
301
|
|
|
295
302
|
items.each_with_index do |item, i|
|
|
296
303
|
return false unless __kap_match_pattern_into(item, array[i], bindings)
|
|
@@ -307,8 +314,16 @@ module Kapusta
|
|
|
307
314
|
true
|
|
308
315
|
when :lit
|
|
309
316
|
value == pattern[1]
|
|
310
|
-
when :
|
|
311
|
-
value
|
|
317
|
+
when :pin
|
|
318
|
+
value == pattern[1]
|
|
319
|
+
when :or
|
|
320
|
+
pattern[1].any? do |option|
|
|
321
|
+
option_bindings = bindings.dup
|
|
322
|
+
next false unless __kap_match_pattern_into(option, value, option_bindings)
|
|
323
|
+
|
|
324
|
+
bindings.replace(option_bindings)
|
|
325
|
+
true
|
|
326
|
+
end
|
|
312
327
|
else
|
|
313
328
|
raise "bad pattern: #{pattern.inspect}"
|
|
314
329
|
end
|
data/lib/kapusta/formatter.rb
CHANGED
|
@@ -519,11 +519,12 @@ module Kapusta
|
|
|
519
519
|
def render_pairwise_vec(vec, indent)
|
|
520
520
|
lines = ['[']
|
|
521
521
|
|
|
522
|
-
vec.items.each_slice(2) do |
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
522
|
+
vec.items.each_slice(2) do |pair|
|
|
523
|
+
left, right = pair
|
|
524
|
+
if pair.length == 2
|
|
525
|
+
rendered_pair = render_pair(left, right, indent + INDENT)
|
|
526
|
+
if rendered_pair
|
|
527
|
+
lines << indent_block(rendered_pair, INDENT)
|
|
527
528
|
else
|
|
528
529
|
lines << indent_block(render(left, indent + INDENT), INDENT)
|
|
529
530
|
lines << indent_block(render(right, indent + INDENT), INDENT)
|
|
@@ -547,8 +548,9 @@ module Kapusta
|
|
|
547
548
|
|
|
548
549
|
def render_hanging_pairwise_vec(vec)
|
|
549
550
|
pairs = vec.items.each_slice(2).to_a
|
|
550
|
-
rendered_pairs = pairs.map do |
|
|
551
|
-
|
|
551
|
+
rendered_pairs = pairs.map do |pair|
|
|
552
|
+
left, right = pair
|
|
553
|
+
return nil unless pair.length == 2
|
|
552
554
|
|
|
553
555
|
render_binding_pair(left, right)
|
|
554
556
|
end
|
data/lib/kapusta/version.rb
CHANGED
data/spec/examples_spec.rb
CHANGED
|
@@ -31,6 +31,10 @@ RSpec.describe 'examples' do
|
|
|
31
31
|
expect(run_example('anagram.kap')).to eq("true\ntrue\nfalse\n")
|
|
32
32
|
end
|
|
33
33
|
|
|
34
|
+
it 'anonymous-greeter.kap' do
|
|
35
|
+
expect(run_example('anonymous-greeter.kap')).to eq("Hello, anonymous!\nHello, Ada!\n")
|
|
36
|
+
end
|
|
37
|
+
|
|
34
38
|
it 'calc.kap' do
|
|
35
39
|
expect(run_example('calc.kap')).to eq("14\n")
|
|
36
40
|
end
|
|
@@ -39,6 +43,10 @@ RSpec.describe 'examples' do
|
|
|
39
43
|
expect(run_example('binary-search.kap')).to eq("3\nnil\n")
|
|
40
44
|
end
|
|
41
45
|
|
|
46
|
+
it 'binary-to-decimal.kap' do
|
|
47
|
+
expect(run_example('binary-to-decimal.kap')).to eq("11\n0\n42\n")
|
|
48
|
+
end
|
|
49
|
+
|
|
42
50
|
it 'blocks-and-kwargs.kap' do
|
|
43
51
|
path = File.expand_path('../tmp/blocks-and-kwargs.txt', EXAMPLES_DIR)
|
|
44
52
|
FileUtils.rm_f(path)
|
|
@@ -169,6 +177,18 @@ RSpec.describe 'examples' do
|
|
|
169
177
|
expect(run_example('match.kap')).to eq("Ada: 9\nLin: no score\nunknown\n")
|
|
170
178
|
end
|
|
171
179
|
|
|
180
|
+
it 'packet-router.kap' do
|
|
181
|
+
expect(run_example('packet-router.kap')).to eq("score:9\nother\ncity:nil\n5\n0\nping:7\n")
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
it 'or-patterns.kap' do
|
|
185
|
+
expect(run_example('or-patterns.kap')).to eq("1:2\n2:1\nother\n")
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
it 'underscore-patterns.kap' do
|
|
189
|
+
expect(run_example('underscore-patterns.kap')).to eq("5\nnil\n5\nfallback\n")
|
|
190
|
+
end
|
|
191
|
+
|
|
172
192
|
it 'scopes.kap' do
|
|
173
193
|
expect(run_example('scopes.kap')).to eq("5\n9\n9\n9\n")
|
|
174
194
|
end
|
|
@@ -224,6 +244,10 @@ RSpec.describe 'examples' do
|
|
|
224
244
|
it 'threading.kap' do
|
|
225
245
|
expect(run_example('threading.kap')).to eq("[Ada Lovelace]!\t<Ada!>\tnil\tATSUPAK\tnil\n")
|
|
226
246
|
end
|
|
247
|
+
|
|
248
|
+
it 'tic-tac-toe.kap' do
|
|
249
|
+
expect(run_example('tic-tac-toe.kap')).to eq("X\nO\nX\ndraw\n")
|
|
250
|
+
end
|
|
227
251
|
end
|
|
228
252
|
|
|
229
253
|
RSpec.describe Kapusta do
|
data/spec/formatter_spec.rb
CHANGED
|
@@ -272,6 +272,26 @@ RSpec.describe Kapusta::Formatter do
|
|
|
272
272
|
end
|
|
273
273
|
end
|
|
274
274
|
|
|
275
|
+
it 'preserves nil-valued let bindings before function bindings' do
|
|
276
|
+
Dir.mktmpdir do |dir|
|
|
277
|
+
path = File.join(dir, 'sample.kap')
|
|
278
|
+
File.write(path, <<~KAP)
|
|
279
|
+
(let [name nil get-input (fn [] "Dave")]
|
|
280
|
+
(print (get-input)))
|
|
281
|
+
KAP
|
|
282
|
+
|
|
283
|
+
output = capture_stdout do
|
|
284
|
+
expect(described_class.new([path]).run).to eq(0)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
expect(output).to eq(<<~KAP)
|
|
288
|
+
(let [name nil
|
|
289
|
+
get-input (fn [] "Dave")]
|
|
290
|
+
(print (get-input)))
|
|
291
|
+
KAP
|
|
292
|
+
end
|
|
293
|
+
end
|
|
294
|
+
|
|
275
295
|
example_idempotence_paths.each do |relative_path|
|
|
276
296
|
it "keeps #{relative_path} unchanged" do
|
|
277
297
|
path = File.expand_path("../#{relative_path}", __dir__)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: kapusta
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Evgenii Morozov
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-04-
|
|
11
|
+
date: 2026-04-24 00:00:00.000000000 Z
|
|
12
12
|
dependencies: []
|
|
13
13
|
description: Kapusta is a Lisp for the Ruby runtime.
|
|
14
14
|
email:
|
|
@@ -27,7 +27,9 @@ files:
|
|
|
27
27
|
- examples/accumulator.kap
|
|
28
28
|
- examples/ackermann.kap
|
|
29
29
|
- examples/anagram.kap
|
|
30
|
+
- examples/anonymous-greeter.kap
|
|
30
31
|
- examples/binary-search.kap
|
|
32
|
+
- examples/binary-to-decimal.kap
|
|
31
33
|
- examples/block-sort.kap
|
|
32
34
|
- examples/blocks-and-kwargs.kap
|
|
33
35
|
- examples/calc.kap
|
|
@@ -51,6 +53,8 @@ files:
|
|
|
51
53
|
- examples/match.kap
|
|
52
54
|
- examples/min-max.kap
|
|
53
55
|
- examples/module-header.kap
|
|
56
|
+
- examples/or-patterns.kap
|
|
57
|
+
- examples/packet-router.kap
|
|
54
58
|
- examples/palindrome.kap
|
|
55
59
|
- examples/pangram.kap
|
|
56
60
|
- examples/pcall.kap
|
|
@@ -68,11 +72,12 @@ files:
|
|
|
68
72
|
- examples/stack.kap
|
|
69
73
|
- examples/sum.kap
|
|
70
74
|
- examples/threading.kap
|
|
75
|
+
- examples/tic-tac-toe.kap
|
|
71
76
|
- examples/tset.kap
|
|
72
77
|
- examples/two-sum.kap
|
|
78
|
+
- examples/underscore-patterns.kap
|
|
73
79
|
- exe/kapfmt
|
|
74
80
|
- exe/kapusta
|
|
75
|
-
- kapfmt
|
|
76
81
|
- kapusta.gemspec
|
|
77
82
|
- lib/kapusta.rb
|
|
78
83
|
- lib/kapusta/ast.rb
|
data/kapfmt
DELETED