kapusta 0.3.0 → 0.5.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 +1 -2
- data/bin/check-all +19 -0
- data/bin/fennel-parity +187 -0
- data/examples/even-squares.kap +22 -7
- data/examples/macros-dbg.kap +9 -0
- data/examples/macros-multi.kap +12 -0
- data/examples/macros-swap.kap +9 -0
- data/examples/macros-thrice-if.kap +18 -0
- data/examples/macros-unless.kap +7 -0
- data/examples/macros-when-let.kap +7 -0
- data/examples/packet-router.kap +2 -5
- data/examples/roman-to-integer.kap +3 -3
- data/examples/tic-tac-toe.kap +4 -9
- data/examples/ugly-number.kap +22 -0
- data/lib/kapusta/ast.rb +77 -1
- data/lib/kapusta/cli.rb +3 -0
- data/lib/kapusta/compiler/emitter/bindings.rb +52 -6
- data/lib/kapusta/compiler/emitter/collections.rb +64 -20
- data/lib/kapusta/compiler/emitter/control_flow.rb +7 -4
- data/lib/kapusta/compiler/emitter/expressions.rb +58 -13
- data/lib/kapusta/compiler/emitter/interop.rb +6 -4
- data/lib/kapusta/compiler/emitter/patterns.rb +29 -12
- data/lib/kapusta/compiler/emitter/support.rb +46 -23
- data/lib/kapusta/compiler/macro_expander.rb +286 -0
- data/lib/kapusta/compiler/normalizer.rb +35 -9
- data/lib/kapusta/compiler.rb +13 -1
- data/lib/kapusta/error.rb +25 -1
- data/lib/kapusta/errors.rb +69 -0
- data/lib/kapusta/formatter.rb +228 -92
- data/lib/kapusta/reader.rb +80 -22
- data/lib/kapusta/version.rb +1 -1
- data/lib/kapusta.rb +6 -5
- data/spec/examples_errors_spec.rb +229 -0
- data/spec/examples_spec.rb +51 -0
- data/spec/formatter_spec.rb +15 -16
- metadata +13 -2
- data/spec/reader_spec.rb +0 -26
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f58b2e1f81723470a819ba38e3e6e811a3aa3a97ba79443e6ad0c59457d0fa08
|
|
4
|
+
data.tar.gz: 9207c1491218f71eeeedf2495a22805b9e2d1a8336200d212437fb813005c1d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 9e0e2220a388a1a06ed86733e01fbc1eacf3a48536c503e7e8ccbcf85871fcda35cce16ddc9b434f3d1ac3c8dc23cb15af3d78528fc269fae62d25ef49a6fba2
|
|
7
|
+
data.tar.gz: 3a0f8628514de972b1def03cfd8f47798b630632fe728a41d46a0c9aade7587c011004695903697b929fd82c3302bf90c7efdc7d5e4edb7f0ab7bd3c9c1c7387
|
data/README.md
CHANGED
|
@@ -49,7 +49,7 @@ See `examples/bank-account.kap` and `examples/use_bank_account.rb`.
|
|
|
49
49
|
|
|
50
50
|
## Examples
|
|
51
51
|
|
|
52
|
-
See [`examples/`](https://github.com/evmorov/kapusta/tree/main/examples).
|
|
52
|
+
See [`examples/`](https://github.com/evmorov/kapusta/tree/main/examples/) and [`examples-compiled/`](https://github.com/evmorov/kapusta/tree/main/examples-compiled/).
|
|
53
53
|
|
|
54
54
|
```fennel
|
|
55
55
|
(fn ack [m n]
|
|
@@ -90,7 +90,6 @@ Kapusta keeps most core Fennel forms. The main differences come from Ruby's runt
|
|
|
90
90
|
| `values` uses Lua multiple returns | `values` lowers to a Ruby array, usually destructured |
|
|
91
91
|
| `(print x)` is Lua's `print` (bare) | `(print x)` is Ruby's `p` (inspect-style) |
|
|
92
92
|
| `with-open`, `tail!` | not provided |
|
|
93
|
-
| macros | not provided for now |
|
|
94
93
|
|
|
95
94
|
Kapusta-specific additions:
|
|
96
95
|
|
data/bin/check-all
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
echo "== rspec =="
|
|
5
|
+
bundle exec rspec
|
|
6
|
+
|
|
7
|
+
echo "== rubocop -A =="
|
|
8
|
+
bundle exec rubocop -A
|
|
9
|
+
|
|
10
|
+
echo "== kapfmt =="
|
|
11
|
+
for file in examples/*.kap; do ./exe/kapfmt --fix "$file"; done
|
|
12
|
+
|
|
13
|
+
echo "== fennel parity =="
|
|
14
|
+
bin/fennel-parity
|
|
15
|
+
|
|
16
|
+
echo "== compile examples =="
|
|
17
|
+
bin/compile-examples
|
|
18
|
+
|
|
19
|
+
echo "== Success! =="
|
data/bin/fennel-parity
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require 'open3'
|
|
5
|
+
|
|
6
|
+
ROOT = File.expand_path('..', __dir__)
|
|
7
|
+
EXAMPLES = File.join(ROOT, 'examples')
|
|
8
|
+
EXAMPLES_ERRORS = File.join(ROOT, 'examples-errors')
|
|
9
|
+
KAPUSTA = File.join(ROOT, 'exe', 'kapusta')
|
|
10
|
+
KAPFMT = File.join(ROOT, 'exe', 'kapfmt')
|
|
11
|
+
|
|
12
|
+
COMPATIBLE = %w[
|
|
13
|
+
ackermann.kap
|
|
14
|
+
anonymous-greeter.kap
|
|
15
|
+
climbing-stairs.kap
|
|
16
|
+
describe.kap
|
|
17
|
+
destructure.kap
|
|
18
|
+
even-squares.kap
|
|
19
|
+
factorial.kap
|
|
20
|
+
fib.kap
|
|
21
|
+
fizzbuzz.kap
|
|
22
|
+
gcd.kap
|
|
23
|
+
hashfn.kap
|
|
24
|
+
leap-year.kap
|
|
25
|
+
macros-dbg.kap
|
|
26
|
+
macros-multi.kap
|
|
27
|
+
macros-swap.kap
|
|
28
|
+
macros-thrice-if.kap
|
|
29
|
+
macros-unless.kap
|
|
30
|
+
macros-when-let.kap
|
|
31
|
+
match.kap
|
|
32
|
+
min-max.kap
|
|
33
|
+
or-patterns.kap
|
|
34
|
+
packet-router.kap
|
|
35
|
+
points.kap
|
|
36
|
+
primes.kap
|
|
37
|
+
safe-lookup.kap
|
|
38
|
+
shapes.kap
|
|
39
|
+
squares.kap
|
|
40
|
+
sum.kap
|
|
41
|
+
tic-tac-toe.kap
|
|
42
|
+
underscore-patterns.kap
|
|
43
|
+
].freeze
|
|
44
|
+
|
|
45
|
+
def run(cmd, file, chdir: EXAMPLES)
|
|
46
|
+
out, err, status = Open3.capture3(cmd, file, chdir:)
|
|
47
|
+
[out, err, status]
|
|
48
|
+
rescue StandardError => e
|
|
49
|
+
['', e.message, nil]
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def strip_outer_quotes(line)
|
|
53
|
+
if line.length >= 2 && line.start_with?('"') && line.end_with?('"')
|
|
54
|
+
line[1..-2]
|
|
55
|
+
else
|
|
56
|
+
line
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def normalize_kapusta(out)
|
|
61
|
+
out.lines.map { |l| strip_outer_quotes(l.chomp) }
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def normalize_fennel(out)
|
|
65
|
+
# Lua's `print` joins multiple args with TAB on one line; Ruby's `p`
|
|
66
|
+
# prints each on its own line. Split tabs so the two layouts line up.
|
|
67
|
+
out.lines.flat_map { |l| l.chomp.split("\t") }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def unified_diff(label_kap, kap_lines, label_fnl, fnl_lines)
|
|
71
|
+
max = [kap_lines.length, fnl_lines.length].max
|
|
72
|
+
rows = []
|
|
73
|
+
(0...max).each do |i|
|
|
74
|
+
la = kap_lines[i]
|
|
75
|
+
lb = fnl_lines[i]
|
|
76
|
+
next if la == lb
|
|
77
|
+
|
|
78
|
+
rows << format(' line %<n>3d %<a>s: %<la>s',
|
|
79
|
+
n: i + 1, a: label_kap, la: la.inspect)
|
|
80
|
+
rows << format(' %<b>s: %<lb>s',
|
|
81
|
+
b: label_fnl, lb: lb.inspect)
|
|
82
|
+
end
|
|
83
|
+
rows.join("\n")
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def check_format(path)
|
|
87
|
+
k_out, k_err, k_status = run(KAPFMT, path)
|
|
88
|
+
f_out, f_err, f_status = run('fnlfmt', path)
|
|
89
|
+
|
|
90
|
+
return "kapfmt exited #{k_status&.exitstatus}: #{k_err.strip}" if k_status.nil? || !k_status.success?
|
|
91
|
+
return "fnlfmt exited #{f_status&.exitstatus}: #{f_err.strip}" if f_status.nil? || !f_status.success?
|
|
92
|
+
return if k_out == f_out
|
|
93
|
+
|
|
94
|
+
k_lines = k_out.lines.map(&:chomp)
|
|
95
|
+
f_lines = f_out.lines.map(&:chomp)
|
|
96
|
+
"format differs:\n#{unified_diff('kapfmt', k_lines, 'fnlfmt', f_lines)}"
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def check_run(path)
|
|
100
|
+
k_out, k_err, k_status = run(KAPUSTA, path)
|
|
101
|
+
f_out, f_err, f_status = run('fennel', path)
|
|
102
|
+
|
|
103
|
+
return "kapusta exited #{k_status&.exitstatus}: #{k_err.strip}" if k_status.nil? || !k_status.success?
|
|
104
|
+
return "fennel exited #{f_status&.exitstatus}: #{f_err.strip}" if f_status.nil? || !f_status.success?
|
|
105
|
+
|
|
106
|
+
k_lines = normalize_kapusta(k_out)
|
|
107
|
+
f_lines = normalize_fennel(f_out)
|
|
108
|
+
return if k_lines == f_lines
|
|
109
|
+
|
|
110
|
+
if k_lines.length == f_lines.length
|
|
111
|
+
"output differs:\n#{unified_diff('kapusta', k_lines, 'fennel', f_lines)}"
|
|
112
|
+
else
|
|
113
|
+
"line count differs (kapusta=#{k_lines.length}, fennel=#{f_lines.length})"
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def check(name)
|
|
118
|
+
path = File.join(EXAMPLES, name)
|
|
119
|
+
return [:miss, "missing file: #{path}"] unless File.exist?(path)
|
|
120
|
+
|
|
121
|
+
fmt_reason = check_format(path)
|
|
122
|
+
return [:fail, fmt_reason] if fmt_reason
|
|
123
|
+
|
|
124
|
+
run_reason = check_run(path)
|
|
125
|
+
return [:fail, run_reason] if run_reason
|
|
126
|
+
|
|
127
|
+
[:ok, nil]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def check_error(name)
|
|
131
|
+
path = File.join(EXAMPLES_ERRORS, name)
|
|
132
|
+
_, _, k_status = run(KAPUSTA, path, chdir: EXAMPLES_ERRORS)
|
|
133
|
+
_, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS)
|
|
134
|
+
|
|
135
|
+
return [:fail, 'fennel unexpectedly succeeded'] if f_status&.success?
|
|
136
|
+
return [:fail, "kapusta unexpectedly succeeded (exit #{k_status&.exitstatus})"] if k_status&.success?
|
|
137
|
+
|
|
138
|
+
[:ok, nil]
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
puts "Checking #{COMPATIBLE.size} compatible examples (kapfmt vs fnlfmt, kapusta vs fennel)...\n\n"
|
|
142
|
+
|
|
143
|
+
failures = []
|
|
144
|
+
COMPATIBLE.each do |name|
|
|
145
|
+
status, reason = check(name)
|
|
146
|
+
case status
|
|
147
|
+
when :ok
|
|
148
|
+
puts "OK #{name}"
|
|
149
|
+
when :miss
|
|
150
|
+
failures << [name, reason]
|
|
151
|
+
puts "MISS #{name}"
|
|
152
|
+
when :fail
|
|
153
|
+
failures << [name, reason]
|
|
154
|
+
puts "FAIL #{name}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
error_files = Dir.children(EXAMPLES_ERRORS).select { |n| n.end_with?('.kap') }.sort
|
|
159
|
+
puts "\nChecking #{error_files.size} error examples (both kapusta and fennel must fail)...\n\n"
|
|
160
|
+
|
|
161
|
+
error_failures = []
|
|
162
|
+
error_files.each do |name|
|
|
163
|
+
status, reason = check_error(name)
|
|
164
|
+
case status
|
|
165
|
+
when :ok
|
|
166
|
+
puts "OK #{name}"
|
|
167
|
+
when :fail
|
|
168
|
+
error_failures << [name, reason]
|
|
169
|
+
puts "FAIL #{name}"
|
|
170
|
+
end
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
puts
|
|
174
|
+
total_failures = failures + error_failures
|
|
175
|
+
total = COMPATIBLE.size + error_files.size
|
|
176
|
+
if total_failures.empty?
|
|
177
|
+
puts "All #{total} examples (#{COMPATIBLE.size} compatible + #{error_files.size} error) match expectations."
|
|
178
|
+
exit 0
|
|
179
|
+
else
|
|
180
|
+
puts "#{total_failures.size} of #{total} examples failed:\n\n"
|
|
181
|
+
total_failures.each do |name, reason|
|
|
182
|
+
puts " * #{name}"
|
|
183
|
+
reason.each_line { |l| puts " #{l.rstrip}" }
|
|
184
|
+
puts
|
|
185
|
+
end
|
|
186
|
+
exit 1
|
|
187
|
+
end
|
data/examples/even-squares.kap
CHANGED
|
@@ -1,7 +1,22 @@
|
|
|
1
|
-
(
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
(fn even? [n] (= 0 (% n 2)))
|
|
2
|
+
|
|
3
|
+
(fn select [tbl pred]
|
|
4
|
+
(icollect [_ x (ipairs tbl)]
|
|
5
|
+
(when (pred x) x)))
|
|
6
|
+
|
|
7
|
+
(fn map [tbl f]
|
|
8
|
+
(icollect [_ x (ipairs tbl)]
|
|
9
|
+
(f x)))
|
|
10
|
+
|
|
11
|
+
(fn join [tbl sep]
|
|
12
|
+
(var s "")
|
|
13
|
+
(each [_ x (ipairs tbl)]
|
|
14
|
+
(if (= s "")
|
|
15
|
+
(set s (.. x))
|
|
16
|
+
(set s (.. s sep x))))
|
|
17
|
+
s)
|
|
18
|
+
|
|
19
|
+
(let [xs [1 2 3 4 5 6]
|
|
20
|
+
filtered (select xs even?)
|
|
21
|
+
squared (map filtered #(* $ $))]
|
|
22
|
+
(print (join squared ", ")))
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
(macro thrice-if [condition result]
|
|
2
|
+
(fn step [i]
|
|
3
|
+
(if (< 0 i)
|
|
4
|
+
`(if ,condition
|
|
5
|
+
(do
|
|
6
|
+
,result
|
|
7
|
+
,(step (- i 1))))))
|
|
8
|
+
|
|
9
|
+
(step 3))
|
|
10
|
+
|
|
11
|
+
(var counter 0)
|
|
12
|
+
|
|
13
|
+
(fn ready? [] (< counter 5))
|
|
14
|
+
|
|
15
|
+
(fn tick [] (set counter (+ counter 1)) (print "tick" counter))
|
|
16
|
+
|
|
17
|
+
(thrice-if (ready?) (tick))
|
|
18
|
+
(print "final" counter)
|
data/examples/packet-router.kap
CHANGED
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
(fn inbox-line [user event]
|
|
2
2
|
(match event
|
|
3
3
|
[:score user points] (.. "score:" points)
|
|
4
|
-
[:profile user ?city] (if city (.. "city:" city) "city:nil")
|
|
4
|
+
[:profile user ?city] (if ?city (.. "city:" ?city) "city:nil")
|
|
5
5
|
_ "other"))
|
|
6
6
|
|
|
7
7
|
(fn score-delta [user event]
|
|
8
8
|
(case event
|
|
9
|
-
(where (or [:bonus (= user)
|
|
10
|
-
(> points 0)
|
|
11
|
-
(< points 10))
|
|
12
|
-
points
|
|
9
|
+
(where (or [:bonus (= user) p] [:score (= user) p]) (> p 0) (< p 10)) p
|
|
13
10
|
_ 0))
|
|
14
11
|
|
|
15
12
|
(fn packet-kind [packet]
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
(fn roman-to-integer [s]
|
|
2
|
-
(let [
|
|
2
|
+
(let [value-map {"I" 1 "V" 5 "X" 10 "L" 50 "C" 100 "D" 500 "M" 1000}
|
|
3
3
|
chars (s.chars)
|
|
4
4
|
n (length chars)]
|
|
5
5
|
(var total 0)
|
|
6
6
|
(var i 0)
|
|
7
7
|
(while (< i n)
|
|
8
|
-
(let [curr (.
|
|
9
|
-
ahead (if (< (+ i 1) n) (.
|
|
8
|
+
(let [curr (. value-map (. chars i))
|
|
9
|
+
ahead (if (< (+ i 1) n) (. value-map (. chars (+ i 1))) 0)
|
|
10
10
|
subtract? (< curr ahead)]
|
|
11
11
|
(set total (+ total (if subtract? (- ahead curr) curr)))
|
|
12
12
|
(set i (+ i (if subtract? 2 1)))))
|
data/examples/tic-tac-toe.kap
CHANGED
|
@@ -6,13 +6,8 @@
|
|
|
6
6
|
[["O" _ _] ["O" _ _] ["O" _ _]] "O"
|
|
7
7
|
_ "draw"))
|
|
8
8
|
|
|
9
|
-
(each
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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"]]])]
|
|
9
|
+
(each [_ board (ipairs [[["X" "X" "X"] ["O" "" ""] ["" "O" ""]]
|
|
10
|
+
[["O" "X" "X"] ["O" "" "X"] ["O" "" ""]]
|
|
11
|
+
[["X" "O" ""] ["" "X" "O"] ["" "" "X"]]
|
|
12
|
+
[["X" "O" "X"] ["O" "X" "O"] ["O" "X" "O"]]])]
|
|
18
13
|
(print (winner board)))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
(macro floor-div [a b]
|
|
2
|
+
`(: (/ ,a ,b) :floor))
|
|
3
|
+
|
|
4
|
+
(macro divide-out! [v d]
|
|
5
|
+
`(while (= 0 (% ,v ,d))
|
|
6
|
+
(set ,v (floor-div ,v ,d))))
|
|
7
|
+
|
|
8
|
+
(fn ugly? [n]
|
|
9
|
+
(var x n)
|
|
10
|
+
(if (<= n 0)
|
|
11
|
+
false
|
|
12
|
+
(do
|
|
13
|
+
(divide-out! x 2)
|
|
14
|
+
(divide-out! x 3)
|
|
15
|
+
(divide-out! x 5)
|
|
16
|
+
(= x 1))))
|
|
17
|
+
|
|
18
|
+
(print (ugly? 6))
|
|
19
|
+
(print (ugly? 1))
|
|
20
|
+
(print (ugly? 14))
|
|
21
|
+
(print (ugly? 0))
|
|
22
|
+
(print (ugly? 30))
|
data/lib/kapusta/ast.rb
CHANGED
|
@@ -9,8 +9,11 @@ module Kapusta
|
|
|
9
9
|
end
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
+
BlankLine = Class.new
|
|
13
|
+
|
|
12
14
|
class Sym
|
|
13
15
|
attr_reader :name
|
|
16
|
+
attr_accessor :line, :column
|
|
14
17
|
|
|
15
18
|
def initialize(name)
|
|
16
19
|
@name = name.to_s
|
|
@@ -21,7 +24,7 @@ module Kapusta
|
|
|
21
24
|
end
|
|
22
25
|
|
|
23
26
|
def inspect
|
|
24
|
-
|
|
27
|
+
@name.to_s
|
|
25
28
|
end
|
|
26
29
|
|
|
27
30
|
def ==(other)
|
|
@@ -74,17 +77,33 @@ module Kapusta
|
|
|
74
77
|
|
|
75
78
|
class Vec
|
|
76
79
|
attr_reader :items
|
|
80
|
+
attr_accessor :multiline_source, :line, :column
|
|
77
81
|
|
|
78
82
|
def initialize(items)
|
|
79
83
|
@items = items
|
|
84
|
+
@multiline_source = false
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def to_ary
|
|
88
|
+
@items
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def inspect
|
|
92
|
+
"[#{@items.map(&:inspect).join(' ')}]"
|
|
80
93
|
end
|
|
81
94
|
end
|
|
82
95
|
|
|
83
96
|
class HashLit
|
|
84
97
|
attr_reader :entries
|
|
98
|
+
attr_accessor :multiline_source, :line, :column
|
|
85
99
|
|
|
86
100
|
def initialize(entries)
|
|
87
101
|
@entries = entries
|
|
102
|
+
@multiline_source = false
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def inspect
|
|
106
|
+
pairs.map { |k, v| "#{k.inspect} #{v.inspect}" }.then { |p| "{#{p.join(' ')}}" }
|
|
88
107
|
end
|
|
89
108
|
|
|
90
109
|
def pairs
|
|
@@ -98,9 +117,11 @@ module Kapusta
|
|
|
98
117
|
|
|
99
118
|
class List
|
|
100
119
|
attr_reader :items
|
|
120
|
+
attr_accessor :multiline_source, :line, :column
|
|
101
121
|
|
|
102
122
|
def initialize(items)
|
|
103
123
|
@items = items
|
|
124
|
+
@multiline_source = false
|
|
104
125
|
end
|
|
105
126
|
|
|
106
127
|
def head
|
|
@@ -114,5 +135,60 @@ module Kapusta
|
|
|
114
135
|
def empty?
|
|
115
136
|
@items.empty?
|
|
116
137
|
end
|
|
138
|
+
|
|
139
|
+
def inspect
|
|
140
|
+
"(#{@items.map(&:inspect).join(' ')})"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class AutoGensym < Sym
|
|
145
|
+
def inspect
|
|
146
|
+
"#<AutoGensym #{@name}#>"
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
class MacroSym < Sym
|
|
151
|
+
def inspect
|
|
152
|
+
"#<MacroSym #{@name}>"
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
class Quasiquote
|
|
157
|
+
attr_reader :form
|
|
158
|
+
attr_accessor :line, :column
|
|
159
|
+
|
|
160
|
+
def initialize(form)
|
|
161
|
+
@form = form
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def inspect
|
|
165
|
+
"`#{@form.inspect}"
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
class Unquote
|
|
170
|
+
attr_reader :form
|
|
171
|
+
attr_accessor :line, :column
|
|
172
|
+
|
|
173
|
+
def initialize(form)
|
|
174
|
+
@form = form
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def inspect
|
|
178
|
+
",#{@form.inspect}"
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
class UnquoteSplice
|
|
183
|
+
attr_reader :form
|
|
184
|
+
attr_accessor :line, :column
|
|
185
|
+
|
|
186
|
+
def initialize(form)
|
|
187
|
+
@form = form
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def inspect
|
|
191
|
+
",@#{@form.inspect}"
|
|
192
|
+
end
|
|
117
193
|
end
|
|
118
194
|
end
|
data/lib/kapusta/cli.rb
CHANGED
|
@@ -13,6 +13,8 @@ module Kapusta
|
|
|
13
13
|
name_sym = args[0]
|
|
14
14
|
pattern = args[1]
|
|
15
15
|
body = args[2..]
|
|
16
|
+
emit_error!(:fn_no_params) unless name_sym.is_a?(Sym) && pattern.is_a?(Vec)
|
|
17
|
+
|
|
16
18
|
fn_env = env.child
|
|
17
19
|
ruby_name = define_local(fn_env, name_sym.name)
|
|
18
20
|
<<~RUBY.chomp
|
|
@@ -59,7 +61,9 @@ module Kapusta
|
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
def simple_parameter_pattern?(pattern)
|
|
62
|
-
pattern.is_a?(Vec) && pattern.items.all?
|
|
64
|
+
pattern.is_a?(Vec) && pattern.items.all? do |item|
|
|
65
|
+
item.is_a?(Sym) && (!item.dotted? || item.name == '...') && item.name != '&'
|
|
66
|
+
end
|
|
63
67
|
end
|
|
64
68
|
|
|
65
69
|
def emit_definition_form(form, env, current_scope)
|
|
@@ -227,6 +231,9 @@ module Kapusta
|
|
|
227
231
|
|
|
228
232
|
def emit_let_parts(args, env, current_scope, result:)
|
|
229
233
|
bindings = args[0]
|
|
234
|
+
emit_error!(:let_odd_bindings) if bindings.items.length.odd?
|
|
235
|
+
emit_error!(:let_no_body) if args.length < 2
|
|
236
|
+
|
|
230
237
|
body = args[1..]
|
|
231
238
|
child_env = env.child
|
|
232
239
|
binding_codes = []
|
|
@@ -234,7 +241,9 @@ module Kapusta
|
|
|
234
241
|
i = 0
|
|
235
242
|
while i < items.length
|
|
236
243
|
pattern = items[i]
|
|
237
|
-
|
|
244
|
+
value_form = items[i + 1]
|
|
245
|
+
check_destructure_value!(pattern, value_form)
|
|
246
|
+
value_code = emit_expr(value_form, child_env, current_scope)
|
|
238
247
|
bind_code, child_env = emit_pattern_bind(pattern, value_code, child_env)
|
|
239
248
|
binding_codes << bind_code
|
|
240
249
|
i += 2
|
|
@@ -250,11 +259,15 @@ module Kapusta
|
|
|
250
259
|
end
|
|
251
260
|
|
|
252
261
|
def emit_local_form(form, env, current_scope)
|
|
262
|
+
emit_error!(:local_arity, form: form.head.name) unless form.items.length == 3
|
|
263
|
+
|
|
253
264
|
target = form.items[1]
|
|
254
265
|
value_code = emit_expr(form.items[2], env, current_scope)
|
|
255
266
|
|
|
256
267
|
if target.is_a?(Sym)
|
|
268
|
+
validate_binding_symbol!(target)
|
|
257
269
|
ruby_name = define_local(env, target.name)
|
|
270
|
+
mark_mutability(env, target.name, mutable: form.head.name == 'var')
|
|
258
271
|
["#{ruby_name} = #{value_code}\nnil", env]
|
|
259
272
|
else
|
|
260
273
|
bind_code, env = emit_pattern_bind(target, value_code, env)
|
|
@@ -262,11 +275,43 @@ module Kapusta
|
|
|
262
275
|
end
|
|
263
276
|
end
|
|
264
277
|
|
|
278
|
+
def check_destructure_value!(pattern, value_form)
|
|
279
|
+
return unless pattern.is_a?(Vec) || pattern.is_a?(HashLit)
|
|
280
|
+
|
|
281
|
+
case value_form
|
|
282
|
+
when String, Numeric, Symbol, true, false
|
|
283
|
+
emit_error!(:could_not_destructure_literal)
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def mark_mutability(env, name, mutable:)
|
|
288
|
+
@binding_mutability ||= {}
|
|
289
|
+
ruby_name = env.lookup(name)
|
|
290
|
+
@binding_mutability[ruby_name] = mutable
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def mutable_binding?(env, name)
|
|
294
|
+
ruby_name = env.lookup_if_defined(name)
|
|
295
|
+
return false unless ruby_name
|
|
296
|
+
|
|
297
|
+
(@binding_mutability ||= {}).fetch(ruby_name, true)
|
|
298
|
+
end
|
|
299
|
+
|
|
265
300
|
def emit_local_expr(args, env, current_scope)
|
|
266
301
|
code, = emit_local_form(List.new([Sym.new('local'), *args]), env.child, current_scope)
|
|
267
302
|
"(-> do\n#{indent(code)}\nend).call"
|
|
268
303
|
end
|
|
269
304
|
|
|
305
|
+
def emit_global_expr(args, _env, _current_scope)
|
|
306
|
+
emit_error!(:global_arity) unless args.length == 2
|
|
307
|
+
unless args[0].is_a?(Sym)
|
|
308
|
+
emit_error!(:global_non_symbol_name, type: args[0].class.name.downcase, value: args[0].inspect)
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
name = args[0].name
|
|
312
|
+
"$#{global_name(name)} = #{emit_expr(args[1], Env.new, :toplevel)}\nnil"
|
|
313
|
+
end
|
|
314
|
+
|
|
270
315
|
def emit_set_form(form, env, current_scope)
|
|
271
316
|
target = form.items[1]
|
|
272
317
|
value_code = emit_expr(form.items[2], env, current_scope)
|
|
@@ -275,7 +320,7 @@ module Kapusta
|
|
|
275
320
|
binding = env.lookup_if_defined(target.name)
|
|
276
321
|
ruby_name =
|
|
277
322
|
if binding
|
|
278
|
-
emit_error!(
|
|
323
|
+
emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
|
|
279
324
|
|
|
280
325
|
binding
|
|
281
326
|
else
|
|
@@ -319,7 +364,8 @@ module Kapusta
|
|
|
319
364
|
end
|
|
320
365
|
else
|
|
321
366
|
binding = env.lookup(target.name)
|
|
322
|
-
emit_error!(
|
|
367
|
+
emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
|
|
368
|
+
emit_error!(:expected_var, name: target.name) unless mutable_binding?(env, target.name)
|
|
323
369
|
|
|
324
370
|
emit_assignment(binding, value_code)
|
|
325
371
|
end
|
|
@@ -338,10 +384,10 @@ module Kapusta
|
|
|
338
384
|
elsif head.is_a?(Sym) && head.name == 'gvar'
|
|
339
385
|
emit_assignment("$#{global_name(target.items[1].name)}", value_code)
|
|
340
386
|
else
|
|
341
|
-
emit_error!(
|
|
387
|
+
emit_error!(:bad_set_target, target: target.inspect)
|
|
342
388
|
end
|
|
343
389
|
else
|
|
344
|
-
emit_error!(
|
|
390
|
+
emit_error!(:bad_set_target, target: target.inspect)
|
|
345
391
|
end
|
|
346
392
|
end
|
|
347
393
|
end
|