kapusta 0.9.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 520158590f1244ac17f70b58ded945f66be00060f6da89cebb366fefaf27b62e
4
- data.tar.gz: 6773e0faa320f23cb15106e710f380a39093c9fc6e56cead8941c28e0ccf16c5
3
+ metadata.gz: 3cafef3668504ca08a40ba087c4c2b754bfda02e6f61161f19bcf8d56a254e00
4
+ data.tar.gz: 501b99f6ec3bfa950865b63b26ec6fd7f5b64977bb86f41d4eff51957602cc84
5
5
  SHA512:
6
- metadata.gz: 814d37c473287d2d1e54568c7d21c22d88703cd89da761100c1f20950e47cb18eb2ac4807aa596878ded72eb7049a6083d2b9de46bc76ce2ea55787f6993f61f
7
- data.tar.gz: 3a15fc05522e617cf46e8709a55dc8497252503f2b995dffaa4f8995fc34a54e9bcc2a711cd501884203bf0113c7924f159377459de8f2524e9c371c018d8238
6
+ metadata.gz: b774d8f0bbd772e223743a2ec844bec4949a6f1a5a545767c3b608c8ec0c2da055fe9537c68a3cf57573ac80ed75d11d85884245b4fcbd401af16646aaab5283
7
+ data.tar.gz: 10b6d3f30fdfc760569c40c70e22f2b63dcf124ea3de3cffca2f061642523bc7057c878b81460181950356193c5786a35df28d4fd2b4859c1c38bc2285392f40
data/README.md CHANGED
@@ -4,7 +4,7 @@ Kapusta is a Lisp for the Ruby runtime.
4
4
 
