kapusta 0.8.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.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +8 -2
  3. data/bin/check-all +17 -0
  4. data/bin/compile-examples +70 -0
  5. data/bin/fennel-parity +4 -37
  6. data/examples/account-lockout.kap +11 -0
  7. data/examples/circle.kap +16 -0
  8. data/examples/convert-temperature.kap +14 -0
  9. data/examples/count-effects.kap +13 -0
  10. data/examples/falling-drops.kap +12 -0
  11. data/examples/fennel-parity-examples.txt +40 -0
  12. data/examples/hit-counter.kap +17 -0
  13. data/examples/max-achievable.kap +8 -0
  14. data/examples/mruby-runtime-examples.txt +89 -0
  15. data/examples/number-of-1-bits.kap +13 -0
  16. data/examples/number-of-steps.kap +15 -0
  17. data/examples/parking-system.kap +18 -0
  18. data/examples/thread-styles.kap +41 -0
  19. data/examples/two-sum-hash.kap +11 -14
  20. data/examples/underscore-patterns.kap +1 -1
  21. data/lib/kapusta/ast.rb +1 -1
  22. data/lib/kapusta/cli.rb +11 -6
  23. data/lib/kapusta/compiler/emitter/bindings.rb +27 -2
  24. data/lib/kapusta/compiler/emitter/control_flow.rb +97 -14
  25. data/lib/kapusta/compiler/emitter/interop.rb +2 -0
  26. data/lib/kapusta/compiler/emitter/patterns.rb +125 -0
  27. data/lib/kapusta/compiler/emitter/support.rb +9 -2
  28. data/lib/kapusta/compiler/emitter.rb +2 -1
  29. data/lib/kapusta/compiler/normalizer.rb +22 -12
  30. data/lib/kapusta/compiler.rb +13 -4
  31. data/lib/kapusta/errors.rb +3 -0
  32. data/lib/kapusta/formatter.rb +9 -2
  33. data/lib/kapusta/lsp/scope_walker.rb +55 -5
  34. data/lib/kapusta/reader.rb +28 -0
  35. data/lib/kapusta/version.rb +1 -1
  36. data/lib/kapusta.rb +2 -2
  37. data/spec/cli_spec.rb +35 -0
  38. data/spec/examples_spec.rb +128 -0
  39. data/spec/lsp_spec.rb +86 -0
  40. data/spec/spec_helper.rb +9 -0
  41. metadata +14 -1
@@ -181,6 +181,14 @@ module Kapusta
181
181
 
182
182
  def emit_toplevel_method_bridge(ruby_name)
183
183
  method_name = ruby_name.to_sym.inspect
184
+ if mruby_target?
185
+ return [
186
+ "define_singleton_method(#{method_name}) do |*args|",
187
+ indent("Object.instance_method(#{method_name}).bind(self).call(*args)"),
188
+ 'end'
189
+ ].join("\n")
190
+ end
191
+
184
192
  "define_singleton_method(#{method_name}, Object.instance_method(#{method_name}).bind(self))"
185
193
  end
186
194
 
@@ -229,8 +237,14 @@ module Kapusta
229
237
 
230
238
  binding = env.lookup_if_defined(name)
231
239
  return false if binding.nil?
240
+ return false if method_binding?(binding)
241
+ return false if constant_binding?(binding)
242
+
243
+ true
244
+ end
232
245
 
233
- !method_binding?(binding)
246
+ def constant_binding?(binding)
247
+ binding.is_a?(String) && binding.match?(/\A[A-Z][A-Z0-9_]*\z/)
234
248
  end
235
249
 
236
250
  def emit_let(args, env, current_scope)
@@ -277,7 +291,7 @@ module Kapusta
277
291
  chunks.reject(&:empty?).join("\n")
278
292
  end
279
293
 
