kapusta 0.4.1 → 0.7.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: 01cc980a37ff63b56db021edb64f7ed9b4381569bb82e8e4ada099a2b17ba19e
4
- data.tar.gz: 035f0f4a38d5d9fad65f4a53e8343bbcad4b681bd33dfa975f7c9c638a83ccb4
3
+ metadata.gz: 76f35530eebc269dfec62ee1f36f6cc11a252ee4590eeac961c9b47e4e49ca09
4
+ data.tar.gz: a0ee60d21fb9a6066915d916e465bef09d9208c0469712673db35bea83efd7c4
5
5
  SHA512:
6
- metadata.gz: caaeb31630a5c80607fe6c2907548c2874f177add0bdbd3555e72039dd598aaf481f91778043f6b81cd511c9ac8609e48f61c474766dac9b3e8a2fa41d408970
7
- data.tar.gz: 5417ad8088be602618c3662660af820be6dbf15eb27d8300b1c542e3889fbfe1813d51ca7306f988a630616d1857f333141bf20b964d0869c2caa8c3eba869e3
6
+ metadata.gz: 350e111e7cfe045723ea1982c18762727d64ea615219d904a79af2933874af5ce355076e242ed9c0bf5e91d135dbd4498b2b5ae107b05035f205e3ccb87bbb54
7
+ data.tar.gz: 7e242c3fb28fb0d5d2839f7e3d6e5dca96dda90204e1a81024111e1fc5ef4f6c7abb2a6e3628e5894eac46ce9d1c10f21db80bcae63da3ec9b4874a747b1bba1
data/README.md CHANGED
@@ -14,11 +14,21 @@ For more information about Kapusta, see the official Fennel documentation and tu
14
14
  2. Compiled `.rb` files don't depend on Kapusta. Run with plain `ruby`, or load `.kap` files at runtime via `require 'kapusta'`.
15
15
  3. Two-way Ruby interop.
16
16
 
17
- ## Usage
17
+ ## Install
18
18
 
19
19
  ```
20
20
  gem install kapusta
21
- kapfmt --fix examples/fizzbuzz.kap
21
+ ```
22
+
23
+ It installs three executables:
24
+
25
+ 1. `kapusta`
26
+ 2. `kapfmt`
27
+ 3. `kapusta-ls`
28
+
29
+ ## Use
30
+
31
+ ```
22
32
  kapusta examples/fizzbuzz.kap
23
33
  ```
24
34
 
@@ -35,7 +45,7 @@ exe/kapusta --compile examples/fizzbuzz.kap > examples/fizzbuzz.rb
35
45
  ruby examples/fizzbuzz.rb
36
46
  ```
37
47
 
38
- ## Using from Ruby
48
+ ## Use from Ruby
39
49
 
40
50
  Ruby can require a `.kap` file and use it directly.
41
51
 
@@ -100,12 +110,20 @@ Kapusta-specific additions:
100
110
  - a trailing symbol-keyed hash is emitted as Ruby keyword arguments
101
111
  - a final function literal argument is emitted as a Ruby block
102
112
 
103
- ## Formatting
113
+ ## Format
104
114
 
105
115
  ```
