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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 25c55d7f1c7a4b1dcd3cca0e7a513a74591905e9dfc97e96f015b695d75905a2
4
- data.tar.gz: '0940fc49053103149667dd8bf515e159d87a10182bb3fbd5eb152ba63f487e06'
3
+ metadata.gz: 2398c1c908306ac808914717a67e74f8085ee6048deb9c9dd665f4dd8c39cc2f
4
+ data.tar.gz: 1022c2df415418d26f1b84b1b16830cc525829182a82e93c1a450d0dd4b5bdd2
5
5
  SHA512:
6
- metadata.gz: a3a8cb442fcb6d5157dcf3885e9d6a7323735418e488715e35baea0006b098e8822b8252013eea2636f96a35665db8173556507af293188f632108827f8119d5
7
- data.tar.gz: d0f19cbc23c569524cb64c0b70b14d4ea5ed149291acad18528611a7b12c2ff1b4d9c27ce1a239ec07e9c19bb36c5907f294fbca4b0b783bfc2991c880b93fc7
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,4 @@
1
+ (let [name nil
2
+ greet (fn [value] (if value (.. "Hello, " value "!") "Hello, anonymous!"))]
3
+ (print (greet name))
4
+ (print (greet "Ada")))
@@ -0,0 +1,7 @@
1
+ (fn binary-to-decimal [bits]
2
+ (faccumulate [value 0 i 0 (- (length bits) 1)]
3
+ (+ (* value 2) (if (= (. bits i) "1") 1 0))))
4
+
5
+ (print (binary-to-decimal "1011"))
6
+ (print (binary-to-decimal "0"))
7
+ (print (binary-to-decimal "101010"))
@@ -0,0 +1,8 @@
1
+ (fn swap-kind [v]
2
+ (case v
3
+ (where (or [:pair x y] [:flipped y x])) (.. x ":" y)
4
+ _ "other"))
5
+
6
+ (print (swap-kind [:pair 1 2]))
7
+ (print (swap-kind [:flipped 1 2]))
8
+ (print (swap-kind [:nope 1 2]))
@@ -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)))
@@ -0,0 +1,14 @@
1
+ (fn loose [v]
2
+ (case v
3
+ _x _x
4
+ _ "fallback"))
5
+
6
+ (fn strict [v]
7
+ (case v
8
+ x x
9
+ _ "fallback"))
10
+
11
+ (print (loose 5))
12
+ (print (loose nil))
13
+ (print (strict 5))
14
+ (print (strict nil))
@@ -50,44 +50,19 @@ module Kapusta
50
50
 
51
51
  def emit_fcollect(args, env, current_scope)
52
52
  result_var = temp('result')
53
- bindings = args[0].items
54
- ruby_name = temp(sanitize_local(bindings[0].name))
55
- loop_env = env.child
56
- loop_env.define(bindings[0].name, ruby_name)
57
- start_code = emit_expr(bindings[1], env, current_scope)
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
- end
71
- body_code, = emit_sequence(args[1..], loop_env, current_scope, allow_method_definitions: false)
72
- until_code = until_form ? "break if #{emit_expr(until_form, loop_env, current_scope)}" : nil
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
- #{ruby_name} = #{start_code}
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
- finish_var = temp('finish')
132
- step_var = temp('step')
133
- cmp_var = temp('cmp')
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
- #{loop_var} = #{start_code}
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(pattern, bindings_var, arm_env)
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, emit_pattern(pattern), value_var)}
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
- guard = pattern.items[2]
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(inner, bindings_var, arm_env)
82
- guard_code = emit_expr(guard, arm_env, current_scope)
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, emit_pattern(inner), value_var)}
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
- bindings = args[0].items
114
- name = bindings[0]
115
- start_code = emit_expr(bindings[1], env, current_scope)
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
- #{ruby_name} = #{start_code}
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', 'match' then emit_case(args, env, current_scope)
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(pattern, bindings_var, env)
31
+ def emit_bindings_from_match(binding_names, bindings_var, env)
32
32
  current_env = env
33
33
  lines = []
34
- pattern_names(pattern).each do |name|
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 :sym
273
+ when :bind
257
274
  name = pattern[1]
258
- bindings[name] = value unless name == '_'
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 == items.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 :nil
294
- value.nil?
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
- def call(callee, positional, kwargs = nil, block = nil)
331
- raise "not callable: #{callee.inspect}" unless callee.respond_to?(:call)
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
- def get_gvar(name)
440
- Kernel.eval("$#{Kapusta.kebab_to_snake(name)}", binding, __FILE__, __LINE__) # $stderr
441
- end
366
+ helper_methods = []
442
367
 
443
- def set_gvar(name, value)
444
- Kernel.eval("$#{Kapusta.kebab_to_snake(name)} = value", binding, __FILE__, __LINE__) # $stderr = value
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
- def ensure_module(holder, path)
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
@@ -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 |left, right|
523
- if right
524
- pair = render_pair(left, right, indent + INDENT)
525
- if pair
526
- lines << indent_block(pair, INDENT)
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 |left, right|
551
- return nil unless right
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.1.1'
4
+ VERSION = '0.1.3'
5
5
  end
@@ -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
@@ -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.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-23 00:00:00.000000000 Z
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
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- load File.expand_path('exe/kapfmt', __dir__)