280
- def emit_local_form(form, env, current_scope)
294
+ def emit_local_form(form, env, current_scope, allow_constant: false)
281
295
  emit_error!(:local_arity, form: form.head.name) unless form.items.length == 3
282
296
 
283
297
  target = form.items[1]
@@ -285,6 +299,12 @@ module Kapusta
285
299
 
286
300
  if target.is_a?(Sym)
287
301
  validate_binding_symbol!(target)
302
+ if allow_constant && form.head.name == 'local' && (constant_name = constant_name_for(target.name))
303
+ env.define(target.name, constant_name)
304
+ mark_mutability(env, target.name, mutable: false)
305
+ return ["#{constant_name} = #{value_code}\nnil", env]
306
+ end
307
+
288
308
  ruby_name = define_local(env, target.name)
289
309
  mark_mutability(env, target.name, mutable: form.head.name == 'var')
290
310
  ["#{ruby_name} = #{value_code}\nnil", env]
@@ -294,6 +314,11 @@ module Kapusta
294
314
  end
295
315
  end
296
316
 
317
+ def constant_name_for(source_name)
318
+ candidate = source_name.tr('-', '_').upcase
319
+ candidate if candidate.match?(/\A[A-Z][A-Z0-9_]*\z/)
320
+ end
321
+
297
322
  def check_destructure_value!(pattern, value_form)
298
323
  return unless pattern.is_a?(Vec) || pattern.is_a?(HashLit)
299
324
 
@@ -57,6 +57,25 @@ module Kapusta
57
57
  end
58
58
 
59
59
  def emit_case(args, env, current_scope, mode)
60
+ value_code, value_var, body = build_case_parts(args, env, current_scope, mode)
61
+ return body unless value_var
62
+
63
+ [
64
+ '(-> do',
65
+ indent("#{value_var} = #{value_code}"),
66
+ indent(body),
67
+ 'end).call'
68
+ ].join("\n")
69
+ end
70
+
71
+ def emit_case_statement(args, env, current_scope, mode)
72
+ value_code, value_var, body = build_case_parts(args, env, current_scope, mode)
73
+ return body unless value_var
74
+
75
+ "#{value_var} = #{value_code}\n#{body}"
76
+ end
77
+
78
+ def build_case_parts(args, env, current_scope, mode)
60
79
  emit_error!(:case_no_subject) if args.empty?
61
80
 
62
81
  clauses = args[1..]
@@ -65,20 +84,22 @@ module Kapusta
65
84
 
66
85
  value_code = emit_expr(args[0], env, current_scope)
67
86
  if simple_case_subject?(args[0]) && simple_expression?(value_code)
68
- body = try_emit_native_case(value_code, clauses, env, current_scope, mode)
87
+ body = emit_case_body(value_code, clauses, env, current_scope, mode)
69
88
  emit_error!(:case_unsupported) unless body
70
- return body
89
+ return [value_code, nil, body]
71
90
  end
72
91
 
73
92
  value_var = temp('case_value')
74
- body = try_emit_native_case(value_var, clauses, env, current_scope, mode)
93
+ body = emit_case_body(value_var, clauses, env, current_scope, mode)
75
94
  emit_error!(:case_unsupported) unless body
76
- [
77
- '(-> do',
78
- indent("#{value_var} = #{value_code}"),
79
- indent(body),
80
- 'end).call'
81
- ].join("\n")
95
+ [value_code, value_var, body]
96
+ end
97
+
98
+ def emit_case_body(value_var, clauses, env, current_scope, mode)
99
+ return try_emit_compat_case(value_var, clauses, env, current_scope, mode) if mruby_target?
100
+
101
+ try_emit_native_case(value_var, clauses, env, current_scope, mode) ||
102
+ try_emit_compat_case(value_var, clauses, env, current_scope, mode)
82
103
  end
83
104
 
84
105
  def simple_case_subject?(form)
@@ -113,11 +134,6 @@ module Kapusta
113
134
  ["case #{value_var}", *arms, 'end'].join("\n")
