kapusta 0.1.1 → 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/README.md +9 -1
- 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 +46 -252
- 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
|
data/README.md
CHANGED
|
@@ -10,6 +10,14 @@ For more information about Kapusta, see the official Fennel documentation and tu
|
|
|
10
10
|
|
|
11
11
|
## Usage
|
|
12
12
|
|
|
13
|
+
```
|
|
14
|
+
gem install kapusta
|
|
15
|
+
kapfmt --fix examples/fizzbuzz.kap
|
|
16
|
+
kapusta examples/fizzbuzz.kap
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
or
|
|
20
|
+
|
|
13
21
|
```
|
|
14
22
|
exe/kapusta examples/fizzbuzz.kap
|
|
15
23
|
```
|
|
@@ -37,7 +45,7 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
|
|
|
37
45
|
Kapusta-specific additions:
|
|
38
46
|
|
|
39
47
|
- `module` and `class` for Ruby host structure, including file-header forms
|
|
40
|
-
- `ivar` / `cvar` / `gvar` escape hatches
|
|
48
|
+
- `ivar` (`@var`) / `cvar` (`@@var`) / `gvar` (`$var`) escape hatches
|
|
41
49
|
- `try` / `catch` / `finally` plus `raise` for exceptions
|
|
42
50
|
- `(ruby "...")` raw host escape hatch
|
|
43
51
|
- a trailing symbol-keyed hash is emitted as Ruby keyword arguments
|
|
@@ -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')
|
|
@@ -63,12 +63,29 @@ module Kapusta
|
|
|
63
63
|
end
|
|
64
64
|
end
|
|
65
65
|
RUBY
|
|
66
|
-
stringify: <<~RUBY.chomp,
|
|
66
|
+
stringify: <<~'RUBY'.chomp,
|
|
67
67
|
def __kap_stringify(value)
|
|
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
|
+
|
|
68
84
|
case value
|
|
69
85
|
when nil then 'nil'
|
|
70
86
|
when true then 'true'
|
|
71
87
|
when false then 'false'
|
|
88
|
+
when Array, Hash then render.call(value)
|
|
72
89
|
else value.to_s
|
|
73
90
|
end
|
|
74
91
|
end
|
|
@@ -253,9 +270,16 @@ module Kapusta
|
|
|
253
270
|
match_pattern_into: <<~'RUBY'.chomp
|
|
254
271
|
def __kap_match_pattern_into(pattern, value, bindings)
|
|
255
272
|
case pattern[0]
|
|
256
|
-
when :
|
|
273
|
+
when :bind
|
|
257
274
|
name = pattern[1]
|
|
258
|
-
|
|
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
|
|
259
283
|
true
|
|
260
284
|
when :vec
|
|
261
285
|
return false unless value.is_a?(Array) || value.respond_to?(:to_ary)
|
|
@@ -273,7 +297,7 @@ module Kapusta
|
|
|
273
297
|
end
|
|
274
298
|
__kap_match_pattern_into(rest_pattern, array[rest_idx..], bindings)
|
|
275
299
|
else
|
|
276
|
-
return false unless array.length
|
|
300
|
+
return false unless array.length >= items.length
|
|
277
301
|
|
|
278
302
|
items.each_with_index do |item, i|
|
|
279
303
|
return false unless __kap_match_pattern_into(item, array[i], bindings)
|
|
@@ -290,8 +314,16 @@ module Kapusta
|
|
|
290
314
|
true
|
|
291
315
|
when :lit
|
|
292
316
|
value == pattern[1]
|
|
293
|
-
when :
|
|
294
|
-
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
|
|
295
327
|
else
|
|
296
328
|
raise "bad pattern: #{pattern.inspect}"
|
|
297
329
|
end
|
|
@@ -327,257 +359,19 @@ module Kapusta
|
|
|
327
359
|
seen[name] = true
|
|
328
360
|
end
|
|
329
361
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if block
|
|
334
|
-
kwargs ? callee.call(*positional, **kwargs, &block) : callee.call(*positional, &block)
|
|
335
|
-
else
|
|
336
|
-
kwargs ? callee.call(*positional, **kwargs) : callee.call(*positional)
|
|
337
|
-
end
|
|
338
|
-
end
|
|
339
|
-
|
|
340
|
-
def send_call(receiver, method_name, positional, kwargs = nil, block = nil)
|
|
341
|
-
if block
|
|
342
|
-
if kwargs
|
|
343
|
-
receiver.public_send(method_name, *positional, **kwargs,
|
|
344
|
-
&block)
|
|
345
|
-
else
|
|
346
|
-
receiver.public_send(method_name, *positional, &block)
|
|
347
|
-
end
|
|
348
|
-
elsif kwargs
|
|
349
|
-
receiver.public_send(method_name, *positional,
|
|
350
|
-
**kwargs)
|
|
351
|
-
else
|
|
352
|
-
receiver.public_send(method_name, *positional)
|
|
353
|
-
end
|
|
354
|
-
end
|
|
355
|
-
|
|
356
|
-
def invoke_self(receiver, method_name, positional, kwargs = nil, block = nil)
|
|
357
|
-
if block
|
|
358
|
-
if kwargs
|
|
359
|
-
receiver.send(method_name, *positional, **kwargs,
|
|
360
|
-
&block)
|
|
361
|
-
else
|
|
362
|
-
receiver.send(method_name, *positional, &block)
|
|
363
|
-
end
|
|
364
|
-
else
|
|
365
|
-
kwargs ? receiver.send(method_name, *positional, **kwargs) : receiver.send(method_name, *positional)
|
|
366
|
-
end
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
def stringify(value)
|
|
370
|
-
case value
|
|
371
|
-
when nil then 'nil'
|
|
372
|
-
when true then 'true'
|
|
373
|
-
when false then 'false'
|
|
374
|
-
else value.to_s
|
|
375
|
-
end
|
|
376
|
-
end
|
|
377
|
-
|
|
378
|
-
def print_values(*values)
|
|
379
|
-
$stdout.puts(values.map { |value| stringify(value) }.join("\t"))
|
|
380
|
-
nil
|
|
381
|
-
end
|
|
382
|
-
|
|
383
|
-
def concat(values)
|
|
384
|
-
values.map { |value| stringify(value) }.join
|
|
385
|
-
end
|
|
386
|
-
|
|
387
|
-
def get_path(obj, keys)
|
|
388
|
-
keys.reduce(obj) { |acc, key| acc[key] }
|
|
389
|
-
end
|
|
390
|
-
|
|
391
|
-
def qget_path(obj, keys)
|
|
392
|
-
keys.each do |key|
|
|
393
|
-
return nil if obj.nil?
|
|
394
|
-
|
|
395
|
-
obj = obj[key]
|
|
396
|
-
end
|
|
397
|
-
obj
|
|
398
|
-
end
|
|
399
|
-
|
|
400
|
-
def set_path(obj, keys, value)
|
|
401
|
-
target = obj
|
|
402
|
-
keys[0...-1].each { |key| target = target[key] }
|
|
403
|
-
target[keys.last] = value
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
def method_path_value(base, segments)
|
|
407
|
-
segments.reduce(base) { |obj, segment| obj.public_send(Kapusta.kebab_to_snake(segment).to_sym) }
|
|
408
|
-
end
|
|
409
|
-
|
|
410
|
-
def set_method_path(base, segments, value)
|
|
411
|
-
target = base
|
|
412
|
-
segments[0...-1].each do |segment|
|
|
413
|
-
target = target.public_send(Kapusta.kebab_to_snake(segment).to_sym)
|
|
414
|
-
end
|
|
415
|
-
setter = "#{Kapusta.kebab_to_snake(segments.last)}="
|
|
416
|
-
target.public_send(setter.to_sym, value)
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def current_class_scope(receiver)
|
|
420
|
-
receiver.is_a?(Module) ? receiver : receiver.class
|
|
421
|
-
end
|
|
422
|
-
|
|
423
|
-
def get_ivar(receiver, name)
|
|
424
|
-
receiver.instance_variable_get("@#{Kapusta.kebab_to_snake(name)}")
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
def set_ivar(receiver, name, value)
|
|
428
|
-
receiver.instance_variable_set("@#{Kapusta.kebab_to_snake(name)}", value)
|
|
429
|
-
end
|
|
430
|
-
|
|
431
|
-
def get_cvar(receiver, name)
|
|
432
|
-
current_class_scope(receiver).class_variable_get("@@#{Kapusta.kebab_to_snake(name)}")
|
|
433
|
-
end
|
|
434
|
-
|
|
435
|
-
def set_cvar(receiver, name, value)
|
|
436
|
-
current_class_scope(receiver).class_variable_set("@@#{Kapusta.kebab_to_snake(name)}", value)
|
|
362
|
+
HELPER_SOURCES.each_value do |source|
|
|
363
|
+
module_eval(source, __FILE__, __LINE__)
|
|
437
364
|
end
|
|
438
365
|
|
|
439
|
-
|
|
440
|
-
Kernel.eval("$#{Kapusta.kebab_to_snake(name)}", binding, __FILE__, __LINE__) # $stderr
|
|
441
|
-
end
|
|
366
|
+
helper_methods = []
|
|
442
367
|
|
|
443
|
-
|
|
444
|
-
|
|
368
|
+
HELPER_SOURCES.each_key do |name|
|
|
369
|
+
helper_method = :"__kap_#{name}"
|
|
370
|
+
define_singleton_method(name, instance_method(helper_method))
|
|
371
|
+
helper_methods << helper_method
|
|
445
372
|
end
|
|
446
373
|
|
|
447
|
-
|
|
448
|
-
segments = path.split('.')
|
|
449
|
-
last = segments.pop
|
|
450
|
-
scope = holder.is_a?(Module) ? holder : Object
|
|
451
|
-
segments.each do |segment|
|
|
452
|
-
scope =
|
|
453
|
-
if scope.const_defined?(segment, false)
|
|
454
|
-
scope.const_get(segment, false)
|
|
455
|
-
else
|
|
456
|
-
mod = Module.new
|
|
457
|
-
scope.const_set(segment, mod)
|
|
458
|
-
mod
|
|
459
|
-
end
|
|
460
|
-
end
|
|
461
|
-
if scope.const_defined?(last, false)
|
|
462
|
-
scope.const_get(last, false)
|
|
463
|
-
else
|
|
464
|
-
mod = Module.new
|
|
465
|
-
scope.const_set(last, mod)
|
|
466
|
-
mod
|
|
467
|
-
end
|
|
468
|
-
end
|
|
469
|
-
|
|
470
|
-
def ensure_class(holder, path, super_class)
|
|
471
|
-
segments = path.split('.')
|
|
472
|
-
last = segments.pop
|
|
473
|
-
scope = holder.is_a?(Module) ? holder : Object
|
|
474
|
-
segments.each do |segment|
|
|
475
|
-
scope =
|
|
476
|
-
if scope.const_defined?(segment, false)
|
|
477
|
-
scope.const_get(segment, false)
|
|
478
|
-
else
|
|
479
|
-
mod = Module.new
|
|
480
|
-
scope.const_set(segment, mod)
|
|
481
|
-
mod
|
|
482
|
-
end
|
|
483
|
-
end
|
|
484
|
-
if scope.const_defined?(last, false)
|
|
485
|
-
scope.const_get(last, false)
|
|
486
|
-
else
|
|
487
|
-
klass = Class.new(super_class)
|
|
488
|
-
scope.const_set(last, klass)
|
|
489
|
-
klass
|
|
490
|
-
end
|
|
491
|
-
end
|
|
492
|
-
|
|
493
|
-
def destructure(pattern, value)
|
|
494
|
-
bindings = {}
|
|
495
|
-
destructure_into(pattern, value, bindings)
|
|
496
|
-
bindings
|
|
497
|
-
end
|
|
498
|
-
|
|
499
|
-
def destructure_into(pattern, value, bindings)
|
|
500
|
-
case pattern[0]
|
|
501
|
-
when :sym
|
|
502
|
-
name = pattern[1]
|
|
503
|
-
bindings[name] = value unless name == '_'
|
|
504
|
-
when :vec
|
|
505
|
-
items = pattern[1]
|
|
506
|
-
rest_idx = items.index { |item| item.is_a?(Array) && item[0] == :rest }
|
|
507
|
-
if rest_idx
|
|
508
|
-
before = items[0...rest_idx]
|
|
509
|
-
rest_pattern = items[rest_idx][1]
|
|
510
|
-
before.each_with_index do |item, i|
|
|
511
|
-
destructure_into(item, value ? value[i] : nil, bindings)
|
|
512
|
-
end
|
|
513
|
-
rest_value = value ? (value[rest_idx..] || []) : []
|
|
514
|
-
destructure_into(rest_pattern, rest_value, bindings)
|
|
515
|
-
else
|
|
516
|
-
items.each_with_index do |item, i|
|
|
517
|
-
destructure_into(item, value ? value[i] : nil, bindings)
|
|
518
|
-
end
|
|
519
|
-
end
|
|
520
|
-
when :hash
|
|
521
|
-
pattern[1].each do |key, subpattern|
|
|
522
|
-
destructure_into(subpattern, value ? value[key] : nil, bindings)
|
|
523
|
-
end
|
|
524
|
-
when :ignore
|
|
525
|
-
nil
|
|
526
|
-
else
|
|
527
|
-
raise "unknown destructure pattern: #{pattern.inspect}"
|
|
528
|
-
end
|
|
529
|
-
end
|
|
530
|
-
|
|
531
|
-
def match_pattern(pattern, value)
|
|
532
|
-
bindings = {}
|
|
533
|
-
[match_pattern_into(pattern, value, bindings), bindings]
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
def match_pattern_into(pattern, value, bindings)
|
|
537
|
-
case pattern[0]
|
|
538
|
-
when :sym
|
|
539
|
-
name = pattern[1]
|
|
540
|
-
bindings[name] = value unless name == '_'
|
|
541
|
-
true
|
|
542
|
-
when :vec
|
|
543
|
-
return false unless value.is_a?(Array) || value.respond_to?(:to_ary)
|
|
544
|
-
|
|
545
|
-
array = value.is_a?(Array) ? value : value.to_ary
|
|
546
|
-
items = pattern[1]
|
|
547
|
-
rest_idx = items.index { |item| item.is_a?(Array) && item[0] == :rest }
|
|
548
|
-
if rest_idx
|
|
549
|
-
before = items[0...rest_idx]
|
|
550
|
-
rest_pattern = items[rest_idx][1]
|
|
551
|
-
return false if array.length < before.length
|
|
552
|
-
|
|
553
|
-
before.each_with_index do |item, i|
|
|
554
|
-
return false unless match_pattern_into(item, array[i], bindings)
|
|
555
|
-
end
|
|
556
|
-
match_pattern_into(rest_pattern, array[rest_idx..], bindings)
|
|
557
|
-
else
|
|
558
|
-
return false unless array.length == items.length
|
|
559
|
-
|
|
560
|
-
items.each_with_index do |item, i|
|
|
561
|
-
return false unless match_pattern_into(item, array[i], bindings)
|
|
562
|
-
end
|
|
563
|
-
true
|
|
564
|
-
end
|
|
565
|
-
when :hash
|
|
566
|
-
return false unless value.is_a?(Hash)
|
|
567
|
-
|
|
568
|
-
pattern[1].each do |key, subpattern|
|
|
569
|
-
return false unless value.key?(key)
|
|
570
|
-
return false unless match_pattern_into(subpattern, value[key], bindings)
|
|
571
|
-
end
|
|
572
|
-
true
|
|
573
|
-
when :lit
|
|
574
|
-
value == pattern[1]
|
|
575
|
-
when :nil
|
|
576
|
-
value.nil?
|
|
577
|
-
else
|
|
578
|
-
raise "bad pattern: #{pattern.inspect}"
|
|
579
|
-
end
|
|
580
|
-
end
|
|
374
|
+
send(:private, *helper_methods)
|
|
581
375
|
end
|
|
582
376
|
end
|
|
583
377
|
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