kapusta 0.7.0 → 0.9.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.
@@ -0,0 +1,693 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../ast'
4
+ require_relative '../compiler'
5
+
6
+ module Kapusta
7
+ class LSP
8
+ class ScopeWalker
9
+ Binding = Struct.new(:kind, :name, :line, :column, :end_column, :scope, :segments,
10
+ :sym, :in_module_or_class, :import_module, :import_key, keyword_init: true)
11
+ Reference = Struct.new(:name, :line, :column, :end_column, :scope, :sym,
12
+ :target, keyword_init: true)
13
+ Scope = Struct.new(:id, :parent, :bindings, :kind) do
14
+ def lookup(name)
15
+ bindings[name] || parent&.lookup(name)
16
+ end
17
+ end
18
+
19
+ SKIPPED_HEADS = %w[macros quasi-sym quasi-list
20
+ quasi-list-tail quasi-vec quasi-vec-tail quasi-hash quasi-gensym].freeze
21
+
22
+ DISPATCHERS = {
23
+ 'let' => :walk_let,
24
+ 'local' => :walk_local_var,
25
+ 'var' => :walk_local_var,
26
+ 'global' => :walk_global,
27
+ 'set' => :walk_set,
28
+ 'fn' => :walk_fn,
29
+ 'lambda' => :walk_fn,
30
+ 'λ' => :walk_fn,
31
+ 'for' => :walk_for,
32
+ 'each' => :walk_each_like,
33
+ 'collect' => :walk_each_like,
34
+ 'icollect' => :walk_each_like,
35
+ 'fcollect' => :walk_for_like,
36
+ 'accumulate' => :walk_accumulate,
37
+ 'faccumulate' => :walk_faccumulate,
38
+ 'case' => :walk_case_match,
39
+ 'match' => :walk_case_match,
40
+ 'try' => :walk_try,
41
+ 'module' => :walk_module_class,
42
+ 'class' => :walk_module_class,
43
+ 'hashfn' => :walk_hashfn,
44
+ 'macro' => :walk_macro_def,
45
+ 'import-macros' => :walk_import_macros,
46
+ 'ivar' => :walk_sigil_form,
47
+ 'cvar' => :walk_sigil_form,
48
+ 'gvar' => :walk_sigil_form
49
+ }.freeze
50
+
51
+ attr_reader :bindings, :references, :root_scope
52
+
53
+ def self.analyze(forms)
54
+ walker = new
55
+ walker.walk_top(forms)
56
+ walker
57
+ end
58
+
59
+ def initialize
60
+ @bindings = []
61
+ @references = []
62
+ @scope_seq = 0
63
+ @root_scope = make_scope(nil, :file)
64
+ @gvar_scope = make_scope(nil, :gvars)
65
+ @in_module_or_class = 0
66
+ @sigil_scope_stack = [make_sigil_scopes]
67
+ end
68
+
69
+ def walk_top(forms)
70
+ i = 0
71
+ while i < forms.length
72
+ form = forms[i]
73
+ if bodyless_header?(form)
74
+ walk_bodyless_header(form, forms[(i + 1)..] || [], @root_scope)
75
+ break
76
+ end
77
+
78
+ walk_form(form, @root_scope)
79
+ i += 1
80
+ end
81
+ end
82
+
83
+ def binding_at(line, column)
84
+ @bindings.each do |b|
85
+ return b if b.line == line && column >= b.column && column <= b.end_column
86
+ end
87
+ nil
88
+ end
89
+
90
+ def reference_at(line, column)
91
+ @references.each do |r|
92
+ return r if r.line == line && column >= r.column && column <= r.end_column
93
+ end
94
+ nil
95
+ end
96
+
97
+ def sym_at(line, column)
98
+ binding_at(line, column) || reference_at(line, column)
99
+ end
100
+
101
+ private
102
+
103
+ def make_scope(parent, kind)
104
+ @scope_seq += 1
105
+ Scope.new(@scope_seq, parent, {}, kind)
106
+ end
107
+
108
+ def bodyless_header?(form)
109
+ return false unless form.is_a?(List) && !form.empty? && form.head.is_a?(Sym)
110
+
111
+ case form.head.name
112
+ when 'module'
113
+ body = form.items[2..] || []
114
+ body.empty? || (body.length == 1 && bodyless_header?(body[0]))
115
+ when 'class'
116
+ _name_sym, _supers, body = split_class_args(form.items[1..] || [])
117
+ body.empty?
118
+ else
119
+ false
120
+ end
121
+ end
122
+
123
+ def walk_bodyless_header(form, remaining_forms, scope)
124
+ case form.head.name
125
+ when 'module'
126
+ name_sym = form.items[1]
127
+ add_constant_binding(name_sym, scope, :module) if name_sym.is_a?(Sym)
128
+ body = form.items[2..] || []
129
+ inside_module_or_class do
130
+ if body.length == 1 && bodyless_header?(body[0])
131
+ walk_bodyless_header(body[0], remaining_forms, scope)
132
+ else
133
+ remaining_forms.each { |item| walk_form(item, scope) }
134
+ end
135
+ end
136
+ when 'class'
137
+ name_sym, supers, = split_class_args(form.items[1..] || [])
138
+ supers&.items&.each { |item| walk_form(item, scope) }
139
+ add_constant_binding(name_sym, scope, :class) if name_sym.is_a?(Sym)
140
+ inside_class do
141
+ remaining_forms.each { |item| walk_form(item, scope) }
142
+ end
143
+ end
144
+ end
145
+
146
+ def split_class_args(args)
147
+ name_sym = args[0]
148
+ if args[1].is_a?(Vec)
149
+ [name_sym, args[1], args[2..] || []]
150
+ else
151
+ [name_sym, nil, args[1..] || []]
152
+ end
153
+ end
154
+
155
+ def inside_module_or_class
156
+ @in_module_or_class += 1
157
+ yield
158
+ ensure
159
+ @in_module_or_class -= 1
160
+ end
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
+
177
+ def walk_form(form, scope)
178
+ case form
179
+ when List then walk_list(form, scope)
180
+ when Vec then form.items.each { |item| walk_form(item, scope) }
181
+ when HashLit then walk_hash(form, scope)
182
+ when Sym then walk_reference(form, scope)
183
+ when Quasiquote then walk_quasi(form.form, scope)
184
+ when Unquote, UnquoteSplice then walk_form(form.form, scope)
185
+ end
186
+ end
187
+
188
+ def walk_hash(hash, scope)
189
+ hash.entries.each do |entry|
190
+ next unless entry.is_a?(Array)
191
+
192
+ _key, value = entry
193
+ walk_form(value, scope)
194
+ end
195
+ end
196
+
197
+ def walk_quasi(form, scope)
198
+ case form
199
+ when Unquote, UnquoteSplice then walk_form(form.form, scope)
200
+ when List, Vec then form.items.each { |item| walk_quasi(item, scope) }
201
+ when HashLit
202
+ form.entries.each do |entry|
203
+ next unless entry.is_a?(Array)
204
+
205
+ _key, value = entry
206
+ walk_quasi(value, scope)
207
+ end
208
+ end
209
+ end
210
+
211
+ def walk_list(list, scope)
212
+ return if list.empty?
213
+
214
+ head = list.head
215
+ unless head.is_a?(Sym)
216
+ list.items.each { |item| walk_form(item, scope) }
217
+ return
218
+ end
219
+
220
+ return if SKIPPED_HEADS.include?(head.name)
221
+
222
+ dispatcher = DISPATCHERS[head.name]
223
+ return send(dispatcher, list, scope) if dispatcher
224
+
225
+ list.items.each { |item| walk_form(item, scope) }
226
+ end
227
+
228
+ def walk_let(list, scope)
229
+ bindings_vec = list.items[1]
230
+ body = list.items[2..]
231
+ return unless bindings_vec.is_a?(Vec)
232
+
233
+ let_scope = make_scope(scope, :let)
234
+ items = bindings_vec.items
235
+ i = 0
236
+ while i < items.length
237
+ name_pat = items[i]
238
+ value = items[i + 1]
239
+ walk_form(value, let_scope) if value
240
+ bind_pattern(name_pat, let_scope, :let)
241
+ i += 2
242
+ end
243
+ body&.each { |form| walk_form(form, let_scope) }
244
+ end
245
+
246
+ def walk_local_var(list, scope)
247
+ kind = list.head.name == 'var' ? :var : :local
248
+ target = list.items[1]
249
+ value = list.items[2]
250
+ walk_form(value, scope) if value
251
+ bind_pattern(target, scope, kind)
252
+ end
253
+
254
+ def walk_global(list, _scope)
255
+ # Globals are not renamable; skip the binder name and walk only the value.
256
+ value = list.items[2]
257
+ walk_form(value, @root_scope) if value
258
+ end
259
+
260
+ def walk_hashfn(list, scope)
261
+ list.items[1..]&.each { |form| walk_form(form, scope) }
262
+ end
263
+
264
+ def walk_macro_def(list, scope)
265
+ items = list.items
266
+ name_sym = items[1]
267
+ params = items[2]
268
+ body = items[3..] || []
269
+ return unless name_sym.is_a?(Sym) && params.is_a?(Vec)
270
+
271
+ add_binding(name_sym, @root_scope, :macro)
272
+ fn_scope = make_scope(scope, :fn)
273
+ bind_param_vec(params, fn_scope)
274
+ body.each { |form| walk_form(form, fn_scope) }
275
+ end
276
+
277
+ def walk_import_macros(list, scope)
278
+ destructure = list.items[1]
279
+ module_arg = list.items[2]
280
+ return unless destructure.is_a?(HashLit)
281
+ return unless module_arg.is_a?(Symbol) || module_arg.is_a?(String)
282
+
283
+ module_label = module_arg.to_s.tr('_', '-')
284
+ destructure.pairs.each do |key, target|
285
+ next unless target.is_a?(Sym) && key.is_a?(Symbol)
286
+
287
+ add_import_macro_binding(target, scope, module_label, key)
288
+ end
289
+ end
290
+
291
+ def add_import_macro_binding(sym, _scope, module_label, import_key)
292
+ b = Binding.new(
293
+ kind: :macro_import,
294
+ name: sym.name,
295
+ line: sym.line,
296
+ column: sym.column,
297
+ end_column: sym.column + sym.name.length,
298
+ scope: @root_scope,
299
+ segments: sym.dotted? ? sym.segments : nil,
300
+ sym:,
301
+ in_module_or_class: false,
302
+ import_module: module_label,
303
+ import_key:
304
+ )
305
+ @bindings << b
306
+ @root_scope.bindings[sym.name] = b
307
+ b
308
+ end
309
+
310
+ def walk_set(list, scope)
311
+ target = list.items[1]
312
+ value = list.items[2]
313
+ walk_form(value, scope) if value
314
+ if target.is_a?(List)
315
+ walk_form(target, scope)
316
+ return
317
+ end
318
+ return unless target.is_a?(Sym) && !target.dotted?
319
+
320
+ existing = scope.lookup(target.name)
321
+ if existing
322
+ add_reference(target, scope, existing)
323
+ else
324
+ add_binding(target, scope, :set)
325
+ end
326
+ end
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
+
351
+ def walk_fn(list, scope)
352
+ items = list.items
353
+ if items[1].is_a?(Vec)
354
+ params = items[1]
355
+ body = items[2..]
356
+ fn_scope = make_scope(scope, :fn)
357
+ bind_param_vec(params, fn_scope)
358
+ body.each { |form| walk_form(form, fn_scope) }
359
+ elsif items[1].is_a?(Sym) && items[2].is_a?(Vec)
360
+ name_sym = items[1]
361
+ params = items[2]
362
+ body = items[3..]
363
+ kind = if method_definition_context?
364
+ :method
365
+ else
366
+ (scope == @root_scope ? :toplevel_fn : :fn_local)
367
+ end
368
+ fn_scope = make_scope(scope, :fn)
369
+ binding = add_binding(name_sym, scope, kind, lexical: kind != :method)
370
+ fn_scope.bindings[name_sym.name] = binding unless kind == :method
371
+ bind_param_vec(params, fn_scope)
372
+ body.each { |form| walk_form(form, fn_scope) }
373
+ else
374
+ items[1..]&.each { |item| walk_form(item, scope) }
375
+ end
376
+ end
377
+
378
+ def method_definition_context?
379
+ @in_module_or_class.positive?
380
+ end
381
+
382
+ def walk_for(list, scope)
383
+ bindings_vec = list.items[1]
384
+ body = list.items[2..]
385
+ return unless bindings_vec.is_a?(Vec)
386
+
387
+ for_scope = make_scope(scope, :for)
388
+ items = bindings_vec.items
389
+ counter = items[0]
390
+ i = 1
391
+ until_forms = []
392
+ while i < items.length
393
+ item = items[i]
394
+ if item.is_a?(Sym) && item.name == '&until'
395
+ until_forms << items[i + 1] if items[i + 1]
396
+ i += 2
397
+ else
398
+ walk_form(item, scope)
399
+ i += 1
400
+ end
401
+ end
402
+ bind_pattern(counter, for_scope, :for_counter) if counter
403
+ until_forms.each { |form| walk_form(form, for_scope) }
404
+ body&.each { |form| walk_form(form, for_scope) }
405
+ end
406
+
407
+ def walk_for_like(list, scope) = walk_for(list, scope)
408
+
409
+ def walk_each_like(list, scope)
410
+ bindings_vec = list.items[1]
411
+ body = list.items[2..]
412
+ return unless bindings_vec.is_a?(Vec)
413
+
414
+ items = bindings_vec.items
415
+ return if items.empty?
416
+
417
+ each_scope = make_scope(scope, :each)
418
+ iter_expr = items.last
419
+ binders = items[0..-2]
420
+ walk_form(iter_expr, scope)
421
+ binders.each { |b| bind_pattern(b, each_scope, :each_var) }
422
+ body&.each { |form| walk_form(form, each_scope) }
423
+ end
424
+
425
+ def walk_accumulate(list, scope)
426
+ bindings_vec = list.items[1]
427
+ body = list.items[2..]
428
+ return unless bindings_vec.is_a?(Vec)
429
+
430
+ items = bindings_vec.items
431
+ return if items.length < 4
432
+
433
+ acc_scope = make_scope(scope, :accumulate)
434
+ acc_name = items[0]
435
+ acc_init = items[1]
436
+ iter_items = items[2..]
437
+ iter_expr = iter_items.last
438
+ binders = iter_items[0...-1]
439
+ walk_form(acc_init, scope)
440
+ bind_pattern(acc_name, acc_scope, :accumulator)
441
+ walk_form(iter_expr, scope)
442
+ binders.each { |b| bind_pattern(b, acc_scope, :each_var) }
443
+ body&.each { |form| walk_form(form, acc_scope) }
444
+ end
445
+
446
+ def walk_faccumulate(list, scope)
447
+ bindings_vec = list.items[1]
448
+ body = list.items[2..]
449
+ return unless bindings_vec.is_a?(Vec)
450
+
451
+ items = bindings_vec.items
452
+ return if items.length < 5
453
+
454
+ acc_scope = make_scope(scope, :faccumulate)
455
+ acc_name = items[0]
456
+ acc_init = items[1]
457
+ counter = items[2]
458
+ walk_form(acc_init, scope)
459
+ items[3..]&.each { |form| walk_form(form, scope) }
460
+ bind_pattern(acc_name, acc_scope, :accumulator)
461
+ bind_pattern(counter, acc_scope, :for_counter)
462
+ body&.each { |form| walk_form(form, acc_scope) }
463
+ end
464
+
465
+ def walk_case_match(list, scope)
466
+ mode = list.head.name == 'match' ? :match : :case
467
+ subject = list.items[1]
468
+ arms = list.items[2..] || []
469
+ walk_form(subject, scope)
470
+ arms.each_slice(2) do |pattern, body|
471
+ arm_scope = make_scope(scope, :case_arm)
472
+ walk_pattern(pattern, arm_scope, scope, mode)
473
+ walk_form(body, arm_scope) if body
474
+ end
475
+ end
476
+
477
+ def walk_try(list, scope)
478
+ body = list.items[1]
479
+ clauses = list.items[2..] || []
480
+ walk_form(body, scope)
481
+ clauses.each do |clause|
482
+ next unless clause.is_a?(List)
483
+
484
+ head = clause.head
485
+ next unless head.is_a?(Sym)
486
+
487
+ if head.name == 'catch'
488
+ walk_catch(clause, scope)
489
+ elsif head.name == 'finally'
490
+ clause.items[1..]&.each { |form| walk_form(form, scope) }
491
+ end
492
+ end
493
+ end
494
+
495
+ def walk_catch(clause, scope)
496
+ rest = clause.items[1..]
497
+ if rest[0].is_a?(Sym) && (rest[0].name.match?(/\A[A-Z]/) || rest[0].dotted?)
498
+ klass = rest[0]
499
+ bind_sym = rest[1]
500
+ body = rest[2..]
501
+ walk_form(klass, scope)
502
+ else
503
+ bind_sym = rest[0]
504
+ body = rest[1..]
505
+ end
506
+ catch_scope = make_scope(scope, :catch)
507
+ bind_pattern(bind_sym, catch_scope, :catch) if bind_sym.is_a?(Sym)
508
+ body&.each { |form| walk_form(form, catch_scope) }
509
+ end
510
+
511
+ def walk_module_class(list, scope)
512
+ kind = list.head.name == 'module' ? :module : :class
513
+ name_sym = list.items[1]
514
+ body_start = 2
515
+ if kind == :class && list.items[2].is_a?(Vec)
516
+ list.items[2].items.each { |item| walk_form(item, scope) }
517
+ body_start = 3
518
+ end
519
+
520
+ add_constant_binding(name_sym, scope, kind) if name_sym.is_a?(Sym)
521
+
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) } }
527
+ end
528
+ end
529
+
530
+ def walk_reference(sym, scope)
531
+ return if hashfn_synthetic?(sym.name)
532
+ return if sym.is_a?(MacroSym) || sym.is_a?(AutoGensym)
533
+
534
+ target_name = sym.dotted? ? sym.segments.first : sym.name
535
+ return if target_name.nil? || target_name.empty?
536
+
537
+ target = scope.lookup(target_name)
538
+ return if target.nil? && Compiler::SPECIAL_FORMS.include?(sym.name)
539
+
540
+ add_reference(sym, scope, target)
541
+ end
542
+
543
+ def hashfn_synthetic?(name)
544
+ name == '$' || name == '$...' || name.match?(/\A\$\d\z/)
545
+ end
546
+
547
+ def bind_pattern(pattern, scope, kind)
548
+ case pattern
549
+ when Sym
550
+ return if pattern.name == '_'
551
+
552
+ add_binding(pattern, scope, kind)
553
+ when Vec
554
+ bind_vec_pattern(pattern, scope, kind)
555
+ when HashLit
556
+ bind_hash_pattern(pattern, scope, kind)
557
+ end
558
+ end
559
+
560
+ def bind_param_vec(vec, scope)
561
+ items = vec.items
562
+ i = 0
563
+ while i < items.length
564
+ item = items[i]
565
+ if item.is_a?(Sym) && item.name == '&'
566
+ rest = items[i + 1]
567
+ bind_pattern(rest, scope, :fn_param) if rest.is_a?(Sym) && rest.name != '_'
568
+ i += 2
569
+ elsif item.is_a?(Sym) && ['...', '_'].include?(item.name)
570
+ i += 1
571
+ else
572
+ bind_pattern(item, scope, :fn_param)
573
+ i += 1
574
+ end
575
+ end
576
+ end
577
+
578
+ def bind_vec_pattern(vec, scope, kind)
579
+ items = vec.items
580
+ i = 0
581
+ while i < items.length
582
+ item = items[i]
583
+ if item.is_a?(Sym) && item.name == '&'
584
+ rest = items[i + 1]
585
+ bind_pattern(rest, scope, kind) if rest
586
+ i += 2
587
+ else
588
+ bind_pattern(item, scope, kind)
589
+ i += 1
590
+ end
591
+ end
592
+ end
593
+
594
+ def bind_hash_pattern(hash, scope, kind)
595
+ hash.pairs.each do |pair|
596
+ bind_pattern(pair[1], scope, kind)
597
+ end
598
+ end
599
+
600
+ def walk_pattern(pattern, scope, outer_scope, mode)
601
+ case pattern
602
+ when Sym then walk_pattern_symbol(pattern, scope, outer_scope, mode)
603
+ when Vec then pattern.items.each { |item| walk_pattern_seq_item(item, scope, outer_scope, mode) }
604
+ when HashLit then pattern.pairs.each { |pair| walk_pattern(pair[1], scope, outer_scope, mode) }
605
+ when List then walk_pattern_list(pattern, scope, outer_scope, mode)
606
+ end
607
+ end
608
+
609
+ def walk_pattern_symbol(sym, scope, outer_scope, mode)
610
+ return if sym.name == '_'
611
+
612
+ if mode == :match && (existing = outer_scope.lookup(sym.name))
613
+ add_reference(sym, outer_scope, existing)
614
+ else
615
+ bind_pattern(sym, scope, :case_pattern)
616
+ end
617
+ end
618
+
619
+ def walk_pattern_seq_item(item, scope, outer_scope, mode)
620
+ return if item.is_a?(Sym) && item.name == '&'
621
+
622
+ walk_pattern(item, scope, outer_scope, mode)
623
+ end
624
+
625
+ def walk_pattern_list(list, scope, outer_scope, mode)
626
+ head = list.head
627
+ if head.is_a?(Sym) && head.name == 'where'
628
+ inner = list.items[1]
629
+ guards = list.items[2..]
630
+ walk_pattern(inner, scope, outer_scope, mode)
631
+ guards&.each { |g| walk_form(g, scope) }
632
+ elsif head.is_a?(Sym) && head.name == 'or'
633
+ list.items[1..]&.each { |alt| walk_pattern(alt, scope, outer_scope, mode) }
634
+ elsif head.is_a?(Sym) && head.name == '=' && list.items.length == 2
635
+ name_sym = list.items[1]
636
+ if name_sym.is_a?(Sym) && (existing = outer_scope.lookup(name_sym.name))
637
+ add_reference(name_sym, outer_scope, existing)
638
+ end
639
+ else
640
+ list.items.each { |item| walk_pattern(item, scope, outer_scope, mode) }
641
+ end
642
+ end
643
+
644
+ def add_binding(sym, scope, kind, lexical: true)
645
+ return unless sym.is_a?(Sym)
646
+
647
+ b = Binding.new(
648
+ kind:,
649
+ name: sym.name,
650
+ line: sym.line,
651
+ column: sym.column,
652
+ end_column: sym.column + sym.name.length,
653
+ scope:,
654
+ segments: sym.dotted? ? sym.segments : nil,
655
+ sym:,
656
+ in_module_or_class: @in_module_or_class.positive?
657
+ )
658
+ @bindings << b
659
+ scope.bindings[sym.name] = b if lexical
660
+ b
661
+ end
662
+
663
+ def add_constant_binding(sym, scope, kind)
664
+ b = Binding.new(
665
+ kind:,
666
+ name: sym.name,
667
+ line: sym.line,
668
+ column: sym.column,
669
+ end_column: sym.column + sym.name.length,
670
+ scope:,
671
+ segments: sym.segments,
672
+ sym:,
673
+ in_module_or_class: @in_module_or_class.positive?
674
+ )
675
+ @bindings << b
676
+ # Constants stay out of scope.bindings: they resolve workspace-wide, not lexically.
677
+ b
678
+ end
679
+
680
+ def add_reference(sym, scope, target)
681
+ @references << Reference.new(
682
+ name: sym.name,
683
+ line: sym.line,
684
+ column: sym.column,
685
+ end_column: sym.column + sym.name.length,
686
+ scope:,
687
+ sym:,
688
+ target:
689
+ )
690
+ end
691
+ end
692
+ end
693
+ end