114
135
  end
115
136
 
116
- def wildcard_last?(clauses)
117
- last_pattern = clauses[-2]
118
- last_pattern.is_a?(Sym) && last_pattern.name == '_'
119
- end
120
-
121
137
  def try_native_arm(pattern, body, where_guards, env, current_scope, mode)
122
138
  allow_pins = !where_guards.empty? && mode == :case
123
139
  plan = native_pattern_plan(pattern, env, mode:, allow_pins:)
@@ -132,6 +148,73 @@ module Kapusta
132
148
  ["in #{plan[:pattern]}#{guard_clause}", indent(body_code)].join("\n")
133
149
  end
134
150
 
151
+ def try_emit_compat_case(value_var, clauses, env, current_scope, mode)
152
+ arms = []
153
+ i = 0
154
+ while i < clauses.length
155
+ pattern = clauses[i]
156
+ body = clauses[i + 1]
157
+ inner, where_guards = if where_pattern?(pattern)
158
+ [pattern.items[1], pattern.items[2..]]
159
+ else
160
+ [pattern, []]
161
+ end
162
+ sub_patterns = or_pattern?(inner) ? inner.items[1..] : [inner]
163
+ sub_arms = sub_patterns.map do |sub|
164
+ try_compat_arm(sub, body, where_guards, value_var, env, current_scope, mode)
165
+ end
166
+ return if sub_arms.any?(&:nil?)
167
+
168
+ arms.concat(sub_arms)
169
+ i += 2
170
+ end
171
+ emit_compat_case_lines(arms)
172
+ end
173
+
174
+ def wildcard_last?(clauses)
175
+ last_pattern = clauses[-2]
176
+ last_pattern.is_a?(Sym) && last_pattern.name == '_'
177
+ end
178
+
179
+ def try_compat_arm(pattern, body, where_guards, value_var, env, current_scope, mode)
180
+ allow_pins = !where_guards.empty? && mode == :case
181
+ arm_env = env.child
182
+ plan = compat_pattern_plan(pattern, value_var, env, arm_env, mode:, allow_pins:)
183
+ return unless plan
184
+
185
+ where_guard_codes = where_guards.map { |g| emit_expr(g, arm_env, current_scope) }
186
+ if where_guard_codes.empty?
187
+ guard_codes = plan[:conditions]
188
+ prelude = plan[:prelude]
189
+ else
190
+ prelude_guards = plan[:prelude].map { |line| "begin #{line}; true end" }
191
+ guard_codes = plan[:conditions] + prelude_guards + where_guard_codes
192
+ prelude = []
193
+ end
194
+ body_code = emit_expr(body, arm_env, current_scope)
195
+ [guard_codes, prelude, body_code]
196
+ end
197
+
198
+ def emit_compat_case_lines(arms)
199
+ last_idx = arms.length - 1
200
+ has_unconditional = arms.any? { |conditions, _prelude, _body_code| conditions.empty? }
201
+ lines = ['case']
202
+ arms.each_with_index do |(conditions, prelude, body_code), idx|
203
+ lines << if conditions.empty?
204
+ idx == last_idx ? 'else' : 'when true'
205
+ else
206
+ "when #{conditions.join(' && ')}"
207
+ end
208
+ lines << indent([*prelude, body_code].join("\n"))
209
+ end
210
+ unless has_unconditional
211
+ lines << 'else'
212
+ lines << indent('nil')
213
+ end
214
+ lines << 'end'
215
+ lines.join("\n")
216
+ end
217
+
135
218
  def emit_while(args, env, current_scope)
136
219
  <<~RUBY.chomp