5
5
  It is inspired by [Fennel](https://fennel-lang.org). It is not intended to be production-ready like Clojure: that would be a lot of work, and Ruby is already a rich, elegant language.
6
6
 
7
- Instead, Kapusta aims to bring some of the simplicity and joy of Lisp to Ruby. Where Lua is intentionally minimal, and Fennel follows that design for good reason, Kapusta exists mostly for fun. You can use it for small apps, LeetCode, DragonRuby, or maybe even Rails.
7
+ Instead, Kapusta aims to bring some of the simplicity and joy of Lisp to Ruby. Where Fennel uses Lua's stdlib and runtime, Kapusta uses Ruby's. You can use it for small apps, LeetCode, DragonRuby, or maybe even Rails.
8
8
 
9
9
  For more information about Kapusta, see the official Fennel documentation and tutorials.
10
10
 
@@ -45,6 +45,12 @@ exe/kapusta --compile examples/fizzbuzz.kap > examples/fizzbuzz.rb
45
45
  ruby examples/fizzbuzz.rb
46
46
  ```
47
47
 
48
+ For mruby-compatible output, such as DragonRuby, use:
49
+
50
+ ```
51
+ exe/kapusta --compile --target=mruby examples/match.kap > examples/match-mruby.rb
52
+ ```
53
+
48
54
  ## Use from Ruby
49
55
 
50
56
  Ruby can require a `.kap` file and use it directly.
@@ -104,7 +110,7 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
104
110
  Kapusta-specific additions:
105
111
 
106
112
  - `module` and `class` for Ruby host structure, including file-header forms
107
- - `ivar` or `@var`) / `cvar` or `@@var` / `gvar` or `$var`
113
+ - `ivar` or `@var` / `cvar` or `@@var` / `gvar` or `$var`
108
114
  - `try` / `catch` / `finally` plus `raise` for exceptions
109
115
  - `(ruby "...")` raw host escape hatch
110
116
  - a trailing symbol-keyed hash is emitted as Ruby keyword arguments
data/bin/check-all CHANGED
@@ -16,4 +16,21 @@ bin/fennel-parity
16
16
  echo "== compile examples =="
17
17
  bin/compile-examples
18
18
 
19
+ echo "== run mruby-compatible examples on mruby =="
20
+ MRUBY_RUNTIME_EXAMPLES=()
21
+ while IFS= read -r example || [[ -n "$example" ]]; do
22
+ [[ -n "$example" ]] && MRUBY_RUNTIME_EXAMPLES+=("$example")
23
+ done < examples/mruby-runtime-examples.txt
24
+
25
+ for example in "${MRUBY_RUNTIME_EXAMPLES[@]}"; do
26
+ ruby_file="examples-compiled/$example.rb"
27
+ file="examples-compiled/$example-mruby.rb"
28
+ if [[ ! -f "$file" ]]; then
29
+ file="$ruby_file"
30
+ fi
31
+
32
+ ruby "$ruby_file" >/dev/null
33
+ mruby "$file" >/dev/null
34
+ done
35
+
19
36
  echo "== Success! =="
data/bin/compile-examples CHANGED
@@ -4,6 +4,61 @@ set -euo pipefail
4
4
  ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
5
5
  SOURCE_DIR="$ROOT_DIR/examples"
6
6
  TARGET_DIR="$ROOT_DIR/examples-compiled"
7
+ tmp_files=()
8
+ MRUBY_RUNTIME_EXAMPLES=()
9
+
10
+ while IFS= read -r example || [[ -n "$example" ]]; do
11
+ [[ -n "$example" ]] && MRUBY_RUNTIME_EXAMPLES+=("$example")
12
+ done < "$SOURCE_DIR/mruby-runtime-examples.txt"
13
+
14
+ cleanup() {
15
+ rm -f "${tmp_files[@]}"
16
+ }
17
+ trap cleanup EXIT
18
+
19
+ is_mruby_runtime_example() {
20
+ local name="$1"
21
+
22
+ for example in "${MRUBY_RUNTIME_EXAMPLES[@]}"; do
23
+ [[ "$name" == "$example" ]] && return 0
24
+ done
25
+
26
+ return 1
27
+ }
28
+
29
+ needs_mruby_artifact() {
30
+ local name="$1"
31
+ local ruby_file="$2"
32
+ local ruby_stdout
33
+ local ruby_stderr
34
+ local mruby_stdout
35
+ local mruby_stderr
36
+
37
+ is_mruby_runtime_example "$name" || return 1
38
+
39
+ ruby_stdout="$(mktemp)"
40
+ ruby_stderr="$(mktemp)"
41
+ mruby_stdout="$(mktemp)"
42
+ mruby_stderr="$(mktemp)"
43
+ tmp_files+=("$ruby_stdout" "$ruby_stderr" "$mruby_stdout" "$mruby_stderr")
44
+
45
+ if ! ruby "$ruby_file" > "$ruby_stdout" 2> "$ruby_stderr"; then
46
+ printf 'Compiled example failed under ruby: %s\n' "$ruby_file" >&2
47
+ cat "$ruby_stderr" >&2
48
+ exit 1
49
+ fi
50
+
51
+ if ! mruby "$ruby_file" > "$mruby_stdout" 2> "$mruby_stderr"; then
52
+ return 0
53
+ fi
54
+
55
+ ! cmp -s "$ruby_stdout" "$mruby_stdout"
56
+ }
57
+
58
+ if (($# > 0)); then
59
+ printf 'usage: bin/compile-examples\n' >&2
60
+ exit 1
61
+ fi
7
62
 
8
63
  mkdir -p "$TARGET_DIR"
9
64
 
@@ -18,7 +73,22 @@ fi
18
73
  for kap_file in "${kap_files[@]}"; do
19
74
  name="$(basename "$kap_file" .kap)"
20
75
  ruby_file="$TARGET_DIR/$name.rb"
76
+ mruby_file="$TARGET_DIR/$name-mruby.rb"
21
77
 
22
78
  "$ROOT_DIR/exe/kapusta" --compile "$kap_file" > "$ruby_file"
23
79
  printf 'Compiled %s -> %s\n' "$kap_file" "$ruby_file"
80
+
81
+ if needs_mruby_artifact "$name" "$ruby_file"; then
82
+ tmp_mruby="$(mktemp)"
83
+ tmp_files+=("$tmp_mruby")
84
+ "$ROOT_DIR/exe/kapusta" --compile --target=mruby "$kap_file" > "$tmp_mruby"
85
+ if cmp -s "$ruby_file" "$tmp_mruby"; then
86
+ rm -f "$mruby_file"
87
+ else
88
+ mv "$tmp_mruby" "$mruby_file"
89
+ printf 'Compiled %s -> %s\n' "$kap_file" "$mruby_file"
90
+ fi
91
+ else
92
+ rm -f "$mruby_file"
93
+ fi
24
94
  done
data/bin/fennel-parity CHANGED
@@ -9,44 +9,10 @@ EXAMPLES_ERRORS = File.join(ROOT, 'examples-errors')
9
9
  KAPUSTA = File.join(ROOT, 'exe', 'kapusta')
10
10
  KAPFMT = File.join(ROOT, 'exe', 'kapfmt')
11
11
 
12
- COMPATIBLE = %w[
13
- ackermann.kap
14
- anonymous-greeter.kap
15
- classify-wallet.kap
16
- climbing-stairs.kap
17
- describe.kap
18
- destructure.kap
19
- even-squares.kap
20
- factorial.kap
21
- fib.kap
22
- fizzbuzz.kap
23
- gcd.kap
24
- hashfn.kap
25
- leap-year.kap
26
- macros-dbg.kap
27
- macros-import-helpers.kap
28
- macros-import-whole.kap
29
- macros-import.kap
30
- macros-multi.kap
31
- macros-swap.kap
32
- macros-thrice-if.kap
33
- macros-unless.kap
34
- macros-when-let.kap
35
- match.kap
36
- min-max.kap
37
- or-patterns.kap
38
- power-of-three.kap
39
- packet-router.kap
40
- points.kap
41
- primes.kap
42
- safe-lookup.kap
43
- shapes.kap
44
- squares.kap
45
- sum.kap
46
- thread-styles.kap
47
- tic-tac-toe.kap
48
- underscore-patterns.kap
49
- ].freeze
12
+ COMPATIBLE = File.readlines(File.join(EXAMPLES, 'fennel-parity-examples.txt'), chomp: true)
13
+ .reject(&:empty?)
14
+ .map { |name| "#{name}.kap" }
15
+ .freeze
50
16
 
51
17
  def run(cmd, file, chdir: EXAMPLES, env: {})
52
18
  out, err, status = Open3.capture3(env, cmd, file, chdir:)
@@ -0,0 +1,11 @@
1
+ (local max-missed 3)
2
+
3
+ (fn classify [guesses]
4
+ (var missed 0)
5
+ (each [_ g (ipairs guesses)]
6
+ (when (= g 1) (set missed (+ missed 1))))
7
+ (if (< missed max-missed) :ok :locked))
8
+
9
+ (print (classify [0 1 0 1]))
10
+ (print (classify [1 1 1]))
11
+ (print (classify [1 1 1 0]))
@@ -0,0 +1,16 @@
1
+ (class Circle)
2
+
3
+ (local pi 3.14159)
4
+
5
+ (fn initialize [radius]
6
+ (set @radius radius))
7
+
8
+ (fn area []
9
+ (* pi @radius @radius))
10
+
11
+ (fn circumference []
12
+ (* 2 pi @radius))
13
+
14
+ (let [c (Circle.new 5)]
15
+ (print (c.area))
16
+ (print (c.circumference)))
@@ -0,0 +1,14 @@
1
+ (local KELVIN-OFFSET 273.15)
2
+ (local FAHRENHEIT-SCALE 1.8)
3
+ (local FAHRENHEIT-OFFSET 32.0)
4
+
5
+ (fn convert-temperature [celsius]
6
+ (let [k (+ celsius KELVIN-OFFSET)
7
+ f (+ (* celsius FAHRENHEIT-SCALE) FAHRENHEIT-OFFSET)]
8
+ [k f]))
9
+
10
+ (let [[k f] (convert-temperature 36.5)]
11
+ (print k f))
12
+
13
+ (let [[k f] (convert-temperature 122.11)]
14
+ (print k f))
@@ -0,0 +1,13 @@
1
+ (fn count-by-kind [effects]
2
+ (var quits 0)
3
+ (var moves 0)
4
+ (each [_ effect (ipairs effects)]
5
+ (case (. effect :kind)
6
+ :quit (set quits (+ quits 1))
7
+ :move (set moves (+ moves 1))
8
+ _ nil))
9
+ [quits moves])
10
+
11
+ (let [effects [{:kind :move} {:kind :quit} {:kind :move} {:kind :other}]
12
+ [q m] (count-by-kind effects)]
13
+ (print q m))
@@ -0,0 +1,12 @@
1
+ (fn step [drops]
2
+ (each [_ drop (ipairs drops)]
3
+ (let [falling {:kind (. drop :kind)
4
+ :x (. drop :x)
5
+ :y (+ (. drop :y) (. drop :speed))
6
+ :w (. drop :w)
7
+ :h (. drop :h)
8
+ :speed (. drop :speed)}]
9
+ (print (. falling :kind) (. falling :x) (. falling :y)))))
10
+
11
+ (step [{:kind "rain" :x 0 :y 0 :w 1 :h 1 :speed 2}
12
+ {:kind "snow" :x 5 :y 1 :w 1 :h 1 :speed 1}])
@@ -0,0 +1,40 @@
1
+ ackermann
2
+ anonymous-greeter
3
+ classify-wallet
4
+ climbing-stairs
5
+ convert-temperature
6
+ count-effects
7
+ describe
8
+ destructure
9
+ even-squares
10
+ factorial
11
+ falling-drops
12
+ fib
13
+ fizzbuzz
14
+ gcd
15
+ hashfn
16
+ leap-year
17
+ macros-dbg
18
+ macros-import-helpers
19
+ macros-import-whole
20
+ macros-import
21
+ macros-multi
22
+ macros-swap
23
+ macros-thrice-if
24
+ macros-unless
25
+ macros-when-let
26
+ match
27
+ max-achievable
28
+ min-max
29
+ or-patterns
30
+ power-of-three
31
+ packet-router
32
+ points
33
+ primes
34
+ safe-lookup
35
+ shapes
36
+ squares
37
+ sum
38
+ thread-styles
39
+ tic-tac-toe
40
+ underscore-patterns
@@ -0,0 +1,8 @@
1
+ (local steps 2)
2
+
3
+ (fn max-achievable [num t]
4
+ (+ num (* steps t)))
5
+
6
+ (print (max-achievable 4 1))
7
+ (print (max-achievable 3 2))
8
+ (print (max-achievable 0 5))
@@ -0,0 +1,89 @@
1
+ account-lockout
2
+ accumulator
3
+ ackermann
4
+ anagram
5
+ anonymous-greeter
6
+ bank-account
7
+ baseball-game
8
+ best-time-to-buy-sell-stock
9
+ binary-search
10
+ binary-to-decimal
11
+ block-sort
12
+ calc
13
+ circle
14
+ classify-wallet
15
+ climbing-stairs
16
+ contains-duplicate
17
+ convert-temperature
18
+ count-effects
19
+ counter
20
+ describe
21
+ destructure
22
+ doto-hygiene
23
+ doto
24
+ egg-count
25
+ even-squares
26
+ exceptions
27
+ factorial
28
+ falling-drops
29
+ fib
30
+ fizzbuzz
31
+ gcd
32
+ greet
33
+ happy-number
34
+ hashfn
35
+ hit-counter
36
+ kwargs
37
+ leap-year
38
+ length-of-last-word
39
+ macros-dbg
40
+ macros-import-helpers
41
+ macros-import-whole
42
+ macros-import
43
+ macros-multi
44
+ macros-swap
45
+ macros-thrice-if
46
+ macros-unless
47
+ macros-when-let
48
+ majority-element
49
+ manhattan-distance
50
+ match
51
+ max-achievable
52
+ maximum-subarray
53
+ min-max
54
+ module-header
55
+ move-zeroes
56
+ number-of-1-bits
57
+ number-of-steps
58
+ or-patterns
59
+ packet-router
60
+ parking-system
61
+ pipeline
62
+ pivot-index
63
+ plus-one
64
+ points
65
+ power-of-three
66
+ primes
67
+ raindrops
68
+ record
69
+ reverse-integer
70
+ roman-to-integer
71
+ ruby-eval
72
+ safe-lookup
73
+ scopes
74
+ shapes
75
+ single-number
76
+ squares
77
+ stack
78
+ subtract-product-sum
79
+ sum
80
+ thread-styles
81
+ threading
82
+ tic-tac-toe
83
+ tset
84
+ two-sum-hash
85
+ two-sum
86
+ ugly-number
87
+ underscore-patterns
88
+ valid-parentheses-1
89
+ zoo-animal-1
@@ -0,0 +1,13 @@
1
+ (local BIT-WIDTH 32)
2
+
3
+ (fn hamming-weight [n]
4
+ (var x n)
5
+ (var count 0)
6
+ (for [_ 1 BIT-WIDTH]
7
+ (set count (+ count (% x 2)))
8
+ (set x (: (/ x 2) :floor)))
9
+ count)
10
+
11
+ (print (hamming-weight 11))
12
+ (print (hamming-weight 128))
13
+ (print (hamming-weight 4294967293))
@@ -0,0 +1,15 @@
1
+ (local target 0)
2
+
3
+ (fn steps-to-zero [n]
4
+ (var x n)
5
+ (var steps 0)
6
+ (while (not= x target)
7
+ (if (= 0 (% x 2))
8
+ (set x (: (/ x 2) :floor))
9
+ (set x (- x 1)))
10
+ (set steps (+ steps 1)))
11
+ steps)
12
+
13
+ (print (steps-to-zero 14))
14
+ (print (steps-to-zero 8))
15
+ (print (steps-to-zero 123))
@@ -1,17 +1,14 @@
1
- (let [
2
- two-sum-hash
3
- (fn [nums target seen]
4
- (var i 0)
5
- (var answer nil)
6
- (while (and (< i (length nums)) (= answer nil))
7
- (let [n (. nums i)
8
- complement (- target n)]
9
- (if (seen.key? complement)
10
- (set answer [(. seen complement) i])
11
- (tset seen n i)))
12
- (set i (+ i 1)))
13
- answer)
14
- ]
1
+ (let [two-sum-hash (fn [nums target seen]
2
+ (var i 0)
3
+ (var answer nil)
4
+ (while (and (< i (length nums)) (= answer nil))
5
+ (let [n (. nums i)
6
+ complement (- target n)]
7
+ (if (seen.key? complement)
8
+ (set answer [(. seen complement) i])
9
+ (tset seen n i)))
10
+ (set i (+ i 1)))
11
+ answer)]
15
12
  (print (two-sum-hash [2 7 11 15] 9 {}))
16
13
  (print (two-sum-hash [3 2 4] 6 {}))
17
14
  (print (two-sum-hash [1 2 3] 10 {})))
@@ -1,7 +1,7 @@
1
1
  (fn loose [v]
2
2
  (case v
3
3
  _x _x
4
- _ "fallback"))
4
+ _ "not-reachable"))
5
5
 
6
6
  (fn strict [v]
7
7
  (case v
data/lib/kapusta/cli.rb CHANGED
@@ -5,7 +5,7 @@ require 'optparse'
5
5
 
6
6
  module Kapusta
7
7
  class CLI
8
- Options = Struct.new(:compile, :help, :version, keyword_init: true)
8
+ Options = Struct.new(:compile, :target, :help, :version, keyword_init: true)
9
9
 
10
10
  def self.start(argv = ARGV)
11
11
  args = argv.dup
@@ -21,8 +21,10 @@ module Kapusta
21
21
  return
22
22
  end
23
23
 
24
+ raise Kapusta::Error, Kapusta::Errors.format(:target_requires_compile) if options.target && !options.compile
25
+
24
26
  if options.compile
25
- compile_file(args)
27
+ compile_file(args, target: options.target)
26
28
  else
27
29
  run_file(args)
28
30
  end
@@ -32,11 +34,14 @@ module Kapusta
32
34
  end
33
35
 
34
36
  def self.parse_options(args)
35
- options = Options.new(compile: false, help: false, version: false)
37
+ options = Options.new(compile: false, target: nil, help: false, version: false)
36
38
 
37
39
  OptionParser.new do |parser|
38
40
  parser.banner = usage
39
41
  parser.on('-c', '--compile', 'Compile .kap to Ruby') { options.compile = true }
42
+ parser.on('--target=TARGET', 'Compile for mruby') do |target|
43
+ options.target = Kapusta::Compiler.normalize_target(target)
44
+ end
40
45
  parser.on('-h', '--help', 'Show this help') { options.help = true }
41
46
  parser.on('-v', '--version', 'Show version') { options.version = true }
42
47
  end.order!(args)
@@ -44,12 +49,12 @@ module Kapusta
44
49
  options
45
50
  end
46
51
 
47
- def self.compile_file(args)
52
+ def self.compile_file(args, target:)
48
53
  path = args.shift
49
54
  abort usage unless path
50
55
  abort usage unless args.empty?
51
56
 
52
- $stdout.write(Kapusta.compile(File.read(path), path:))
57
+ $stdout.write(Kapusta.compile(File.read(path), path:, target:))
53
58
  end
54
59
 
55
60
  def self.run_file(args)
@@ -64,7 +69,7 @@ module Kapusta
64
69
  end
65
70
 
66
71
  def self.usage
67
- 'usage: kapusta [--compile|-c] <file.kap> | kapusta <file.kap> [args...]'
72
+ 'usage: kapusta [--compile|-c] [--target=mruby] <file.kap> | kapusta <file.kap> [args...]'
68
73
  end
69
74
  end
70
75
  end
@@ -181,6 +181,14 @@ module Kapusta
181
181
 
182
182
  def emit_toplevel_method_bridge(ruby_name)
183
183
  method_name = ruby_name.to_sym.inspect
184
+ if mruby_target?
185
+ return [
186
+ "define_singleton_method(#{method_name}) do |*args|",
187
+ indent("Object.instance_method(#{method_name}).bind(self).call(*args)"),
188
+ 'end'
189
+ ].join("\n")
190
+ end
191
+
184
192
  "define_singleton_method(#{method_name}, Object.instance_method(#{method_name}).bind(self))"
185
193
  end
186
194
 
@@ -229,8 +237,14 @@ module Kapusta
229
237
 
230
238
  binding = env.lookup_if_defined(name)
231
239
  return false if binding.nil?
240
+ return false if method_binding?(binding)
241
+ return false if constant_binding?(binding)
242
+
243
+ true
244
+ end
232
245
 
233
- !method_binding?(binding)
246
+ def constant_binding?(binding)
247
+ binding.is_a?(String) && binding.match?(/\A[A-Z][A-Z0-9_]*\z/)
234
248
  end
235
249
 
236
250
  def emit_let(args, env, current_scope)
@@ -277,7 +291,7 @@ module Kapusta
277
291
  chunks.reject(&:empty?).join("\n")
278
292
  end
279
293
 
280
- def emit_local_form(form, env, current_scope)
294
+ def emit_local_form(form, env, current_scope, allow_constant: false)
281
295
  emit_error!(:local_arity, form: form.head.name) unless form.items.length == 3
282
296
 
283
297
  target = form.items[1]
@@ -285,6 +299,12 @@ module Kapusta
285
299
 
286
300
  if target.is_a?(Sym)
287
301
  validate_binding_symbol!(target)
302
+ if allow_constant && form.head.name == 'local' && (constant_name = constant_name_for(target.name))
303
+ env.define(target.name, constant_name)
304
+ mark_mutability(env, target.name, mutable: false)
305
+ return ["#{constant_name} = #{value_code}\nnil", env]
306
+ end
307
+
288
308
  ruby_name = define_local(env, target.name)
289
309
  mark_mutability(env, target.name, mutable: form.head.name == 'var')
290
310
  ["#{ruby_name} = #{value_code}\nnil", env]
@@ -294,6 +314,11 @@ module Kapusta
294
314
  end
295
315
  end
296
316
 
317
+ def constant_name_for(source_name)
318
+ candidate = source_name.tr('-', '_').upcase
319
+ candidate if candidate.match?(/\A[A-Z][A-Z0-9_]*\z/)
320
+ end
321
+
297
322
  def check_destructure_value!(pattern, value_form)
298
323
  return unless pattern.is_a?(Vec) || pattern.is_a?(HashLit)
299
324
 
@@ -57,6 +57,25 @@ module Kapusta
57
57
  end
58
58
 
59
59
  def emit_case(args, env, current_scope, mode)
60
+ value_code, value_var, body = build_case_parts(args, env, current_scope, mode)
61
+ return body unless value_var
62
+
63
+ [
64
+ '(-> do',
65
+ indent("#{value_var} = #{value_code}"),
66
+ indent(body),
67
+ 'end).call'
68
+ ].join("\n")
69
+ end
70
+
71
+ def emit_case_statement(args, env, current_scope, mode)
72
+ value_code, value_var, body = build_case_parts(args, env, current_scope, mode)
73
+ return body unless value_var
74
+
75
+ "#{value_var} = #{value_code}\n#{body}"
76
+ end
77
+
78
+ def build_case_parts(args, env, current_scope, mode)
60
79
  emit_error!(:case_no_subject) if args.empty?
61
80
 
62
81
  clauses = args[1..]
@@ -65,20 +84,22 @@ module Kapusta
65
84
 
66
85
  value_code = emit_expr(args[0], env, current_scope)
67
86
  if simple_case_subject?(args[0]) && simple_expression?(value_code)
68
- body = try_emit_native_case(value_code, clauses, env, current_scope, mode)
87
+ body = emit_case_body(value_code, clauses, env, current_scope, mode)
69
88
  emit_error!(:case_unsupported) unless body
70
- return body
89
+ return [value_code, nil, body]
71
90
  end
72
91
 
73
92
  value_var = temp('case_value')
74
- body = try_emit_native_case(value_var, clauses, env, current_scope, mode)
93
+ body = emit_case_body(value_var, clauses, env, current_scope, mode)
75
94
  emit_error!(:case_unsupported) unless body
76
- [
77
- '(-> do',
78
- indent("#{value_var} = #{value_code}"),
79
- indent(body),
80
- 'end).call'
81
- ].join("\n")
95
+ [value_code, value_var, body]
96
+ end
97
+
98
+ def emit_case_body(value_var, clauses, env, current_scope, mode)
99
+ return try_emit_compat_case(value_var, clauses, env, current_scope, mode) if mruby_target?
100
+
101
+ try_emit_native_case(value_var, clauses, env, current_scope, mode) ||
102
+ try_emit_compat_case(value_var, clauses, env, current_scope, mode)
82
103
  end
83
104
 
84
105
  def simple_case_subject?(form)
@@ -113,11 +134,6 @@ module Kapusta
113
134
  ["case #{value_var}", *arms, 'end'].join("\n")
114
135
  end
115
136
 
116
- def wildcard_last?(clauses)
117
- last_pattern = clauses[-2]
118
- last_pattern.is_a?(Sym) && last_pattern.name == '_'
119
- end
120
-
121
137
  def try_native_arm(pattern, body, where_guards, env, current_scope, mode)
122
138
  allow_pins = !where_guards.empty? && mode == :case
123
139
  plan = native_pattern_plan(pattern, env, mode:, allow_pins:)
@@ -132,6 +148,73 @@ module Kapusta
132
148
  ["in #{plan[:pattern]}#{guard_clause}", indent(body_code)].join("\n")
133
149
  end
134
150
 
151
+ def try_emit_compat_case(value_var, clauses, env, current_scope, mode)
152
+ arms = []
153
+ i = 0
154
+ while i < clauses.length
155
+ pattern = clauses[i]
156
+ body = clauses[i + 1]
157
+ inner, where_guards = if where_pattern?(pattern)
158
+ [pattern.items[1], pattern.items[2..]]
159
+ else
160
+ [pattern, []]
161
+ end
162
+ sub_patterns = or_pattern?(inner) ? inner.items[1..] : [inner]
163
+ sub_arms = sub_patterns.map do |sub|
164
+ try_compat_arm(sub, body, where_guards, value_var, env, current_scope, mode)
165
+ end
166
+ return if sub_arms.any?(&:nil?)
167
+
168
+ arms.concat(sub_arms)
169
+ i += 2
170
+ end
171
+ emit_compat_case_lines(arms)
172
+ end
173
+
174
+ def wildcard_last?(clauses)
175
+ last_pattern = clauses[-2]
176
+ last_pattern.is_a?(Sym) && last_pattern.name == '_'
177
+ end
178
+
179
+ def try_compat_arm(pattern, body, where_guards, value_var, env, current_scope, mode)
180
+ allow_pins = !where_guards.empty? && mode == :case
181
+ arm_env = env.child
182
+ plan = compat_pattern_plan(pattern, value_var, env, arm_env, mode:, allow_pins:)
183
+ return unless plan
184
+
185
+ where_guard_codes = where_guards.map { |g| emit_expr(g, arm_env, current_scope) }
186
+ if where_guard_codes.empty?
187
+ guard_codes = plan[:conditions]
188
+ prelude = plan[:prelude]
189
+ else
190
+ prelude_guards = plan[:prelude].map { |line| "begin #{line}; true end" }
191
+ guard_codes = plan[:conditions] + prelude_guards + where_guard_codes
192
+ prelude = []
193
+ end
194
+ body_code = emit_expr(body, arm_env, current_scope)
195
+ [guard_codes, prelude, body_code]
196
+ end
197
+
198
+ def emit_compat_case_lines(arms)
199
+ last_idx = arms.length - 1
200
+ has_unconditional = arms.any? { |conditions, _prelude, _body_code| conditions.empty? }
201
+ lines = ['case']
202
+ arms.each_with_index do |(conditions, prelude, body_code), idx|
203
+ lines << if conditions.empty?
204
+ idx == last_idx ? 'else' : 'when true'
205
+ else
206
+ "when #{conditions.join(' && ')}"
207
+ end
208
+ lines << indent([*prelude, body_code].join("\n"))
209
+ end
210
+ unless has_unconditional
211
+ lines << 'else'
212
+ lines << indent('nil')
213
+ end
214
+ lines << 'end'
215
+ lines.join("\n")
216
+ end
217
+
135
218
  def emit_while(args, env, current_scope)
136
219
  <<~RUBY.chomp
137
220
  (-> do
@@ -300,6 +300,8 @@ module Kapusta
300
300
  def emit_bound_call(binding, args, env, current_scope)
301
301
  return emit_self_method_binding_call(binding, args, env, current_scope) if method_binding?(binding)
302
302
 
303
+ emit_error!(:cannot_call_constant, name: binding) if constant_binding?(binding)
304
+
303
305
  emit_callable_call(binding, args, env, current_scope)
304
306
  end
305
307
 
@@ -161,6 +161,131 @@ module Kapusta
161
161
 
162
162
  class PatternNotTranslatable < StandardError; end
163
163
 
164
+ def compat_pattern_plan(pattern, value_code, env, arm_env, mode:, allow_pins:)
165
+ state = { bound_names: {}, conditions: [], prelude: [] }
166
+ compile_compat_pattern(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
167
+ { conditions: state[:conditions], prelude: state[:prelude] }
168
+ rescue PatternNotTranslatable
169
+ nil
170
+ end
171
+
172
+ def compile_compat_pattern(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
173
+ case pattern
174
+ when Sym
175
+ compile_compat_symbol(pattern, value_code, env, arm_env, mode:, state:)
176
+ when Vec
177
+ compile_compat_sequence(pattern.items, value_code, env, arm_env, mode:, allow_pins:, state:)
178
+ when HashLit
179
+ compile_compat_hash(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
180
+ when List
181
+ if pin_pattern?(pattern)
182
+ compile_compat_pin(pattern, value_code, env, mode:, allow_pins:, state:)
183
+ elsif or_pattern?(pattern)
184
+ raise PatternNotTranslatable
185
+ else
186
+ compile_compat_sequence(pattern.items, value_code, env, arm_env, mode:, allow_pins:, state:)
187
+ end
188
+ when nil
189
+ state[:conditions] << "#{value_code}.nil?"
190
+ when Symbol, String, Numeric, true, false
191
+ state[:conditions] << "#{value_code} == #{pattern.inspect}"
192
+ else
193
+ raise PatternNotTranslatable
194
+ end
195
+ end
196
+
197
+ def compile_compat_symbol(pattern, value_code, env, arm_env, mode:, state:)
198
+ name = pattern.name
199
+ return if name == '_'
200
+
201
+ if nil_allowing_pattern_name?(name)
202
+ raise PatternNotTranslatable if state[:bound_names].key?(name)
203
+
204
+ ruby = define_local(arm_env, name)
205
+ state[:bound_names][name] = true
206
+ state[:prelude] << "#{ruby} = #{value_code}"
207
+ return
208
+ end
209
+
210
+ binding = mode == :match ? env.lookup_if_defined(name) : nil
211
+ if state[:bound_names].key?(name)
212
+ raise PatternNotTranslatable
213
+ elsif binding
214
+ state[:conditions] << "#{value_code} == #{binding_value_code(binding)}"
215
+ else
216
+ ruby = define_local(arm_env, name)
217
+ state[:bound_names][name] = true
218
+ state[:conditions] << "(#{ruby} = #{value_code}) != nil"
219
+ end
220
+ end
221
+
222
+ def compile_compat_sequence(items, value_code, env, arm_env, mode:, allow_pins:, state:)
223
+ min_length = compat_sequence_min_length(items)
224
+ state[:conditions] << "#{value_code}.is_a?(Array)"
225
+ state[:conditions] << "#{value_code}.length >= #{min_length}"
226
+
227
+ index = 0
228
+ i = 0
229
+ while i < items.length
230
+ if rest_pattern_marker?(items, i)
231
+ sub = items[i + 1]
232
+ raise PatternNotTranslatable unless sub.is_a?(Sym)
233
+
234
+ unless sub.name == '_'
235
+ ruby = define_local(arm_env, sub.name)
236
+ state[:prelude] << "#{ruby} = #{value_code}[#{index}..]"
237
+ end
238
+ i += 2
239
+ else
240
+ compile_compat_pattern(items[i], "#{value_code}[#{index}]", env, arm_env,
241
+ mode:, allow_pins:, state:)
242
+ index += 1
243
+ i += 1
244
+ end
245
+ end
246
+ end
247
+
248
+ def compat_sequence_min_length(items)
249
+ count = 0
250
+ i = 0
251
+ while i < items.length
252
+ if rest_pattern_marker?(items, i)
253
+ i += 2
254
+ else
255
+ count += 1
256
+ i += 1
257
+ end
258
+ end
259
+ count
260
+ end
261
+
262
+ def compile_compat_hash(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
263
+ state[:conditions] << "#{value_code}.is_a?(Hash)"
264
+ pattern.pairs.each do |key, value|
265
+ lookup = "#{value_code}[#{compile_compat_hash_key(key)}]"
266
+ compile_compat_pattern(value, lookup, env, arm_env, mode:, allow_pins:, state:)
267
+ end
268
+ end
269
+
270
+ def compile_compat_hash_key(key)
271
+ case key
272
+ when Symbol, String, Numeric, true, false, nil then key.inspect
273
+ else raise PatternNotTranslatable
274
+ end
275
+ end
276
+
277
+ def compile_compat_pin(pattern, value_code, env, mode:, allow_pins:, state:)
278
+ raise PatternNotTranslatable unless allow_pins && mode == :case
279
+
280
+ name_sym = pattern.items[1]
281
+ raise PatternNotTranslatable unless name_sym.is_a?(Sym)
282
+
283
+ binding = env.lookup_if_defined(name_sym.name)
284
+ raise PatternNotTranslatable unless binding
285
+
286
+ state[:conditions] << "#{value_code} == #{binding_value_code(binding)}"
287
+ end
288
+
164
289
  def native_pattern_plan(pattern, env, mode:, allow_pins:)
165
290
  state = { bound_names: {}, binding_names: [], guards: [] }
166
291
  ruby_pattern = compile_native_pattern(pattern, env, mode:, allow_pins:, state:)
@@ -25,6 +25,10 @@ module Kapusta
25
25
  (@form_stack ||= []).last
26
26
  end
27
27
 
28
+ def mruby_target?
29
+ @target == :mruby
30
+ end
31
+
28
32
  def positionable?(form)
29
33
  form.respond_to?(:line) && form.respond_to?(:column)
30
34
  end
@@ -97,7 +101,8 @@ module Kapusta
97
101
  if named_function_form?(form)
98
102
  emit_named_fn_assignment(form, env, current_scope)
99
103
  elsif local_form?(form)
100
- code, env = emit_local_form(form, env, current_scope)
104
+ code, env = emit_local_form(form, env, current_scope,
105
+ allow_constant: allow_method_definitions)
101
106
  code = code.delete_suffix("\nnil") unless result_needed
102
107
  [code, env]
103
108
  elsif do_form?(form)
@@ -136,7 +141,7 @@ module Kapusta
136
141
  def sequence_statement_form?(form)
137
142
  return false unless form.is_a?(List) && form.head.is_a?(Sym)
138
143
 
139
- %w[let while for each].include?(form.head.name)
144
+ %w[let while for each case match].include?(form.head.name)
140
145
  end
141
146
 
142
147
  def emit_sequence_statement_form(form, env, current_scope, result_needed:)
@@ -152,6 +157,8 @@ module Kapusta
152
157
  return ["#{code}\nnil", env] if result_needed && current_scope != :toplevel
153
158
 
154
159
  return [code, env]
160
+ when 'case', 'match'
161
+ return [emit_case_statement(form.rest, env, current_scope, form.head.name.to_sym), env] unless result_needed
155
162
  end
156
163
 
157
164
  [emit_expr(form, env, current_scope), env]
@@ -24,8 +24,9 @@ module Kapusta
24
24
  include EmitterModules::Interop
25
25
  include EmitterModules::Patterns
26
26
 
27
- def initialize(path:)
27
+ def initialize(path:, target: nil)
28
28
  @path = path
29
+ @target = target
29
30
  @temp_index = 0
30
31
  end
31
32
 
@@ -34,17 +34,17 @@ module Kapusta
34
34
  ].freeze
35
35
  SPECIAL_FORMS = (CORE_SPECIAL_FORMS + LuaCompat::SPECIAL_FORMS).freeze
36
36
 
37
- def self.compile(source, path: '(kapusta)')
37
+ def self.compile(source, path: '(kapusta)', target: nil)
38
38
  forms = Reader.read_all(source)
39
39
  expanded = MacroExpander.new(path:).expand_all(forms)
40
- compile_forms(expanded, path:)
40
+ compile_forms(expanded, path:, target:)
41
41
  rescue Kapusta::Error => e
42
42
  raise e.with_defaults(path:)
43
43
  end
44
44
 
45
- def self.compile_forms(forms, path: '(kapusta)')
45
+ def self.compile_forms(forms, path: '(kapusta)', target: nil)
46
46
  normalized = Normalizer.new.normalize_all(forms)
47
- Emitter.new(path:).emit_file(normalized)
47
+ Emitter.new(path:, target: normalize_target(target)).emit_file(normalized)
48
48
  rescue Kapusta::Error => e
49
49
  raise e.with_defaults(path:)
50
50
  end
@@ -57,5 +57,14 @@ module Kapusta
57
57
  def self.run_file(path)
58
58
  run(File.read(path), path:)
59
59
  end
60
+
61
+ def self.normalize_target(target)
62
+ case target
63
+ when nil then nil
64
+ when :mruby, 'mruby' then :mruby
65
+ else
66
+ raise Error, Kapusta::Errors.format(:unknown_target, target: target.inspect)
67
+ end
68
+ end
60
69
  end
61
70
  end
@@ -10,6 +10,7 @@ module Kapusta
10
10
  bad_set_target: 'bad set target: %{target}',
11
11
  bad_shorthand: 'bad shorthand',
12
12
  bind_table_dots: 'unable to bind table ...',
13
+ cannot_call_constant: 'cannot call constant %{name}; reference it without parentheses',
13
14
  cannot_call_literal: 'cannot call literal value %{value}',
14
15
  cannot_emit_form: 'cannot emit form: %{form}',
15
16
  cannot_set_method_binding: 'cannot set method binding: %{name}',
@@ -51,6 +52,7 @@ module Kapusta
51
52
  odd_forms_in_hash: 'odd number of forms in hash',
52
53
  rest_not_last: 'expected rest argument before last parameter',
53
54
  shadowed_special: 'local %{name} was overshadowed by a special form or macro',
55
+ target_requires_compile: '--target requires --compile',
54
56
  tset_no_value: 'tset: expected table, key, and value arguments',
55
57
  unclosed_delimiter: "unclosed opening delimiter '%{char}'",
56
58
  undefined_symbol: 'undefined symbol: %{name}',
@@ -58,6 +60,7 @@ module Kapusta
58
60
  unexpected_eof: 'unexpected eof',
59
61
  unexpected_vararg: 'unexpected vararg',
60
62
  unknown_special_form: 'unknown special form: %{name}',
63
+ unknown_target: 'unknown target %{target}; only mruby is supported',
61
64
  unquote_outside_quasiquote: 'unquote outside quasiquote',
62
65
  unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
63
66
  unterminated_string: 'unterminated string',
@@ -657,7 +657,6 @@ module Kapusta
657
657
 
658
658
  def render_let_bindings(bindings, indent)
659
659
  return render(bindings, indent + '(let '.length, force_expand: true) if contains_comments?(bindings.items)
660
- return render(bindings, indent + '(let '.length, layout: :pairwise) if bindings.items.length <= 2
661
660
 
662
661
  hanging = render_hanging_pairwise_vec(bindings)
663
662
  hanging || render(bindings, indent + '(let '.length, layout: :pairwise)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Kapusta
4
- VERSION = '0.9.0'
4
+ VERSION = '0.10.0'
5
5
  end
data/lib/kapusta.rb CHANGED
@@ -23,8 +23,8 @@ module Kapusta
23
23
  self.eval(source, path:)
24
24
  end
25
25
 
26
- def self.compile(source, path: '(eval)', **_opts)
27
- Compiler.compile(source, path:)
26
+ def self.compile(source, path: '(eval)', target: nil, **_opts)
27
+ Compiler.compile(source, path:, target:)
28
28
  end
29
29
 
30
30
  def self.require(feature, relative_to: nil)
data/spec/cli_spec.rb CHANGED
@@ -66,6 +66,19 @@ RSpec.describe Kapusta::CLI do
66
66
  end
67
67
  end
68
68
 
69
+ it 'compiles case and match forms for mruby with --target=mruby' do
70
+ path = File.expand_path('../examples/match.kap', __dir__)
71
+
72
+ ruby = capture_stdout do
73
+ described_class.start(['--compile', '--target=mruby', path])
74
+ end
75
+
76
+ expect(ruby).not_to match(/^\s*in\b/)
77
+ expect(ruby).not_to include('^(')
78
+ expect(ruby).to include("case\n")
79
+ expect(ruby).to include('when ')
80
+ end
81
+
69
82
  it 'rejects extra positional arguments in compile mode' do
70
83
  path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
71
84
 
@@ -77,6 +90,28 @@ RSpec.describe Kapusta::CLI do
77
90
  expect(error_output).to include('usage: kapusta')
78
91
  end
79
92
 
93
+ it 'rejects unsupported targets' do
94
+ path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
95
+
96
+ error_output = capture_stderr do
97
+ expect { described_class.start(['--compile', '--target=mri', path]) }
98
+ .to raise_error(SystemExit)
99
+ end
100
+
101
+ expect(error_output).to include('unknown target "mri"; only mruby is supported')
102
+ end
103
+
104
+ it 'rejects target without compile mode' do
105
+ path = File.expand_path('../examples/fizzbuzz.kap', __dir__)
106
+
107
+ error_output = capture_stderr do
108
+ expect { described_class.start(['--target=mruby', path]) }
109
+ .to raise_error(SystemExit)
110
+ end
111
+
112
+ expect(error_output).to include('--target requires --compile')
113
+ end
114
+
80
115
  it 'passes remaining arguments through to the Kapusta program' do
81
116
  path = File.expand_path('../examples/greet.kap', __dir__)
82
117
 
@@ -2,10 +2,21 @@
2
2
 
3
3
  require 'spec_helper'
4
4
  require 'fileutils'
5
+ require 'open3'
5
6
  require 'stringio'
7
+ require 'tempfile'
6
8
 
7
9
  EXAMPLES_DIR = File.expand_path('../examples', __dir__)
8
10
 
11
+ def example_list(name)
12
+ File.readlines(File.join(EXAMPLES_DIR, name), chomp: true)
13
+ .reject(&:empty?)
14
+ .map { |example| "#{example}.kap" }
15
+ .freeze
16
+ end
17
+
18
+ MRUBY_RUNTIME_EXAMPLES = example_list('mruby-runtime-examples.txt')
19
+
9
20
  def run_example(name, argv: [])
10
21
  previous_argv = ARGV.dup
11
22
  previous_stdout = $stdout
@@ -27,6 +38,39 @@ ensure
27
38
  $stdout = previous_stdout
28
39
  end
29
40
 
41
+ def compile_example(name, target: nil)
42
+ path = File.join(EXAMPLES_DIR, name)
43
+ Kapusta.compile(File.read(path), path:, target:)
44
+ end
45
+
46
+ def run_compiled_source(source, path:)
47
+ previous_argv = ARGV.dup
48
+ previous_stdout = $stdout
49
+ ARGV.replace([])
50
+ $stdout = StringIO.new
51
+ TOPLEVEL_BINDING.eval(source, path, 1)
52
+ $stdout.string
53
+ ensure
54
+ $stdout = previous_stdout
55
+ ARGV.replace(previous_argv)
56
+ end
57
+
58
+ def run_mruby_source(source, path:)
59
+ stdout, stderr, status = capture_mruby_source(source, path:)
60
+
61
+ raise "mruby failed for #{path}:\n#{stderr}" unless status.success?
62
+
63
+ stdout
64
+ end
65
+
66
+ def capture_mruby_source(source, path:)
67
+ Tempfile.create([File.basename(path, '.kap'), '.rb']) do |file|
68
+ file.write(source)
69
+ file.close
70
+ Open3.capture3('mruby', file.path)
71
+ end
72
+ end
73
+
30
74
  RSpec.describe 'examples' do
31
75
  it 'ackermann.kap' do
32
76
  expect(run_example('ackermann.kap')).to eq("9\n61\n")
@@ -36,6 +80,18 @@ RSpec.describe 'examples' do
36
80
  expect(run_example('accumulator.kap')).to eq("22\n")
37
81
  end
38
82
 
83
+ it 'account-lockout.kap' do
84
+ expect(run_example('account-lockout.kap')).to eq(<<~OUT)
85
+ :ok
86
+ :locked
87
+ :locked
88
+ OUT
89
+ end
90
+
91
+ it 'circle.kap' do
92
+ expect(run_example('circle.kap')).to eq("78.53975\n31.4159\n")
93
+ end
94
+
39
95
  it 'anagram.kap' do
40
96
  expect(run_example('anagram.kap')).to eq("true\ntrue\nfalse\n")
41
97
  end
@@ -107,6 +163,26 @@ RSpec.describe 'examples' do
107
163
  expect(run_example('happy-number.kap')).to eq("true\nfalse\ntrue\n")
108
164
  end
109
165
 
166
+ it 'number-of-1-bits.kap' do
167
+ expect(run_example('number-of-1-bits.kap')).to eq("3\n1\n31\n")
168
+ end
169
+
170
+ it 'number-of-steps.kap' do
171
+ expect(run_example('number-of-steps.kap')).to eq("6\n4\n12\n")
172
+ end
173
+
174
+ it 'convert-temperature.kap' do
175
+ expect(run_example('convert-temperature.kap')).to eq("309.65\n97.7\n395.26\n251.798\n")
176
+ end
177
+
178
+ it 'count-effects.kap' do
179
+ expect(run_example('count-effects.kap')).to eq("1\n2\n")
180
+ end
181
+
182
+ it 'max-achievable.kap' do
183
+ expect(run_example('max-achievable.kap')).to eq("6\n7\n10\n")
184
+ end
185
+
110
186
  it 'move-zeroes.kap' do
111
187
  expect(run_example('move-zeroes.kap')).to eq(<<~OUT)
112
188
  [1, 3, 12, 0, 0]
@@ -368,6 +444,18 @@ RSpec.describe 'examples' do
368
444
  OUT
369
445
  end
370
446
 
447
+ it 'underscore-patterns.kap on mruby keeps loose nil and strict fallback separate' do
448
+ path = File.join(EXAMPLES_DIR, 'underscore-patterns.kap')
449
+ ruby = compile_example('underscore-patterns.kap', target: :mruby)
450
+
451
+ expect(run_mruby_source(ruby, path:)).to eq(<<~OUT)
452
+ 5
453
+ nil
454
+ 5
455
+ "fallback"
456
+ OUT
457
+ end
458
+
371
459
  it 'scopes.kap' do
372
460
  expect(run_example('scopes.kap')).to eq("5\n9\n9\n9\n")
373
461
  end
@@ -577,3 +665,30 @@ RSpec.describe 'examples' do
577
665
  OUT
578
666
  end
579
667
  end
668
+
669
+ RSpec.describe 'mruby runtime examples' do
670
+ MRUBY_RUNTIME_EXAMPLES.each do |name|
671
+ it name do
672
+ path = File.join(EXAMPLES_DIR, name)
673
+ ruby = compile_example(name)
674
+ expected = run_example(name)
675
+ expect(run_compiled_source(ruby, path:)).to eq(expected)
676
+ mruby_stdout, _mruby_stderr, mruby_status = capture_mruby_source(ruby, path:)
677
+
678
+ if mruby_status.success? && mruby_stdout == expected
679
+ expect(run_mruby_source(ruby, path:)).to eq(expected)
680
+ else
681
+ mruby_ruby = compile_example(name, target: :mruby)
682
+
683
+ if mruby_ruby == ruby
684
+ expect(mruby_status).to be_success
685
+ else
686
+ expect(mruby_ruby).not_to match(/^\s*in\b/)
687
+ expect(mruby_ruby).not_to include('^(')
688
+ expect(run_compiled_source(mruby_ruby, path:)).to eq(expected)
689
+ run_mruby_source(mruby_ruby, path:)
690
+ end
691
+ end
692
+ end
693
+ end
694
+ end
data/spec/spec_helper.rb CHANGED
@@ -3,6 +3,15 @@
3
3
  require 'bundler/setup'
4
4
  require 'kapusta'
5
5
 
6
+ module SilenceConstantRedefinitionWarnings
7
+ def warn(message, category: nil)
8
+ return if /already initialized constant|previous definition of/.match?(message)
9
+
10
+ super
11
+ end
12
+ end
13
+ Warning.singleton_class.prepend(SilenceConstantRedefinitionWarnings)
14
+
6
15
  RSpec.configure do |config|
7
16
  config.disable_monkey_patching!
8
17
  config.example_status_persistence_file_path = '.rspec_status'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kapusta
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.0
4
+ version: 0.10.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evgenii Morozov
@@ -26,6 +26,7 @@ files:
26
26
  - bin/console
27
27
  - bin/fennel-parity
28
28
  - bin/setup
29
+ - examples/account-lockout.kap
29
30
  - examples/accumulator.kap
30
31
  - examples/ackermann.kap
31
32
  - examples/anagram.kap
@@ -38,9 +39,12 @@ files:
38
39
  - examples/block-sort.kap
39
40
  - examples/blocks-and-kwargs.kap
40
41
  - examples/calc.kap
42
+ - examples/circle.kap
41
43
  - examples/classify-wallet.kap
42
44
  - examples/climbing-stairs.kap
43
45
  - examples/contains-duplicate.kap
46
+ - examples/convert-temperature.kap
47
+ - examples/count-effects.kap
44
48
  - examples/counter.kap
45
49
  - examples/describe.kap
46
50
  - examples/destructure.kap
@@ -50,6 +54,8 @@ files:
50
54
  - examples/even-squares.kap
51
55
  - examples/exceptions.kap
52
56
  - examples/factorial.kap
57
+ - examples/falling-drops.kap
58
+ - examples/fennel-parity-examples.txt
53
59
  - examples/fib.kap
54
60
  - examples/files.kap
55
61
  - examples/fizzbuzz.kap
@@ -74,10 +80,14 @@ files:
74
80
  - examples/majority-element.kap
75
81
  - examples/manhattan-distance.kap
76
82
  - examples/match.kap
83
+ - examples/max-achievable.kap
77
84
  - examples/maximum-subarray.kap
78
85
  - examples/min-max.kap
79
86
  - examples/module-header.kap
80
87
  - examples/move-zeroes.kap
88
+ - examples/mruby-runtime-examples.txt
89
+ - examples/number-of-1-bits.kap
90
+ - examples/number-of-steps.kap
81
91
  - examples/or-patterns.kap
82
92
  - examples/packet-router.kap
83
93
  - examples/palindrome.kap