106
- exe/kapfmt
116
+ kapfmt --fix examples/fizzbuzz.kap
107
117
  ```
108
118
 
119
+ ## LSP
120
+
121
+ Use `kapusta-ls` in the editor of your choice.
122
+
109
123
  ## Syntax highlight
110
124
 
111
- For Vim you can use https://git.sr.ht/~m15a/vim-fennel-syntax
125
+ For Vim, you can use https://git.sr.ht/~m15a/vim-fennel-syntax
126
+
127
+ ## License
128
+
129
+ [MIT](LICENSE)
data/bin/fennel-parity CHANGED
@@ -5,15 +5,18 @@ require 'open3'
5
5
 
6
6
  ROOT = File.expand_path('..', __dir__)
7
7
  EXAMPLES = File.join(ROOT, 'examples')
8
+ EXAMPLES_ERRORS = File.join(ROOT, 'examples-errors')
8
9
  KAPUSTA = File.join(ROOT, 'exe', 'kapusta')
9
10
  KAPFMT = File.join(ROOT, 'exe', 'kapfmt')
10
11
 
11
12
  COMPATIBLE = %w[
12
13
  ackermann.kap
13
14
  anonymous-greeter.kap
15
+ classify-wallet.kap
14
16
  climbing-stairs.kap
15
17
  describe.kap
16
18
  destructure.kap
19
+ even-squares.kap
17
20
  factorial.kap
18
21
  fib.kap
19
22
  fizzbuzz.kap
@@ -29,6 +32,7 @@ COMPATIBLE = %w[
29
32
  match.kap
30
33
  min-max.kap
31
34
  or-patterns.kap
35
+ power-of-three.kap
32
36
  packet-router.kap
33
37
  points.kap
34
38
  primes.kap
@@ -40,8 +44,8 @@ COMPATIBLE = %w[
40
44
  underscore-patterns.kap
41
45
  ].freeze
42
46
 
43
- def run(cmd, file)
44
- out, err, status = Open3.capture3(cmd, file, chdir: EXAMPLES)
47
+ def run(cmd, file, chdir: EXAMPLES)
48
+ out, err, status = Open3.capture3(cmd, file, chdir:)
45
49
  [out, err, status]
46
50
  rescue StandardError => e
47
51
  ['', e.message, nil]
@@ -125,6 +129,17 @@ def check(name)
125
129
  [:ok, nil]
126
130
  end
127
131
 
132
+ def check_error(name)
133
+ path = File.join(EXAMPLES_ERRORS, name)
134
+ _, _, k_status = run(KAPUSTA, path, chdir: EXAMPLES_ERRORS)
135
+ _, _, f_status = run('fennel', path, chdir: EXAMPLES_ERRORS)
136
+
137
+ return [:fail, 'fennel unexpectedly succeeded'] if f_status&.success?
138
+ return [:fail, "kapusta unexpectedly succeeded (exit #{k_status&.exitstatus})"] if k_status&.success?
139
+
140
+ [:ok, nil]
141
+ end
142
+
128
143
  puts "Checking #{COMPATIBLE.size} compatible examples (kapfmt vs fnlfmt, kapusta vs fennel)...\n\n"
129
144
 
130
145
  failures = []
@@ -142,13 +157,30 @@ COMPATIBLE.each do |name|
142
157
  end
143
158
  end
144
159
 
160
+ error_files = Dir.children(EXAMPLES_ERRORS).select { |n| n.end_with?('.kap') }.sort
161
+ puts "\nChecking #{error_files.size} error examples (both kapusta and fennel must fail)...\n\n"
162
+
163
+ error_failures = []
164
+ error_files.each do |name|
165
+ status, reason = check_error(name)
166
+ case status
167
+ when :ok
168
+ puts "OK #{name}"
169
+ when :fail
170
+ error_failures << [name, reason]
171
+ puts "FAIL #{name}"
172
+ end
173
+ end
174
+
145
175
  puts
146
- if failures.empty?
147
- puts "All #{COMPATIBLE.size} compatible examples produce matching output."
176
+ total_failures = failures + error_failures
177
+ total = COMPATIBLE.size + error_files.size
178
+ if total_failures.empty?
179
+ puts "All #{total} examples (#{COMPATIBLE.size} compatible + #{error_files.size} error) match expectations."
148
180
  exit 0
149
181
  else
150
- puts "#{failures.size} of #{COMPATIBLE.size} examples failed:\n\n"
151
- failures.each do |name, reason|
182
+ puts "#{total_failures.size} of #{total} examples failed:\n\n"
183
+ total_failures.each do |name, reason|
152
184
  puts " * #{name}"
153
185
  reason.each_line { |l| puts " #{l.rstrip}" }
154
186
  puts
@@ -0,0 +1,11 @@
1
+ (fn classify-wallet [wallet]
2
+ (case wallet
3
+ {1 a 25 b} (.. "pennies-" a "-quarters-" b)
4
+ {25 q} (.. "quarters-only-" q)
5
+ {1 p} (.. "pennies-only-" p)
6
+ _ "mixed"))
7
+
8
+ (print (classify-wallet {1 5 25 2}))
9
+ (print (classify-wallet {25 4}))
10
+ (print (classify-wallet {1 10}))
11
+ (print (classify-wallet {5 3 10 2}))
@@ -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,12 @@
1
+ (fn power-of-three? [n]
2
+ (macro divisible? [v k]
3
+ `(= 0 (% ,v ,k)))
4
+ (var x n)
5
+ (while (and (> x 1) (divisible? x 3))
6
+ (set x (/ x 3)))
7
+ (= x 1))
8
+
9
+ (print (power-of-three? 27))
10
+ (print (power-of-three? 9))
11
+ (print (power-of-three? 45))
12
+ (print (power-of-three? 1))
@@ -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)))))
data/exe/kapusta-ls ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative '../lib/kapusta/lsp'
5
+
6
+ case ARGV.first
7
+ when '--version', '-v'
8
+ puts "kapusta-ls #{Kapusta::VERSION}"
9
+ when '--help', '-h'
10
+ puts 'usage: kapusta-ls # speak LSP over stdio'
11
+ puts ' kapusta-ls --version'
12
+ else
13
+ Kapusta::LSP.start
14
+ end
data/kapusta.gemspec CHANGED
@@ -20,10 +20,10 @@ Gem::Specification.new do |spec|
20
20
  spec.files = Dir.chdir(__dir__) do