137
220
  (-> do
@@ -300,6 +300,8 @@ module Kapusta
300
300
  def emit_bound_call(binding, args, env, current_scope)
301
301
  return emit_self_method_binding_call(binding, args, env, current_scope) if method_binding?(binding)
302
302
 
303
+ emit_error!(:cannot_call_constant, name: binding) if constant_binding?(binding)
304
+
303
305
  emit_callable_call(binding, args, env, current_scope)
304
306
  end
305
307
 
@@ -161,6 +161,131 @@ module Kapusta
161
161
 
162
162
  class PatternNotTranslatable < StandardError; end
163
163
 
164
+ def compat_pattern_plan(pattern, value_code, env, arm_env, mode:, allow_pins:)
165
+ state = { bound_names: {}, conditions: [], prelude: [] }
166
+ compile_compat_pattern(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
167
+ { conditions: state[:conditions], prelude: state[:prelude] }
168
+ rescue PatternNotTranslatable
169
+ nil
170
+ end
171
+
172
+ def compile_compat_pattern(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
173
+ case pattern
174
+ when Sym
175
+ compile_compat_symbol(pattern, value_code, env, arm_env, mode:, state:)
176
+ when Vec
177
+ compile_compat_sequence(pattern.items, value_code, env, arm_env, mode:, allow_pins:, state:)
178
+ when HashLit
179
+ compile_compat_hash(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
180
+ when List
181
+ if pin_pattern?(pattern)
182
+ compile_compat_pin(pattern, value_code, env, mode:, allow_pins:, state:)
183
+ elsif or_pattern?(pattern)
184
+ raise PatternNotTranslatable
185
+ else
186
+ compile_compat_sequence(pattern.items, value_code, env, arm_env, mode:, allow_pins:, state:)
187
+ end
188
+ when nil
189
+ state[:conditions] << "#{value_code}.nil?"
190
+ when Symbol, String, Numeric, true, false
191
+ state[:conditions] << "#{value_code} == #{pattern.inspect}"
192
+ else
193
+ raise PatternNotTranslatable
194
+ end
195
+ end
196
+
197
+ def compile_compat_symbol(pattern, value_code, env, arm_env, mode:, state:)
198
+ name = pattern.name
199
+ return if name == '_'
200
+
201
+ if nil_allowing_pattern_name?(name)
202
+ raise PatternNotTranslatable if state[:bound_names].key?(name)
203
+
204
+ ruby = define_local(arm_env, name)
205
+ state[:bound_names][name] = true
206
+ state[:prelude] << "#{ruby} = #{value_code}"
207
+ return
208
+ end
209
+
210
+ binding = mode == :match ? env.lookup_if_defined(name) : nil
211
+ if state[:bound_names].key?(name)
212
+ raise PatternNotTranslatable
213
+ elsif binding
214
+ state[:conditions] << "#{value_code} == #{binding_value_code(binding)}"
215
+ else
216
+ ruby = define_local(arm_env, name)
217
+ state[:bound_names][name] = true
218
+ state[:conditions] << "(#{ruby} = #{value_code}) != nil"
219
+ end
220
+ end
221
+
222
+ def compile_compat_sequence(items, value_code, env, arm_env, mode:, allow_pins:, state:)
223
+ min_length = compat_sequence_min_length(items)
224
+ state[:conditions] << "#{value_code}.is_a?(Array)"
225
+ state[:conditions] << "#{value_code}.length >= #{min_length}"
226
+
227
+ index = 0
228
+ i = 0
229
+ while i < items.length
230
+ if rest_pattern_marker?(items, i)
231
+ sub = items[i + 1]
232
+ raise PatternNotTranslatable unless sub.is_a?(Sym)
233
+
234
+ unless sub.name == '_'
235
+ ruby = define_local(arm_env, sub.name)
236
+ state[:prelude] << "#{ruby} = #{value_code}[#{index}..]"
237
+ end
238
+ i += 2
239
+ else
240
+ compile_compat_pattern(items[i], "#{value_code}[#{index}]", env, arm_env,
241
+ mode:, allow_pins:, state:)
242
+ index += 1
243
+ i += 1
244
+ end
245
+ end
246
+ end
247
+
248
+ def compat_sequence_min_length(items)
249
+ count = 0
250
+ i = 0
251
+ while i < items.length
252
+ if rest_pattern_marker?(items, i)
253
+ i += 2
254
+ else
255
+ count += 1
256
+ i += 1
257
+ end
258
+ end
259
+ count
260
+ end
261
+
262
+ def compile_compat_hash(pattern, value_code, env, arm_env, mode:, allow_pins:, state:)
263
+ state[:conditions] << "#{value_code}.is_a?(Hash)"
264
+ pattern.pairs.each do |key, value|
265
+ lookup = "#{value_code}[#{compile_compat_hash_key(key)}]"
266
+ compile_compat_pattern(value, lookup, env, arm_env, mode:, allow_pins:, state:)
267
+ end
268
+ end
269
+
270
+ def compile_compat_hash_key(key)
271
+ case key
272
+ when Symbol, String, Numeric, true, false, nil then key.inspect
273
+ else raise PatternNotTranslatable
274
+ end
275
+ end
276
+
277
+ def compile_compat_pin(pattern, value_code, env, mode:, allow_pins:, state:)
278
+ raise PatternNotTranslatable unless allow_pins && mode == :case
279
+
280
+ name_sym = pattern.items[1]
281
+ raise PatternNotTranslatable unless name_sym.is_a?(Sym)
282
+
283
+ binding = env.lookup_if_defined(name_sym.name)
284
+ raise PatternNotTranslatable unless binding
285
+
286
+ state[:conditions] << "#{value_code} == #{binding_value_code(binding)}"
287
+ end
288
+
164
289
  def native_pattern_plan(pattern, env, mode:, allow_pins:)
165
290
  state = { bound_names: {}, binding_names: [], guards: [] }
166
291
  ruby_pattern = compile_native_pattern(pattern, env, mode:, allow_pins:, state:)
@@ -25,6 +25,10 @@ module Kapusta
25
25
  (@form_stack ||= []).last
26
26
  end
27
27
 
28
+ def mruby_target?
29
+ @target == :mruby
30
+ end
31
+
28
32
  def positionable?(form)
29
33
  form.respond_to?(:line) && form.respond_to?(:column)
30
34
  end
@@ -97,7 +101,8 @@ module Kapusta
97
101
  if named_function_form?(form)
98
102
  emit_named_fn_assignment(form, env, current_scope)
99
103
  elsif local_form?(form)
100
- code, env = emit_local_form(form, env, current_scope)
104
+ code, env = emit_local_form(form, env, current_scope,
105
+ allow_constant: allow_method_definitions)
101
106
  code = code.delete_suffix("\nnil") unless result_needed
102
107
  [code, env]
103
108
  elsif do_form?(form)
@@ -136,7 +141,7 @@ module Kapusta
136
141
  def sequence_statement_form?(form)
137
142
  return false unless form.is_a?(List) && form.head.is_a?(Sym)
138
143
 
139
- %w[let while for each].include?(form.head.name)
144
+ %w[let while for each case match].include?(form.head.name)
140
145
  end
141
146
 
142
147
  def emit_sequence_statement_form(form, env, current_scope, result_needed:)
@@ -152,6 +157,8 @@ module Kapusta
152
157
  return ["#{code}\nnil", env] if result_needed && current_scope != :toplevel
153
158
 
154
159
  return [code, env]
160
+ when 'case', 'match'
161
+ return [emit_case_statement(form.rest, env, current_scope, form.head.name.to_sym), env] unless result_needed
155
162
  end
156
163
 
157
164
  [emit_expr(form, env, current_scope), env]
@@ -24,8 +24,9 @@ module Kapusta
24
24
  include EmitterModules::Interop
25
25
  include EmitterModules::Patterns
26
26
 
27
- def initialize(path:)
27
+ def initialize(path:, target: nil)
28
28
  @path = path
29
+ @target = target
29
30
  @temp_index = 0
30
31
  end
31
32
 
@@ -104,19 +104,29 @@ module Kapusta
104
104
  end
105
105
 
106
106
  def thread_short(forms, position)
107
- forms[1..].reduce(forms.first) do |memo, form|
108
- temp = thread_temp
109
- List.new([
110
- Sym.new('let'),
111
- Vec.new([temp, memo]),
112
- List.new([
113
- Sym.new('if'),
114
- List.new([Sym.new('='), temp, nil]),
115
- nil,
116
- thread_step(temp, form, position)
117
- ])
118
- ])
107
+ steps = forms[1..]
108
+ return forms.first if steps.empty?
109
+
110
+ prev_temp = thread_temp
111
+ binding_items = [prev_temp, forms.first]
112
+ body = nil
113
+ last_index = steps.length - 1
114
+ steps.each_with_index do |form, i|
115
+ guarded = List.new([
116
+ Sym.new('if'),
117
+ List.new([Sym.new('='), prev_temp, nil]),
118
+ nil,
119
+ thread_step(prev_temp, form, position)
120
+ ])
121
+ if i == last_index
122
+ body = guarded
123
+ else
124
+ temp = thread_temp
125
+ binding_items.push(temp, guarded)
126
+ prev_temp = temp
127
+ end
119
128
  end
129
+ List.new([Sym.new('let'), Vec.new(binding_items), body])
120
130
  end
121
131
 
122
132
  def thread_step(memo, form, position)
@@ -34,17 +34,17 @@ module Kapusta
34
34
  ].freeze
35
35
  SPECIAL_FORMS = (CORE_SPECIAL_FORMS + LuaCompat::SPECIAL_FORMS).freeze
36
36
 
37
- def self.compile(source, path: '(kapusta)')
37
+ def self.compile(source, path: '(kapusta)', target: nil)
38
38
  forms = Reader.read_all(source)
39
39
  expanded = MacroExpander.new(path:).expand_all(forms)
40
- compile_forms(expanded, path:)
40
+ compile_forms(expanded, path:, target:)
41
41
  rescue Kapusta::Error => e
42
42
  raise e.with_defaults(path:)
43
43
  end
44
44
 
45
- def self.compile_forms(forms, path: '(kapusta)')
45
+ def self.compile_forms(forms, path: '(kapusta)', target: nil)
46
46
  normalized = Normalizer.new.normalize_all(forms)
47
- Emitter.new(path:).emit_file(normalized)
47
+ Emitter.new(path:, target: normalize_target(target)).emit_file(normalized)
48
48
  rescue Kapusta::Error => e
49
49
  raise e.with_defaults(path:)
50
50
  end
@@ -57,5 +57,14 @@ module Kapusta
57
57
  def self.run_file(path)
58
58
  run(File.read(path), path:)
59
59
  end
60
+
61
+ def self.normalize_target(target)
62
+ case target
63
+ when nil then nil
64
+ when :mruby, 'mruby' then :mruby
65
+ else
66
+ raise Error, Kapusta::Errors.format(:unknown_target, target: target.inspect)
67
+ end
68
+ end
60
69
  end
61
70
  end
@@ -10,6 +10,7 @@ module Kapusta
10
10
  bad_set_target: 'bad set target: %{target}',
11
11
  bad_shorthand: 'bad shorthand',
12
12
  bind_table_dots: 'unable to bind table ...',
13
+ cannot_call_constant: 'cannot call constant %{name}; reference it without parentheses',
13
14
  cannot_call_literal: 'cannot call literal value %{value}',
14
15
  cannot_emit_form: 'cannot emit form: %{form}',
15
16
  cannot_set_method_binding: 'cannot set method binding: %{name}',
@@ -51,6 +52,7 @@ module Kapusta
51
52
  odd_forms_in_hash: 'odd number of forms in hash',
52
53
  rest_not_last: 'expected rest argument before last parameter',
53
54
  shadowed_special: 'local %{name} was overshadowed by a special form or macro',
55
+ target_requires_compile: '--target requires --compile',
54
56
  tset_no_value: 'tset: expected table, key, and value arguments',
55
57
  unclosed_delimiter: "unclosed opening delimiter '%{char}'",
56
58
  undefined_symbol: 'undefined symbol: %{name}',
@@ -58,6 +60,7 @@ module Kapusta
58
60
  unexpected_eof: 'unexpected eof',
59
61
  unexpected_vararg: 'unexpected vararg',
60
62
  unknown_special_form: 'unknown special form: %{name}',
63
+ unknown_target: 'unknown target %{target}; only mruby is supported',
61
64
  unquote_outside_quasiquote: 'unquote outside quasiquote',
62
65
  unquote_splice_outside_list: 'unquote-splice must appear inside a quoted list/vec',
63
66
  unterminated_string: 'unterminated string',
@@ -178,7 +178,7 @@ module Kapusta
178
178
 
179
179
  case form
180
180
  when Comment then form.text
181
- when List then render_list(form, indent, top_level:)
181
+ when List then form.sigil ? render_sigil(form) : render_list(form, indent, top_level:)
182
182
  when Vec then render_vec(form, indent, layout:, top_level:, force_expand:)
183
183
  when HashLit then render_hash(form, indent)
184
184
  when Quasiquote then render_prefix('`', form.form, indent, force_expand:)
@@ -189,6 +189,13 @@ module Kapusta
189
189
  end
190
190
  end
191
191
 
192
+ SIGIL_PREFIXES = { ivar: '@', cvar: '@@', gvar: '$' }.freeze
193
+ private_constant :SIGIL_PREFIXES
194
+
195
+ def render_sigil(list)
196
+ "#{SIGIL_PREFIXES.fetch(list.sigil)}#{list.items[1].name}"
197
+ end
198
+
192
199
  def render_prefix(prefix, inner, indent, force_expand: false)
193
200
  rendered = render(inner, indent + prefix.length, force_expand:)
194
201
  lines = rendered.lines(chomp: true)
@@ -221,6 +228,7 @@ module Kapusta
221
228
 
222
229
  "{#{rendered.join(' ')}}"
223
230
  when List
231
+ return render_sigil(form) if form.sigil
224
232
  return if contains_comments?(form.items)
225
233
  return "##{flat_render(semantic_items(form.items)[1])}" if hashfn_literal?(form)
226
234
  return if multiline_in_source?(form)
@@ -649,7 +657,6 @@ module Kapusta
649
657
 
650
658
  def render_let_bindings(bindings, indent)
651
659
  return render(bindings, indent + '(let '.length, force_expand: true) if contains_comments?(bindings.items)
652
- return render(bindings, indent + '(let '.length, layout: :pairwise) if bindings.items.length <= 2
653
660
 
654
661
  hanging = render_hanging_pairwise_vec(bindings)
655
662
  hanging || render(bindings, indent + '(let '.length, layout: :pairwise)
@@ -16,7 +16,7 @@ module Kapusta
16
16
  end
17
17
  end
18
18
 
19
- SKIPPED_HEADS = %w[macros ivar cvar gvar quasi-sym quasi-list
19
+ SKIPPED_HEADS = %w[macros quasi-sym quasi-list
20
20
  quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym].freeze
21
21
 
22
22
  DISPATCHERS = {
@@ -42,7 +42,10 @@ module Kapusta
42
42
  'class' => :walk_module_class,
43
43
  'hashfn' => :walk_hashfn,
44
44
  'macro' => :walk_macro_def,
45
- 'import-macros' => :walk_import_macros
45
+ 'import-macros' => :walk_import_macros,
46
+ 'ivar' => :walk_sigil_form,
47
+ 'cvar' => :walk_sigil_form,
48
+ 'gvar' => :walk_sigil_form
46
49
  }.freeze
47
50
 
48
51
  attr_reader :bindings, :references, :root_scope
@@ -58,7 +61,9 @@ module Kapusta
58
61
  @references = []
59
62
  @scope_seq = 0
60
63
  @root_scope = make_scope(nil, :file)
64
+ @gvar_scope = make_scope(nil, :gvars)
61
65
  @in_module_or_class = 0
66
+ @sigil_scope_stack = [make_sigil_scopes]
62
67
  end
63
68
 
64
69
  def walk_top(forms)
@@ -132,7 +137,7 @@ module Kapusta
132
137
  name_sym, supers, = split_class_args(form.items[1..] || [])
133
138
  supers&.items&.each { |item| walk_form(item, scope) }
134
139
  add_constant_binding(name_sym, scope, :class) if name_sym.is_a?(Sym)
135
- inside_module_or_class do
140
+ inside_class do
136
141
  remaining_forms.each { |item| walk_form(item, scope) }
137
142
  end
138
143
  end
@@ -154,6 +159,21 @@ module Kapusta
154
159
  @in_module_or_class -= 1
155
160
  end
156
161
 
162
+ def inside_class
163
+ inside_module_or_class do
164
+ @sigil_scope_stack.push(make_sigil_scopes)
165
+ begin
166
+ yield
167
+ ensure
168
+ @sigil_scope_stack.pop
169
+ end
170
+ end
171
+ end
172
+
173
+ def make_sigil_scopes
174
+ { ivar: make_scope(nil, :ivars), cvar: make_scope(nil, :cvars) }
175
+ end
176
+
157
177
  def walk_form(form, scope)
158
178
  case form
159
179
  when List then walk_list(form, scope)
@@ -291,6 +311,10 @@ module Kapusta
291
311
  target = list.items[1]
292
312
  value = list.items[2]
293
313
  walk_form(value, scope) if value
314
+ if target.is_a?(List)
315
+ walk_form(target, scope)
316
+ return
317
+ end
294
318
  return unless target.is_a?(Sym) && !target.dotted?
295
319
 
296
320
  existing = scope.lookup(target.name)
@@ -301,6 +325,29 @@ module Kapusta
301
325
  end
302
326
  end
303
327
 
328
+ def walk_sigil_form(list, _scope)
329
+ return if list.items.length < 2
330
+
331
+ inner = list.items[1]
332
+ return unless inner.is_a?(Sym)
333
+
334
+ kind = list.head.name.to_sym
335
+ target_scope = sigil_target_scope(kind)
336
+ existing = target_scope.bindings[inner.name]
337
+ if existing
338
+ add_reference(inner, target_scope, existing)
339
+ else
340
+ add_binding(inner, target_scope, kind)
341
+ end
342
+ end
343
+
344
+ def sigil_target_scope(kind)
345
+ case kind
346
+ when :ivar, :cvar then @sigil_scope_stack.last.fetch(kind)
347
+ when :gvar then @gvar_scope
348
+ end
349
+ end
350
+
304
351
  def walk_fn(list, scope)
305
352
  items = list.items
306
353
  if items[1].is_a?(Vec)
@@ -472,8 +519,11 @@ module Kapusta
472
519
 
473
520
  add_constant_binding(name_sym, scope, kind) if name_sym.is_a?(Sym)
474
521
 
475
- inside_module_or_class do
476
- list.items[body_start..]&.each { |form| walk_form(form, scope) }
522
+ body = list.items[body_start..] || []
523
+ if kind == :class
524
+ inside_class { body.each { |form| walk_form(form, scope) } }
525
+ else
526
+ inside_module_or_class { body.each { |form| walk_form(form, scope) } }
477
527
  end
478
528
  end
479
529