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 +4 -4
- data/README.md +8 -2
- data/bin/check-all +17 -0
- data/bin/compile-examples +70 -0
- data/bin/fennel-parity +4 -38
- data/examples/account-lockout.kap +11 -0
- data/examples/circle.kap +16 -0
- data/examples/convert-temperature.kap +14 -0
- data/examples/count-effects.kap +13 -0
- data/examples/falling-drops.kap +12 -0
- data/examples/fennel-parity-examples.txt +40 -0
- data/examples/max-achievable.kap +8 -0
- data/examples/mruby-runtime-examples.txt +89 -0
- data/examples/number-of-1-bits.kap +13 -0
- data/examples/number-of-steps.kap +15 -0
- data/examples/two-sum-hash.kap +11 -14
- data/examples/underscore-patterns.kap +1 -1
- data/lib/kapusta/cli.rb +11 -6
- data/lib/kapusta/compiler/emitter/bindings.rb +27 -2
- data/lib/kapusta/compiler/emitter/control_flow.rb +97 -14
- data/lib/kapusta/compiler/emitter/interop.rb +2 -0
- data/lib/kapusta/compiler/emitter/patterns.rb +125 -0
- data/lib/kapusta/compiler/emitter/support.rb +9 -2
- data/lib/kapusta/compiler/emitter.rb +2 -1
- data/lib/kapusta/compiler.rb +13 -4
- data/lib/kapusta/errors.rb +3 -0
- data/lib/kapusta/formatter.rb +0 -1
- data/lib/kapusta/version.rb +1 -1
- data/lib/kapusta.rb +2 -2
- data/spec/cli_spec.rb +35 -0
- data/spec/examples_spec.rb +115 -0
- data/spec/spec_helper.rb +9 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3cafef3668504ca08a40ba087c4c2b754bfda02e6f61161f19bcf8d56a254e00
|
|
4
|
+
data.tar.gz: 501b99f6ec3bfa950865b63b26ec6fd7f5b64977bb86f41d4eff51957602cc84
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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`
|
|
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 =
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
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]))
|
data/examples/circle.kap
ADDED
|
@@ -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,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))
|
data/examples/two-sum-hash.kap
CHANGED
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
(let [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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 {})))
|
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
|
-
|
|
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 =
|
|
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 =
|
|
93
|
+
body = emit_case_body(value_var, clauses, env, current_scope, mode)
|
|
75
94
|
emit_error!(:case_unsupported) unless body
|
|
76
|
-
[
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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]
|
data/lib/kapusta/compiler.rb
CHANGED
|
@@ -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
|
data/lib/kapusta/errors.rb
CHANGED
|
@@ -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',
|
data/lib/kapusta/formatter.rb
CHANGED
|
@@ -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)
|
data/lib/kapusta/version.rb
CHANGED
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
|
|
data/spec/examples_spec.rb
CHANGED
|
@@ -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.
|
|
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
|