21
21
  `git ls-files -z`.split("\x0").select do |path|
22
22
  path.start_with?('bin/', 'docs/', 'examples/', 'exe/', 'lib/', 'spec/') ||
23
- %w[.rspec Gemfile README.md Rakefile kapfmt kapusta.gemspec].include?(path)
23
+ %w[.rspec Gemfile README.md Rakefile kapfmt kapusta-ls kapusta.gemspec].include?(path)
24
24
  end
25
25
  end
26
26
  spec.bindir = 'exe'
27
- spec.executables = %w[kapfmt kapusta]
27
+ spec.executables = %w[kapfmt kapusta kapusta-ls]
28
28
  spec.require_paths = ['lib']
29
29
  end
data/lib/kapusta/ast.rb CHANGED
@@ -13,6 +13,7 @@ module Kapusta
13
13
 
14
14
  class Sym
15
15
  attr_reader :name
16
+ attr_accessor :line, :column
16
17
 
17
18
  def initialize(name)
18
19
  @name = name.to_s
@@ -23,7 +24,7 @@ module Kapusta
23
24
  end
24
25
 
25
26
  def inspect
26
- "#<Sym #{@name}>"
27
+ @name.to_s
27
28
  end
28
29
 
29
30
  def ==(other)
@@ -76,7 +77,7 @@ module Kapusta
76
77
 
77
78
  class Vec
78
79
  attr_reader :items
79
- attr_accessor :multiline_source
80
+ attr_accessor :multiline_source, :line, :column
80
81
 
81
82
  def initialize(items)
82
83
  @items = items
@@ -86,17 +87,25 @@ module Kapusta
86
87
  def to_ary
87
88
  @items
88
89
  end
90
+
91
+ def inspect
92
+ "[#{@items.map(&:inspect).join(' ')}]"
93
+ end
89
94
  end
90
95
 
91
96
  class HashLit
92
97
  attr_reader :entries
93
- attr_accessor :multiline_source
98
+ attr_accessor :multiline_source, :line, :column
94
99
 
95
100
  def initialize(entries)
96
101
  @entries = entries
97
102
  @multiline_source = false
98
103
  end
99
104
 
105
+ def inspect
106
+ pairs.map { |k, v| "#{k.inspect} #{v.inspect}" }.then { |p| "{#{p.join(' ')}}" }
107
+ end
108
+
100
109
  def pairs
