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