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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 179b8b97d775c6bff99bd3d5ac3e1df1f6ab8c17637b6e16f9d688ade7f9dd06
4
- data.tar.gz: 899899c13c9b1d4f85216df5d3989807d874a9ec862be94d48989f851b2d8111
3
+ metadata.gz: f58b2e1f81723470a819ba38e3e6e811a3aa3a97ba79443e6ad0c59457d0fa08
4
+ data.tar.gz: 9207c1491218f71eeeedf2495a22805b9e2d1a8336200d212437fb813005c1d8
5
5
  SHA512:
6
- metadata.gz: 69247136466e119860e5fee232b0bb21fa40b9d23986f09051a4acccda476a9c42b5e3e63e1382367df6c3008876af2f6a69a9b6fd2bfc0e05e1c9e69ca6a8b0
7
- data.tar.gz: f65709e841e372a1dcc4c9a453d2251d58fb4fffbecd51fd1f0bcfb7c0423dc51109d6533e1b85d9b39bbc82cc994dcf64bdb5aad586c2b6476858db8e320d25
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
@@ -1,7 +1,22 @@
1
- (let [
2
- even-squares
3
- (-> [1 2 3 4 5 6]
4
- (: :select (fn [n] (n.even?)))
5
- (: :map (fn [n] (* n n))))
6
- ]
7
- (print (even-squares.join ", ")))
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,9 @@
1
+ (macro dbg [expr]
2
+ `(let [v# ,expr]
3
+ (print "dbg" v#)
4
+ v#))
5
+
6
+ (let [result (dbg (+ 1 2 3))]
7
+ (print "result" result))
8
+
9
+ (dbg (* 10 (+ 2 3)))
@@ -0,0 +1,12 @@
1
+ (macros {:my-min (fn [x y]
2
+ `(let [x# ,x
3
+ y# ,y]
4
+ (if (< x# y#) x# y#)))
5
+ :my-max (fn [x y]
6
+ `(let [x# ,x
7
+ y# ,y]
8
+ (if (< x# y#) y# x#)))})
9
+
10
+ (print (my-min 10 20))
11
+ (print (my-max 10 20))
12
+ (print (my-min 7 7))
@@ -0,0 +1,9 @@
1
+ (macro swap! [a b]
2
+ `(let [tmp# ,a]
3
+ (set ,a ,b)
4
+ (set ,b tmp#)))
5
+
6
+ (var x 1)
7
+ (var y 2)
8
+ (swap! x y)
9
+ (print x y)
@@ -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)
@@ -0,0 +1,7 @@
1
+ (macro unless [condition & body]
2
+ `(if (not ,condition)
3
+ (do
4
+ ,(unpack body))))
5
+
6
+ (unless false (print "shown") (print "also shown"))
7
+ (unless true (print "hidden"))
@@ -0,0 +1,7 @@
1
+ (macro when-let [[name val] & body]
2
+ `(let [,name ,val]
3
+ (when ,name ,(unpack body))))
4
+
5
+ (when-let [x (+ 1 2)] (print "got" x))
6
+ (when-let [y nil] (print "should not print"))
7
+ (print "done")
@@ -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) points] [:score (= user) points])
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 [values {"I" 1 "V" 5 "X" 10 "L" 50 "C" 100 "D" 500 "M" 1000}
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 (. values (. chars i))
9
- ahead (if (< (+ i 1) n) (. values (. chars (+ i 1))) 0)
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)))))
@@ -6,13 +6,8 @@
6
6
  [["O" _ _] ["O" _ _] ["O" _ _]] "O"
7
7
  _ "draw"))
8
8
 
9
- (each
10
- [
11
- _
12
- board
13
- (ipairs [
14
- [["X" "X" "X"] ["O" "" ""] ["" "O" ""]]
15
- [["O" "X" "X"] ["O" "" "X"] ["O" "" ""]]
16
- [["X" "O" ""] ["" "X" "O"] ["" "" "X"]]
17
- [["X" "O" "X"] ["O" "X" "O"] ["O" "X" "O"]]])]
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
- "#<Sym #{@name}>"
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
@@ -26,6 +26,9 @@ module Kapusta
26
26
  else
27
27
  run_file(args)
28
28
  end
29
+ rescue Kapusta::Error => e
30
+ warn e.formatted
31
+ exit 1
29
32
  end
30
33
 
31
34
  def self.parse_options(args)
@@ -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? { |item| item.is_a?(Sym) && !item.dotted? && item.name != '&' }
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
- value_code = emit_expr(items[i + 1], child_env, current_scope)
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!("cannot set method binding: #{target.name}") if method_binding?(binding)
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!("cannot set method binding: #{target.name}") if method_binding?(binding)
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!("bad set target: #{target.inspect}")
387
+ emit_error!(:bad_set_target, target: target.inspect)
342
388
  end
343
389
  else
344
- emit_error!("bad set target: #{target.inspect}")
390
+ emit_error!(:bad_set_target, target: target.inspect)
345
391
  end
346
392
  end
347
393
  end