101
110
  @entries.grep(Array)
102
111
  end
@@ -108,7 +117,7 @@ module Kapusta
108
117
 
109
118
  class List
110
119
  attr_reader :items
111
- attr_accessor :multiline_source
120
+ attr_accessor :multiline_source, :line, :column
112
121
 
113
122
  def initialize(items)
114
123
  @items = items
@@ -126,6 +135,10 @@ module Kapusta
126
135
  def empty?
127
136
  @items.empty?
128
137
  end
138
+
139
+ def inspect
140
+ "(#{@items.map(&:inspect).join(' ')})"
141
+ end
129
142
  end
130
143
 
131
144
  class AutoGensym < Sym
@@ -134,27 +147,48 @@ module Kapusta
134
147
  end
135
148
  end
136
149
 
150
+ class MacroSym < Sym
151
+ def inspect
152
+ "#<MacroSym #{@name}>"
153
+ end
154
+ end
155
+
137
156
  class Quasiquote
138
157
  attr_reader :form
158
+ attr_accessor :line, :column
139
159
 
140
160
  def initialize(form)
141
161
  @form = form
142
162
  end
163
+
164
+ def inspect
165
+ "`#{@form.inspect}"
166
+ end
143
167
  end
144
168
 
145
169
  class Unquote
146
170
  attr_reader :form
171
+ attr_accessor :line, :column
147
172
 
148
173
  def initialize(form)
149
174
  @form = form
150
175
  end
176
+
177
+ def inspect
178
+ ",#{@form.inspect}"
179
+ end
151
180
  end
152
181
 
153
182
  class UnquoteSplice
154
183
  attr_reader :form
184
+ attr_accessor :line, :column
155
185
 
156
186
  def initialize(form)
157
187
  @form = form
158
188
  end
189
+
190
+ def inspect
191
+ ",@#{@form.inspect}"
192
+ end
159
193
  end
160
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
@@ -26,12 +28,14 @@ module Kapusta
26
28
  end
27
29
 
28
30
  def emit_lambda(pattern, body, env, current_scope)
31
+ validate_fn_params!(pattern)
29
32
  return emit_simple_lambda(pattern, body, env, current_scope) if simple_parameter_pattern?(pattern)
30
33
 
31
34
  args_var = temp('args')
32
35
  body_env = env.child
33
36
  bindings_code, body_env = emit_pattern_bind(pattern, args_var, body_env)
34
- body_code, = emit_sequence(body, body_env, current_scope, allow_method_definitions: false)
37
+ body_code, = emit_sequence(body, body_env, current_scope,
38
+ allow_method_definitions: false, result: false)
35
39
  block_locals = pattern_names(pattern).map { |name| body_env.lookup(name) }.uniq
36
40
  block_locals_clause = block_locals.empty? ? '' : "; #{block_locals.join(', ')}"
37
41
  [
@@ -54,12 +58,26 @@ module Kapusta
54
58
  def build_simple_block_parts(pattern, body, env, current_scope)
55
59
  body_env = env.child
56
60
  params = pattern.items.map { |sym| define_local(body_env, sym.name, shadow: true) }
57
- body_code, = emit_sequence(body, body_env, current_scope, allow_method_definitions: false)
61
+ body_code, = emit_sequence(body, body_env, current_scope,
62
+ allow_method_definitions: false, result: false)
58
63
  [params, body_code]
59
64
  end
60
65
 
61
66
  def simple_parameter_pattern?(pattern)
62
- pattern.is_a?(Vec) && pattern.items.all? { |item| item.is_a?(Sym) && !item.dotted? && item.name != '&' }
67
+ pattern.is_a?(Vec) && pattern.items.all? do |item|
68
+ item.is_a?(Sym) && (!item.dotted? || item.name == '...') && item.name != '&'
69
+ end
70
+ end
71
+
72
+ def validate_fn_params!(pattern)
73
+ return unless pattern.is_a?(Vec)
74
+
75
+ pattern.items.each_with_index do |item, idx|
76
+ next unless item.is_a?(Sym) && item.name == '...'
77
+ next if idx == pattern.items.length - 1
78
+
79
+ emit_error!(:vararg_not_last)
80
+ end
63
81
  end
64
82
 
65
83
  def emit_definition_form(form, env, current_scope)
@@ -123,6 +141,7 @@ module Kapusta
123
141
  end
124
142
 
125
143
  def emit_direct_method_definition(name_sym, pattern, body, env)
144
+ validate_fn_params!(pattern)
126
145
  return unless simple_parameter_pattern?(pattern)
127
146
 
128
147
  ruby_name = direct_method_definition_name(name_sym)
@@ -131,7 +150,8 @@ module Kapusta
131
150
 
132
151
  body_env = env.child
133
152
  params = pattern.items.map { |sym| define_local(body_env, sym.name, shadow: true) }
134
- body_code, = emit_sequence(body, body_env, :toplevel, allow_method_definitions: false)
153
+ body_code, = emit_sequence(body, body_env, :toplevel,
154
+ allow_method_definitions: false, result: false)
135
155
  header = params.empty? ? "def #{ruby_name}" : "def #{ruby_name}(#{params.join(', ')})"
136
156
  [
137
157
  header,
@@ -165,12 +185,14 @@ module Kapusta
165
185
  end
166
186
 
167
187
  def emit_method_body(pattern, body, env)
188
+ validate_fn_params!(pattern)
168
189
  return emit_simple_method_body(pattern, body, env) if simple_parameter_pattern?(pattern)
169
190
 
170
191
  args_var = temp('args')
171
192
  method_env = env.child
172
193
  bindings_code, body_env = emit_pattern_bind(pattern, args_var, method_env)
173
- body_code, = emit_sequence(body, body_env, :toplevel, allow_method_definitions: false)
194
+ body_code, = emit_sequence(body, body_env, :toplevel,
195
+ allow_method_definitions: false, result: false)
174
196
  block_locals = pattern_names(pattern).map { |name| body_env.lookup(name) }.uniq
175
197
  block_locals_clause = block_locals.empty? ? '' : "; #{block_locals.join(', ')}"
176
198
  ["do |*#{args_var}#{block_locals_clause}|", join_code(bindings_code, body_code)]
@@ -227,6 +249,9 @@ module Kapusta
227
249
 
228
250
  def emit_let_parts(args, env, current_scope, result:)
229
251
  bindings = args[0]
252
+ emit_error!(:let_odd_bindings) if bindings.items.length.odd?
253
+ emit_error!(:let_no_body) if args.length < 2
254
+
230
255
  body = args[1..]
231
256
  child_env = env.child
232
257
  binding_codes = []
@@ -234,8 +259,11 @@ module Kapusta
234
259
  i = 0
235
260
  while i < items.length
236
261
  pattern = items[i]
237
- value_code = emit_expr(items[i + 1], child_env, current_scope)
262
+ value_form = items[i + 1]
263
+ check_destructure_value!(pattern, value_form)
264
+ value_code = emit_expr(value_form, child_env, current_scope)
238
265
  bind_code, child_env = emit_pattern_bind(pattern, value_code, child_env)
266
+ walk_pattern_syms(pattern) { |sym| mark_mutability(child_env, sym, mutable: false) }
239
267
  binding_codes << bind_code
240
268
  i += 2
241
269
  end
@@ -250,11 +278,15 @@ module Kapusta
250
278
  end
251
279
 
252
280
  def emit_local_form(form, env, current_scope)
281
+ emit_error!(:local_arity, form: form.head.name) unless form.items.length == 3
282
+
253
283
  target = form.items[1]
254
284
  value_code = emit_expr(form.items[2], env, current_scope)
255
285
 
256
286
  if target.is_a?(Sym)
287
+ validate_binding_symbol!(target)
257
288
  ruby_name = define_local(env, target.name)
289
+ mark_mutability(env, target.name, mutable: form.head.name == 'var')
258
290
  ["#{ruby_name} = #{value_code}\nnil", env]
259
291
  else
260
292
  bind_code, env = emit_pattern_bind(target, value_code, env)
@@ -262,11 +294,58 @@ module Kapusta
262
294
  end
263
295
  end
264
296
 
297
+ def check_destructure_value!(pattern, value_form)
298
+ return unless pattern.is_a?(Vec) || pattern.is_a?(HashLit)
299
+
300
+ case value_form
301
+ when String, Numeric, Symbol, true, false
302
+ emit_error!(:could_not_destructure_literal)
303
+ end
304
+ end
305
+
306
+ def mark_mutability(env, name, mutable:)
307
+ @binding_mutability ||= {}
308
+ ruby_name = env.lookup(name)
309
+ @binding_mutability[ruby_name] = mutable
310
+ end
311
+
312
+ def walk_pattern_syms(pattern, &block)
313
+ case pattern
314
+ when Sym
315
+ yield pattern unless pattern.name == '_'
316
+ when Vec
317
+ pattern.items.each do |item|
318
+ next if item.is_a?(Sym) && ['&', '...'].include?(item.name)
319
+
320
+ walk_pattern_syms(item, &block)
321
+ end
322
+ when HashLit
323
+ pattern.pairs.each { |pair| walk_pattern_syms(pair[1], &block) }
324
+ end
325
+ end
326
+
327
+ def mutable_binding?(env, name)
328
+ ruby_name = env.lookup_if_defined(name)
329
+ return false unless ruby_name
330
+
331
+ (@binding_mutability ||= {}).fetch(ruby_name, true)
332
+ end
333
+
265
334
  def emit_local_expr(args, env, current_scope)
266
335
  code, = emit_local_form(List.new([Sym.new('local'), *args]), env.child, current_scope)
267
336
  "(-> do\n#{indent(code)}\nend).call"
268
337
  end
269
338
 
339
+ def emit_global_expr(args, _env, _current_scope)
340
+ emit_error!(:global_arity) unless args.length == 2
341
+ unless args[0].is_a?(Sym)
342
+ emit_error!(:global_non_symbol_name, type: args[0].class.name.downcase, value: args[0].inspect)
343
+ end
344
+
345
+ name = args[0].name
346
+ "$#{global_name(name)} = #{emit_expr(args[1], Env.new, :toplevel)}\nnil"
347
+ end
348
+
270
349
  def emit_set_form(form, env, current_scope)
271
350
  target = form.items[1]
272
351
  value_code = emit_expr(form.items[2], env, current_scope)
@@ -275,7 +354,7 @@ module Kapusta
275
354
  binding = env.lookup_if_defined(target.name)
276
355
  ruby_name =
277
356
  if binding
278
- emit_error!("cannot set method binding: #{target.name}") if method_binding?(binding)
357
+ emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
279
358
 
280
359
  binding
281
360
  else
@@ -319,7 +398,8 @@ module Kapusta
319
398
  end
320
399
  else
321
400
  binding = env.lookup(target.name)
322
- emit_error!("cannot set method binding: #{target.name}") if method_binding?(binding)
401
+ emit_error!(:cannot_set_method_binding, name: target.name) if method_binding?(binding)
402
+ emit_error!(:expected_var, name: target.name) unless mutable_binding?(env, target.name)
323
403
 
324
404
  emit_assignment(binding, value_code)
325
405
  end
@@ -338,10 +418,10 @@ module Kapusta
338
418
  elsif head.is_a?(Sym) && head.name == 'gvar'
339
419
  emit_assignment("$#{global_name(target.items[1].name)}", value_code)
340
420
  else
341
- emit_error!("bad set target: #{target.inspect}")
421
+ emit_error!(:bad_set_target, target: target.inspect)
342
422
  end
343
423
  else
344
- emit_error!("bad set target: #{target.inspect}")
424
+ emit_error!(:bad_set_target, target: target.inspect)
345
425
  end
346
426
  end
347
427
  end