type-guessr 0.0.1

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,1649 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prism"
4
+ require_relative "../ir/nodes"
5
+ require_relative "../types"
6
+
7
+ module TypeGuessr
8
+ module Core
9
+ module Converter
10
+ # Converts Prism AST to IR graph (reverse dependency graph)
11
+ # Each IR node points to nodes it depends on
12
+ class PrismConverter
13
+ # Context for tracking variable bindings during conversion
14
+ class Context
15
+ attr_reader :variables
16
+ attr_accessor :current_class, :current_method, :in_singleton_method
17
+
18
+ def initialize(parent = nil)
19
+ @parent = parent
20
+ @variables = {} # name => node
21
+ @instance_variables = {} # @name => node (only for class-level context)
22
+ @constants = {} # name => dependency node (for constant alias tracking)
23
+ @scope_type = nil # :class, :method, :block, :top_level
24
+ @current_class = nil
25
+ @current_method = nil
26
+ @in_singleton_method = false
27
+ end
28
+
29
+ def register_variable(name, node)
30
+ @variables[name] = node
31
+ end
32
+
33
+ def lookup_variable(name)
34
+ @variables[name] || @parent&.lookup_variable(name)
35
+ end
36
+
37
+ # Register an instance variable at the class level
38
+ # Instance variables are shared across all methods in a class
39
+ def register_instance_variable(name, node)
40
+ if @scope_type == :class
41
+ @instance_variables[name] = node
42
+ elsif @parent
43
+ @parent.register_instance_variable(name, node)
44
+ else
45
+ # Top-level instance variable, store locally
46
+ @instance_variables[name] = node
47
+ end
48
+ end
49
+
50
+ # Lookup an instance variable from the class level
51
+ def lookup_instance_variable(name)
52
+ if @scope_type == :class
53
+ @instance_variables[name]
54
+ elsif @parent
55
+ @parent.lookup_instance_variable(name)
56
+ else
57
+ @instance_variables[name]
58
+ end
59
+ end
60
+
61
+ # Register a constant's dependency node for alias tracking
62
+ def register_constant(name, dependency_node)
63
+ @constants[name] = dependency_node
64
+ end
65
+
66
+ # Lookup a constant's dependency node (for alias resolution)
67
+ def lookup_constant(name)
68
+ @constants[name] || @parent&.lookup_constant(name)
69
+ end
70
+
71
+ def fork(scope_type)
72
+ child = Context.new(self)
73
+ child.instance_variable_set(:@scope_type, scope_type)
74
+ child.current_class = current_class_name
75
+ child.current_method = current_method_name
76
+ child.in_singleton_method = @in_singleton_method
77
+ child
78
+ end
79
+
80
+ def scope_type
81
+ @scope_type || @parent&.scope_type
82
+ end
83
+
84
+ # Get the current class name (from this context or parent)
85
+ def current_class_name
86
+ @current_class || @parent&.current_class_name
87
+ end
88
+
89
+ # Get the current method name (from this context or parent)
90
+ def current_method_name
91
+ @current_method || @parent&.current_method_name
92
+ end
93
+
94
+ # Generate scope_id for node lookup (e.g., "User#save" or "User" or "")
95
+ def scope_id
96
+ class_path = current_class_name || ""
97
+ method_name = current_method_name
98
+ if method_name
99
+ "#{class_path}##{method_name}"
100
+ else
101
+ class_path
102
+ end
103
+ end
104
+
105
+ # Get variables that were defined/modified in this context (not from parent)
106
+ def local_variables
107
+ @variables.keys
108
+ end
109
+ end
110
+
111
+ def initialize
112
+ @literal_type_cache = {}
113
+ end
114
+
115
+ # Convert Prism AST to IR graph
116
+ # @param prism_node [Prism::Node] Prism AST node
117
+ # @param context [Context] Conversion context
118
+ # @return [IR::Node, nil] IR node
119
+ def convert(prism_node, context = Context.new)
120
+ case prism_node
121
+ when Prism::IntegerNode, Prism::FloatNode, Prism::StringNode,
122
+ Prism::SymbolNode, Prism::TrueNode, Prism::FalseNode,
123
+ Prism::NilNode, Prism::InterpolatedStringNode, Prism::RangeNode,
124
+ Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode,
125
+ Prism::ImaginaryNode, Prism::RationalNode,
126
+ Prism::XStringNode, Prism::InterpolatedXStringNode
127
+ convert_literal(prism_node)
128
+
129
+ when Prism::ArrayNode
130
+ convert_array_literal(prism_node, context)
131
+
132
+ when Prism::HashNode
133
+ convert_hash_literal(prism_node, context)
134
+
135
+ when Prism::KeywordHashNode
136
+ convert_keyword_hash(prism_node, context)
137
+
138
+ when Prism::LocalVariableWriteNode
139
+ convert_local_variable_write(prism_node, context)
140
+
141
+ when Prism::LocalVariableReadNode
142
+ convert_local_variable_read(prism_node, context)
143
+
144
+ when Prism::InstanceVariableWriteNode
145
+ convert_instance_variable_write(prism_node, context)
146
+
147
+ when Prism::InstanceVariableReadNode
148
+ convert_instance_variable_read(prism_node, context)
149
+
150
+ when Prism::ClassVariableWriteNode
151
+ convert_class_variable_write(prism_node, context)
152
+
153
+ when Prism::ClassVariableReadNode
154
+ convert_class_variable_read(prism_node, context)
155
+
156
+ # Compound assignments (||=, &&=, +=, etc.)
157
+ when Prism::LocalVariableOrWriteNode
158
+ convert_local_variable_or_write(prism_node, context)
159
+
160
+ when Prism::LocalVariableAndWriteNode
161
+ convert_local_variable_and_write(prism_node, context)
162
+
163
+ when Prism::LocalVariableOperatorWriteNode
164
+ convert_local_variable_operator_write(prism_node, context)
165
+
166
+ when Prism::InstanceVariableOrWriteNode
167
+ convert_instance_variable_or_write(prism_node, context)
168
+
169
+ when Prism::InstanceVariableAndWriteNode
170
+ convert_instance_variable_and_write(prism_node, context)
171
+
172
+ when Prism::InstanceVariableOperatorWriteNode
173
+ convert_instance_variable_operator_write(prism_node, context)
174
+
175
+ when Prism::CallNode
176
+ convert_call(prism_node, context)
177
+
178
+ when Prism::IfNode
179
+ convert_if(prism_node, context)
180
+
181
+ when Prism::UnlessNode
182
+ convert_unless(prism_node, context)
183
+
184
+ when Prism::CaseNode
185
+ convert_case(prism_node, context)
186
+
187
+ when Prism::CaseMatchNode
188
+ convert_case_match(prism_node, context)
189
+
190
+ when Prism::StatementsNode
191
+ convert_statements(prism_node, context)
192
+
193
+ when Prism::DefNode
194
+ convert_def(prism_node, context)
195
+
196
+ when Prism::ConstantReadNode, Prism::ConstantPathNode
197
+ convert_constant_read(prism_node, context)
198
+
199
+ when Prism::ConstantWriteNode
200
+ convert_constant_write(prism_node, context)
201
+
202
+ when Prism::ClassNode, Prism::ModuleNode
203
+ convert_class_or_module(prism_node, context)
204
+
205
+ when Prism::SingletonClassNode
206
+ convert_singleton_class(prism_node, context)
207
+
208
+ when Prism::ReturnNode
209
+ # Return statement - wrap in ReturnNode to track explicit returns
210
+ value_node = if prism_node.arguments&.arguments&.first
211
+ convert(prism_node.arguments.arguments.first, context)
212
+ else
213
+ # return with no value returns nil
214
+ IR::LiteralNode.new(
215
+ type: Types::ClassInstance.new("NilClass"),
216
+ literal_value: nil,
217
+ values: nil,
218
+ loc: convert_loc(prism_node.location)
219
+ )
220
+ end
221
+ IR::ReturnNode.new(
222
+ value: value_node,
223
+ loc: convert_loc(prism_node.location)
224
+ )
225
+
226
+ when Prism::SelfNode
227
+ # self keyword - returns the current class instance or singleton
228
+ IR::SelfNode.new(
229
+ class_name: context.current_class_name || "Object",
230
+ singleton: context.in_singleton_method,
231
+ loc: convert_loc(prism_node.location)
232
+ )
233
+
234
+ when Prism::BeginNode
235
+ convert_begin(prism_node, context)
236
+
237
+ when Prism::RescueNode
238
+ # Rescue clause - convert body statements
239
+ convert_statements_body(prism_node.statements&.body, context)
240
+ end
241
+ end
242
+
243
+ private
244
+
245
+ def convert_literal(prism_node)
246
+ type = infer_literal_type(prism_node)
247
+ literal_value = extract_literal_value(prism_node)
248
+ IR::LiteralNode.new(
249
+ type: type,
250
+ literal_value: literal_value,
251
+ values: nil,
252
+ loc: convert_loc(prism_node.location)
253
+ )
254
+ end
255
+
256
+ # Extract the actual value from a literal node (for Symbol, Integer, String)
257
+ def extract_literal_value(prism_node)
258
+ case prism_node
259
+ when Prism::SymbolNode
260
+ prism_node.value.to_sym
261
+ when Prism::IntegerNode
262
+ prism_node.value
263
+ when Prism::StringNode
264
+ prism_node.content
265
+ end
266
+ end
267
+
268
+ def convert_array_literal(prism_node, context)
269
+ type = infer_array_element_type(prism_node)
270
+
271
+ # Convert each element to an IR node
272
+ value_nodes = prism_node.elements.filter_map do |elem|
273
+ next if elem.nil?
274
+
275
+ case elem
276
+ when Prism::SplatNode
277
+ # *arr → convert to CallNode for to_a
278
+ splat_expr = convert(elem.expression, context)
279
+ IR::CallNode.new(
280
+ method: :to_a,
281
+ receiver: splat_expr,
282
+ args: [],
283
+ block_params: [],
284
+ block_body: nil,
285
+ has_block: false,
286
+ loc: convert_loc(elem.location)
287
+ )
288
+ else
289
+ convert(elem, context)
290
+ end
291
+ end
292
+
293
+ IR::LiteralNode.new(
294
+ type: type,
295
+ literal_value: nil,
296
+ values: value_nodes.empty? ? nil : value_nodes,
297
+ loc: convert_loc(prism_node.location)
298
+ )
299
+ end
300
+
301
+ def convert_hash_literal(prism_node, context)
302
+ type = infer_hash_element_types(prism_node)
303
+ build_hash_literal_node(prism_node, type, context)
304
+ end
305
+
306
+ # Convert KeywordHashNode (keyword arguments in method calls like `foo(a: 1, b: x)`)
307
+ def convert_keyword_hash(prism_node, context)
308
+ type = infer_keyword_hash_type(prism_node)
309
+ build_hash_literal_node(prism_node, type, context)
310
+ end
311
+
312
+ # Shared helper for hash-like nodes (HashNode, KeywordHashNode)
313
+ def build_hash_literal_node(prism_node, type, context)
314
+ value_nodes = prism_node.elements.filter_map do |elem|
315
+ case elem
316
+ when Prism::AssocNode
317
+ convert(elem.value, context)
318
+ when Prism::AssocSplatNode
319
+ convert(elem.value, context)
320
+ end
321
+ end
322
+
323
+ IR::LiteralNode.new(
324
+ type: type,
325
+ literal_value: nil,
326
+ values: value_nodes.empty? ? nil : value_nodes,
327
+ loc: convert_loc(prism_node.location)
328
+ )
329
+ end
330
+
331
+ # Infer type for KeywordHashNode (always has symbol keys)
332
+ def infer_keyword_hash_type(keyword_hash_node)
333
+ return Types::HashShape.new({}) if keyword_hash_node.elements.empty?
334
+
335
+ fields = keyword_hash_node.elements.each_with_object({}) do |elem, hash|
336
+ next unless elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
337
+
338
+ hash[elem.key.value.to_sym] = infer_literal_type(elem.value)
339
+ end
340
+ Types::HashShape.new(fields)
341
+ end
342
+
343
+ def infer_literal_type(prism_node)
344
+ case prism_node
345
+ when Prism::IntegerNode
346
+ Types::ClassInstance.new("Integer")
347
+ when Prism::FloatNode
348
+ Types::ClassInstance.new("Float")
349
+ when Prism::StringNode, Prism::InterpolatedStringNode
350
+ Types::ClassInstance.new("String")
351
+ when Prism::SymbolNode
352
+ Types::ClassInstance.new("Symbol")
353
+ when Prism::TrueNode
354
+ Types::ClassInstance.new("TrueClass")
355
+ when Prism::FalseNode
356
+ Types::ClassInstance.new("FalseClass")
357
+ when Prism::NilNode
358
+ Types::ClassInstance.new("NilClass")
359
+ when Prism::ArrayNode
360
+ # Infer element type from array contents
361
+ infer_array_element_type(prism_node)
362
+ when Prism::HashNode
363
+ infer_hash_element_types(prism_node)
364
+ when Prism::RangeNode
365
+ infer_range_element_type(prism_node)
366
+ when Prism::RegularExpressionNode, Prism::InterpolatedRegularExpressionNode
367
+ Types::ClassInstance.new("Regexp")
368
+ when Prism::ImaginaryNode
369
+ Types::ClassInstance.new("Complex")
370
+ when Prism::RationalNode
371
+ Types::ClassInstance.new("Rational")
372
+ when Prism::XStringNode, Prism::InterpolatedXStringNode
373
+ Types::ClassInstance.new("String")
374
+ else
375
+ Types::Unknown.instance
376
+ end
377
+ end
378
+
379
+ def infer_range_element_type(range_node)
380
+ left_type = range_node.left ? infer_literal_type(range_node.left) : nil
381
+ right_type = range_node.right ? infer_literal_type(range_node.right) : nil
382
+
383
+ types = [left_type, right_type].compact
384
+
385
+ # No bounds at all (shouldn't happen in valid Ruby, but handle gracefully)
386
+ return Types::RangeType.new if types.empty?
387
+
388
+ unique_types = types.uniq
389
+
390
+ element_type = if unique_types.size == 1
391
+ unique_types.first
392
+ else
393
+ Types::Union.new(unique_types)
394
+ end
395
+
396
+ Types::RangeType.new(element_type)
397
+ end
398
+
399
+ def convert_local_variable_write(prism_node, context)
400
+ value_node = convert(prism_node.value, context)
401
+ write_node = IR::LocalWriteNode.new(
402
+ name: prism_node.name,
403
+ value: value_node,
404
+ called_methods: [],
405
+ loc: convert_loc(prism_node.location)
406
+ )
407
+ context.register_variable(prism_node.name, write_node)
408
+ write_node
409
+ end
410
+
411
+ def convert_local_variable_read(prism_node, context)
412
+ # Look up the most recent assignment
413
+ write_node = context.lookup_variable(prism_node.name)
414
+ # Share called_methods array with the write node/parameter for method-based inference
415
+ called_methods = if write_node.is_a?(IR::LocalWriteNode) || write_node.is_a?(IR::ParamNode)
416
+ write_node.called_methods
417
+ else
418
+ []
419
+ end
420
+
421
+ IR::LocalReadNode.new(
422
+ name: prism_node.name,
423
+ write_node: write_node,
424
+ called_methods: called_methods,
425
+ loc: convert_loc(prism_node.location)
426
+ )
427
+ end
428
+
429
+ def convert_instance_variable_write(prism_node, context)
430
+ value_node = convert(prism_node.value, context)
431
+ class_name = context.current_class_name
432
+
433
+ # Share called_methods with the underlying param/variable node for type propagation
434
+ called_methods = if value_node.is_a?(IR::LocalReadNode) || value_node.is_a?(IR::ParamNode)
435
+ value_node.called_methods
436
+ else
437
+ []
438
+ end
439
+
440
+ write_node = IR::InstanceVariableWriteNode.new(
441
+ name: prism_node.name,
442
+ class_name: class_name,
443
+ value: value_node,
444
+ called_methods: called_methods,
445
+ loc: convert_loc(prism_node.location)
446
+ )
447
+ # Register at class level so it's visible across methods
448
+ context.register_instance_variable(prism_node.name, write_node)
449
+ write_node
450
+ end
451
+
452
+ def convert_instance_variable_read(prism_node, context)
453
+ # Look up from class level first
454
+ write_node = context.lookup_instance_variable(prism_node.name)
455
+ class_name = context.current_class_name
456
+ called_methods = if write_node.is_a?(IR::InstanceVariableWriteNode) || write_node.is_a?(IR::ParamNode)
457
+ write_node.called_methods
458
+ else
459
+ []
460
+ end
461
+
462
+ IR::InstanceVariableReadNode.new(
463
+ name: prism_node.name,
464
+ class_name: class_name,
465
+ write_node: write_node,
466
+ called_methods: called_methods,
467
+ loc: convert_loc(prism_node.location)
468
+ )
469
+ end
470
+
471
+ def convert_class_variable_write(prism_node, context)
472
+ value_node = convert(prism_node.value, context)
473
+ class_name = context.current_class_name
474
+
475
+ # Share called_methods with the underlying param/variable node for type propagation
476
+ called_methods = if value_node.is_a?(IR::LocalReadNode) || value_node.is_a?(IR::ParamNode)
477
+ value_node.called_methods
478
+ else
479
+ []
480
+ end
481
+
482
+ write_node = IR::ClassVariableWriteNode.new(
483
+ name: prism_node.name,
484
+ class_name: class_name,
485
+ value: value_node,
486
+ called_methods: called_methods,
487
+ loc: convert_loc(prism_node.location)
488
+ )
489
+ context.register_variable(prism_node.name, write_node)
490
+ write_node
491
+ end
492
+
493
+ def convert_class_variable_read(prism_node, context)
494
+ write_node = context.lookup_variable(prism_node.name)
495
+ class_name = context.current_class_name
496
+ called_methods = if write_node.is_a?(IR::ClassVariableWriteNode) || write_node.is_a?(IR::ParamNode)
497
+ write_node.called_methods
498
+ else
499
+ []
500
+ end
501
+
502
+ IR::ClassVariableReadNode.new(
503
+ name: prism_node.name,
504
+ class_name: class_name,
505
+ write_node: write_node,
506
+ called_methods: called_methods,
507
+ loc: convert_loc(prism_node.location)
508
+ )
509
+ end
510
+
511
+ # Compound assignment: x ||= value
512
+ # Result type is union of original and new value type
513
+ def convert_local_variable_or_write(prism_node, context)
514
+ convert_or_write(prism_node, context, :local)
515
+ end
516
+
517
+ # Compound assignment: x &&= value
518
+ # Result type is union of original and new value type
519
+ def convert_local_variable_and_write(prism_node, context)
520
+ convert_and_write(prism_node, context, :local)
521
+ end
522
+
523
+ # Compound assignment: x += value, x -= value, etc.
524
+ # Result type depends on the operator method return type
525
+ def convert_local_variable_operator_write(prism_node, context)
526
+ convert_operator_write(prism_node, context, :local)
527
+ end
528
+
529
+ def convert_instance_variable_or_write(prism_node, context)
530
+ convert_or_write(prism_node, context, :instance)
531
+ end
532
+
533
+ def convert_instance_variable_and_write(prism_node, context)
534
+ convert_and_write(prism_node, context, :instance)
535
+ end
536
+
537
+ def convert_instance_variable_operator_write(prism_node, context)
538
+ convert_operator_write(prism_node, context, :instance)
539
+ end
540
+
541
+ # Generic ||= handler
542
+ # x ||= value means: if x is nil/false, x = value, else keep x
543
+ # Type is union of original type and value type
544
+ def convert_or_write(prism_node, context, kind)
545
+ original_node = lookup_by_kind(prism_node.name, kind, context)
546
+ value_node = convert(prism_node.value, context)
547
+
548
+ # Create merge node for union type (original | value)
549
+ branches = []
550
+ branches << original_node if original_node
551
+ branches << value_node
552
+
553
+ merge_node = if branches.size == 1
554
+ branches.first
555
+ else
556
+ IR::MergeNode.new(
557
+ branches: branches,
558
+ loc: convert_loc(prism_node.location)
559
+ )
560
+ end
561
+
562
+ # Create write node with merged value
563
+ write_node = create_write_node(prism_node.name, kind, merge_node, context, prism_node.location)
564
+ register_by_kind(prism_node.name, write_node, kind, context)
565
+ write_node
566
+ end
567
+
568
+ # Generic &&= handler
569
+ # x &&= value means: if x is truthy, x = value, else keep x
570
+ # Type is union of original type and value type
571
+ def convert_and_write(prism_node, context, kind)
572
+ original_node = lookup_by_kind(prism_node.name, kind, context)
573
+ value_node = convert(prism_node.value, context)
574
+
575
+ # Create merge node for union type (original | value)
576
+ branches = []
577
+ branches << original_node if original_node
578
+ branches << value_node
579
+
580
+ merge_node = if branches.size == 1
581
+ branches.first
582
+ else
583
+ IR::MergeNode.new(
584
+ branches: branches,
585
+ loc: convert_loc(prism_node.location)
586
+ )
587
+ end
588
+
589
+ write_node = create_write_node(prism_node.name, kind, merge_node, context, prism_node.location)
590
+ register_by_kind(prism_node.name, write_node, kind, context)
591
+ write_node
592
+ end
593
+
594
+ # Generic operator write handler (+=, -=, *=, etc.)
595
+ # x += value is equivalent to x = x.+(value)
596
+ # Type is the return type of the operator method
597
+ def convert_operator_write(prism_node, context, kind)
598
+ original_node = lookup_by_kind(prism_node.name, kind, context)
599
+ value_node = convert(prism_node.value, context)
600
+
601
+ # Create a call node representing x.operator(value)
602
+ call_node = IR::CallNode.new(
603
+ method: prism_node.binary_operator,
604
+ receiver: original_node,
605
+ args: [value_node],
606
+ block_params: [],
607
+ block_body: nil,
608
+ has_block: false,
609
+ loc: convert_loc(prism_node.location)
610
+ )
611
+
612
+ # Create write node with call result as value
613
+ write_node = create_write_node(prism_node.name, kind, call_node, context, prism_node.location)
614
+ register_by_kind(prism_node.name, write_node, kind, context)
615
+ write_node
616
+ end
617
+
618
+ # Helper to create the appropriate write node type based on kind
619
+ def create_write_node(name, kind, value, context, location)
620
+ loc = convert_loc(location)
621
+ case kind
622
+ when :local
623
+ IR::LocalWriteNode.new(
624
+ name: name,
625
+ value: value,
626
+ called_methods: [],
627
+ loc: loc
628
+ )
629
+ when :instance
630
+ IR::InstanceVariableWriteNode.new(
631
+ name: name,
632
+ class_name: context.current_class_name,
633
+ value: value,
634
+ called_methods: [],
635
+ loc: loc
636
+ )
637
+ when :class
638
+ IR::ClassVariableWriteNode.new(
639
+ name: name,
640
+ class_name: context.current_class_name,
641
+ value: value,
642
+ called_methods: [],
643
+ loc: loc
644
+ )
645
+ end
646
+ end
647
+
648
+ # Helper to lookup variable by kind
649
+ def lookup_by_kind(name, kind, context)
650
+ case kind
651
+ when :instance
652
+ context.lookup_instance_variable(name)
653
+ else
654
+ context.lookup_variable(name)
655
+ end
656
+ end
657
+
658
+ # Helper to register variable by kind
659
+ def register_by_kind(name, node, kind, context)
660
+ case kind
661
+ when :instance
662
+ context.register_instance_variable(name, node)
663
+ else
664
+ context.register_variable(name, node)
665
+ end
666
+ end
667
+
668
+ def convert_call(prism_node, context)
669
+ # Convert receiver - if nil and inside a class, create implicit SelfNode
670
+ receiver_node = if prism_node.receiver
671
+ convert(prism_node.receiver, context)
672
+ elsif context.current_class_name
673
+ IR::SelfNode.new(
674
+ class_name: context.current_class_name,
675
+ singleton: context.in_singleton_method,
676
+ loc: convert_loc(prism_node.location)
677
+ )
678
+ end
679
+
680
+ args = prism_node.arguments&.arguments&.map { |arg| convert(arg, context) } || []
681
+
682
+ has_block = !prism_node.block.nil?
683
+
684
+ # Track method call on receiver for method-based type inference
685
+ receiver_node.called_methods << prism_node.name if variable_node?(receiver_node) && !receiver_node.called_methods.include?(prism_node.name)
686
+
687
+ # Handle container mutating methods (Hash#[]=, Array#[]=, Array#<<)
688
+ receiver_node = handle_container_mutation(prism_node, receiver_node, args, context) if container_mutating_method?(prism_node.name, receiver_node)
689
+
690
+ call_node = IR::CallNode.new(
691
+ method: prism_node.name,
692
+ receiver: receiver_node,
693
+ args: args,
694
+ block_params: [],
695
+ block_body: nil,
696
+ has_block: has_block,
697
+ loc: convert_loc(prism_node.location)
698
+ )
699
+
700
+ # Handle block if present (but not block arguments like &block)
701
+ if prism_node.block.is_a?(Prism::BlockNode)
702
+ block_body = convert_block(prism_node.block, call_node, context)
703
+ # Recreate CallNode with block_body since Data.define is immutable
704
+ call_node = IR::CallNode.new(
705
+ method: prism_node.name,
706
+ receiver: receiver_node,
707
+ args: args,
708
+ block_params: call_node.block_params,
709
+ block_body: block_body,
710
+ has_block: true,
711
+ loc: convert_loc(prism_node.location)
712
+ )
713
+ end
714
+
715
+ call_node
716
+ end
717
+
718
+ # Check if node is any variable node (for method call tracking)
719
+ def variable_node?(node)
720
+ node.is_a?(IR::LocalWriteNode) ||
721
+ node.is_a?(IR::LocalReadNode) ||
722
+ node.is_a?(IR::InstanceVariableWriteNode) ||
723
+ node.is_a?(IR::InstanceVariableReadNode) ||
724
+ node.is_a?(IR::ClassVariableWriteNode) ||
725
+ node.is_a?(IR::ClassVariableReadNode) ||
726
+ node.is_a?(IR::ParamNode)
727
+ end
728
+
729
+ # Check if node is a local variable node (for indexed assignment)
730
+ def local_variable_node?(node)
731
+ node.is_a?(IR::LocalWriteNode) || node.is_a?(IR::LocalReadNode)
732
+ end
733
+
734
+ def extract_literal_type(ir_node)
735
+ case ir_node
736
+ when IR::LiteralNode
737
+ ir_node.type
738
+ else
739
+ Types::Unknown.instance
740
+ end
741
+ end
742
+
743
+ def widen_to_hash_type(original_type, key_arg, value_type)
744
+ # When mixing key types, widen to generic HashType
745
+ new_key_type = infer_key_type(key_arg)
746
+
747
+ case original_type
748
+ when Types::HashShape
749
+ # HashShape with symbol keys + non-symbol key -> widen to Hash[Symbol | NewKeyType, ValueUnion]
750
+ original_key_type = Types::ClassInstance.new("Symbol")
751
+ original_value_types = original_type.fields.values.uniq
752
+ all_value_types = (original_value_types + [value_type]).uniq
753
+
754
+ combined_key_type = Types::Union.new([original_key_type, new_key_type].uniq)
755
+ combined_value_type = all_value_types.size == 1 ? all_value_types.first : Types::Union.new(all_value_types)
756
+
757
+ Types::HashType.new(combined_key_type, combined_value_type)
758
+ when Types::HashType
759
+ # Already a HashType, just union the key and value types
760
+ combined_key_type = union_types(original_type.key_type, new_key_type)
761
+ combined_value_type = union_types(original_type.value_type, value_type)
762
+ Types::HashType.new(combined_key_type, combined_value_type)
763
+ else
764
+ Types::HashType.new(new_key_type, value_type)
765
+ end
766
+ end
767
+
768
+ def union_types(type1, type2)
769
+ return type2 if type1.nil? || type1.is_a?(Types::Unknown)
770
+ return type1 if type2.nil? || type2.is_a?(Types::Unknown)
771
+ return type1 if type1 == type2
772
+
773
+ types = []
774
+ types += type1.is_a?(Types::Union) ? type1.types : [type1]
775
+ types += type2.is_a?(Types::Union) ? type2.types : [type2]
776
+ Types::Union.new(types.uniq)
777
+ end
778
+
779
+ def infer_key_type(key_arg)
780
+ case key_arg
781
+ when Prism::SymbolNode
782
+ Types::ClassInstance.new("Symbol")
783
+ when Prism::StringNode
784
+ Types::ClassInstance.new("String")
785
+ when Prism::IntegerNode
786
+ Types::ClassInstance.new("Integer")
787
+ else
788
+ Types::Unknown.instance
789
+ end
790
+ end
791
+
792
+ # Check if method is a container mutating method
793
+ def container_mutating_method?(method, receiver_node)
794
+ return false unless local_variable_node?(receiver_node)
795
+
796
+ receiver_type = get_receiver_type(receiver_node)
797
+ return false unless receiver_type
798
+
799
+ case method
800
+ when :[]= then hash_like?(receiver_type) || array_like?(receiver_type)
801
+ when :<< then array_like?(receiver_type)
802
+ else false
803
+ end
804
+ end
805
+
806
+ # Get receiver's current type
807
+ def get_receiver_type(receiver_node)
808
+ return nil unless receiver_node.respond_to?(:write_node)
809
+
810
+ write_node = receiver_node.write_node
811
+ return nil unless write_node
812
+ return nil unless write_node.respond_to?(:value)
813
+
814
+ value = write_node.value
815
+ return nil unless value.respond_to?(:type)
816
+
817
+ value.type
818
+ end
819
+
820
+ # Check if type is hash-like
821
+ def hash_like?(type)
822
+ type.is_a?(Types::HashShape) || type.is_a?(Types::HashType)
823
+ end
824
+
825
+ # Check if type is array-like
826
+ def array_like?(type)
827
+ type.is_a?(Types::ArrayType)
828
+ end
829
+
830
+ # Handle container mutation by creating new LocalWriteNode with merged type
831
+ def handle_container_mutation(prism_node, receiver_node, args, context)
832
+ merged_type = compute_merged_type(receiver_node, prism_node.name, args, prism_node)
833
+ return receiver_node unless merged_type
834
+
835
+ # Create new LocalWriteNode with merged type
836
+ new_write = IR::LocalWriteNode.new(
837
+ name: receiver_node.name,
838
+ value: IR::LiteralNode.new(type: merged_type, literal_value: nil, values: nil, loc: receiver_node.loc),
839
+ called_methods: receiver_node.called_methods,
840
+ loc: convert_loc(prism_node.location)
841
+ )
842
+
843
+ # Register for next line references
844
+ context.register_variable(receiver_node.name, new_write)
845
+
846
+ # Return new LocalReadNode pointing to new write_node
847
+ IR::LocalReadNode.new(
848
+ name: receiver_node.name,
849
+ write_node: new_write,
850
+ called_methods: receiver_node.called_methods,
851
+ loc: receiver_node.loc
852
+ )
853
+ end
854
+
855
+ # Compute merged type for container mutation
856
+ def compute_merged_type(receiver_node, method, args, prism_node)
857
+ original_type = get_receiver_type(receiver_node)
858
+ return nil unless original_type
859
+
860
+ case method
861
+ when :[]=
862
+ if hash_like?(original_type)
863
+ compute_hash_assignment_type(original_type, args, prism_node)
864
+ elsif array_like?(original_type)
865
+ compute_array_assignment_type(original_type, args)
866
+ end
867
+ when :<<
868
+ compute_array_append_type(original_type, args) if array_like?(original_type)
869
+ end
870
+ end
871
+
872
+ # Compute Hash type after indexed assignment
873
+ def compute_hash_assignment_type(original_type, args, prism_node)
874
+ return nil unless args.size == 2
875
+
876
+ key_arg = prism_node.arguments.arguments[0]
877
+ value_type = extract_literal_type(args[1])
878
+
879
+ case original_type
880
+ when Types::HashShape
881
+ if key_arg.is_a?(Prism::SymbolNode)
882
+ # Symbol key → keep HashShape, add field
883
+ key_name = key_arg.value.to_sym
884
+ Types::HashShape.new(original_type.fields.merge(key_name => value_type))
885
+ else
886
+ # Non-symbol key → widen to HashType
887
+ widen_to_hash_type(original_type, key_arg, value_type)
888
+ end
889
+ when Types::HashType
890
+ # Empty hash (Unknown types) + symbol key → becomes HashShape with one field
891
+ if empty_hash_type?(original_type) && key_arg.is_a?(Prism::SymbolNode)
892
+ key_name = key_arg.value.to_sym
893
+ Types::HashShape.new({ key_name => value_type })
894
+ else
895
+ key_type = infer_key_type(key_arg)
896
+ Types::HashType.new(
897
+ union_types(original_type.key_type, key_type),
898
+ union_types(original_type.value_type, value_type)
899
+ )
900
+ end
901
+ end
902
+ end
903
+
904
+ # Check if HashType is empty (has Unknown types)
905
+ def empty_hash_type?(hash_type)
906
+ (hash_type.key_type.nil? || hash_type.key_type.is_a?(Types::Unknown)) &&
907
+ (hash_type.value_type.nil? || hash_type.value_type.is_a?(Types::Unknown))
908
+ end
909
+
910
+ # Compute Array type after indexed assignment
911
+ def compute_array_assignment_type(original_type, args)
912
+ return nil unless args.size == 2
913
+
914
+ value_type = extract_literal_type(args[1])
915
+ combined = union_types(original_type.element_type, value_type)
916
+ Types::ArrayType.new(combined)
917
+ end
918
+
919
+ # Compute Array type after << operator
920
+ def compute_array_append_type(original_type, args)
921
+ return nil unless args.size == 1
922
+
923
+ value_type = extract_literal_type(args[0])
924
+ combined = union_types(original_type.element_type, value_type)
925
+ Types::ArrayType.new(combined)
926
+ end
927
+
928
+ # Extract IR param nodes from a Prism parameter node
929
+ # Handles destructuring (MultiTargetNode) by flattening nested params
930
+ def extract_param_nodes(param, kind, context, default_value: nil)
931
+ case param
932
+ when Prism::MultiTargetNode
933
+ # Destructuring parameter like (a, b) - extract all nested params
934
+ param.lefts.flat_map { |p| extract_param_nodes(p, kind, context) } +
935
+ param.rights.flat_map { |p| extract_param_nodes(p, kind, context) }
936
+ when Prism::RequiredParameterNode, Prism::OptionalParameterNode
937
+ param_node = IR::ParamNode.new(
938
+ name: param.name,
939
+ kind: kind,
940
+ default_value: default_value,
941
+ called_methods: [],
942
+ loc: convert_loc(param.location)
943
+ )
944
+ context.register_variable(param.name, param_node)
945
+ [param_node]
946
+ else
947
+ []
948
+ end
949
+ end
950
+
951
+ def convert_block(block_node, call_node, context)
952
+ # Create block parameter slots and register them in context
953
+ block_context = context.fork(:block)
954
+
955
+ if block_node.parameters.is_a?(Prism::BlockParametersNode)
956
+ parameters_node = block_node.parameters.parameters
957
+ if parameters_node
958
+ # Collect all parameters in order
959
+ params = []
960
+ params.concat(parameters_node.requireds) if parameters_node.requireds
961
+ params.concat(parameters_node.optionals) if parameters_node.optionals
962
+
963
+ params.each_with_index do |param, index|
964
+ param_name, param_loc = case param
965
+ when Prism::RequiredParameterNode
966
+ [param.name, param.location]
967
+ when Prism::OptionalParameterNode
968
+ [param.name, param.location]
969
+ when Prism::MultiTargetNode
970
+ # Destructuring parameters like |a, (b, c)|
971
+ # For now, skip complex cases
972
+ next
973
+ else
974
+ next
975
+ end
976
+
977
+ slot = IR::BlockParamSlot.new(
978
+ index: index,
979
+ call_node: call_node,
980
+ loc: convert_loc(param_loc)
981
+ )
982
+ call_node.block_params << slot
983
+ block_context.register_variable(param_name, slot)
984
+ end
985
+ end
986
+ end
987
+
988
+ # Convert block body and return it for block return type inference
989
+ block_node.body ? convert(block_node.body, block_context) : nil
990
+ end
991
+
992
+ def convert_if(prism_node, context)
993
+ # Convert then branch
994
+ then_context = context.fork(:then)
995
+ then_node = convert(prism_node.statements, then_context) if prism_node.statements
996
+
997
+ # Convert else branch (could be IfNode, ElseNode, or nil)
998
+ else_context = context.fork(:else)
999
+ else_node = if prism_node.subsequent
1000
+ case prism_node.subsequent
1001
+ when Prism::IfNode
1002
+ convert_if(prism_node.subsequent, else_context)
1003
+ when Prism::ElseNode
1004
+ convert(prism_node.subsequent.statements, else_context)
1005
+ end
1006
+ end
1007
+
1008
+ # Create merge nodes for variables modified in branches
1009
+ merge_modified_variables(context, then_context, else_context, then_node, else_node, prism_node.location)
1010
+ end
1011
+
1012
+ def convert_unless(prism_node, context)
1013
+ # Unless is like if with inverted condition
1014
+ # We treat the unless body as the "else" branch and the consequent as "then"
1015
+
1016
+ unless_context = context.fork(:unless)
1017
+ unless_node = convert(prism_node.statements, unless_context) if prism_node.statements
1018
+
1019
+ else_context = context.fork(:else)
1020
+ else_node = (convert(prism_node.else_clause.statements, else_context) if prism_node.else_clause)
1021
+
1022
+ merge_modified_variables(context, unless_context, else_context, unless_node, else_node, prism_node.location)
1023
+ end
1024
+
1025
+ def convert_case(prism_node, context)
1026
+ branches = []
1027
+ branch_contexts = []
1028
+
1029
+ # Convert each when clause
1030
+ prism_node.conditions&.each do |when_node|
1031
+ when_context = context.fork(:when)
1032
+ if when_node.statements
1033
+ when_result = convert(when_node.statements, when_context)
1034
+ # Skip non-returning branches (raise, fail, etc.)
1035
+ unless non_returning?(when_result)
1036
+ branches << (when_result || create_nil_literal(prism_node.location))
1037
+ branch_contexts << when_context
1038
+ end
1039
+ else
1040
+ # Empty when clause → nil
1041
+ branches << create_nil_literal(prism_node.location)
1042
+ branch_contexts << when_context
1043
+ end
1044
+ end
1045
+
1046
+ # Convert else clause
1047
+ if prism_node.else_clause
1048
+ else_context = context.fork(:else)
1049
+ else_result = convert(prism_node.else_clause.statements, else_context)
1050
+ # Skip non-returning else clause (raise, fail, etc.)
1051
+ unless non_returning?(else_result)
1052
+ branches << (else_result || create_nil_literal(prism_node.location))
1053
+ branch_contexts << else_context
1054
+ end
1055
+ else
1056
+ # If no else clause, nil is possible
1057
+ branches << create_nil_literal(prism_node.location)
1058
+ end
1059
+
1060
+ # Merge modified variables across all branches
1061
+ merge_case_variables(context, branch_contexts, branches, prism_node.location)
1062
+ end
1063
+
1064
+ def convert_case_match(prism_node, context)
1065
+ # Pattern matching case (Ruby 3.0+)
1066
+ # For now, treat it similarly to regular case
1067
+ convert_case(prism_node, context)
1068
+ end
1069
+
1070
+ def convert_statements(prism_node, context)
1071
+ last_node = nil
1072
+ prism_node.body.each do |stmt|
1073
+ last_node = convert(stmt, context)
1074
+ end
1075
+ last_node
1076
+ end
1077
+
1078
+ # Helper to convert an array of statement bodies
1079
+ # @param body [Array<Prism::Node>, nil] Array of statement nodes
1080
+ # @param context [Context] Conversion context
1081
+ # @return [Array<IR::Node>] Array of converted IR nodes
1082
+ def convert_statements_body(body, context)
1083
+ return [] unless body
1084
+
1085
+ nodes = []
1086
+ body.each do |stmt|
1087
+ node = convert(stmt, context)
1088
+ nodes << node if node
1089
+ end
1090
+ nodes
1091
+ end
1092
+
1093
+ # Convert begin/rescue/ensure block
1094
+ def convert_begin(prism_node, context)
1095
+ body_nodes = extract_begin_body_nodes(prism_node, context)
1096
+ # Return the last node (represents the value of the begin block)
1097
+ body_nodes.last
1098
+ end
1099
+
1100
+ # Extract all body nodes from a BeginNode (for DefNode bodies with rescue/ensure)
1101
+ # @param begin_node [Prism::BeginNode] The begin node
1102
+ # @param context [Context] Conversion context
1103
+ # @return [Array<IR::Node>] Array of all body nodes
1104
+ def extract_begin_body_nodes(begin_node, context)
1105
+ body_nodes = []
1106
+
1107
+ # Convert main body statements
1108
+ body_nodes.concat(convert_statements_body(begin_node.statements.body, context)) if begin_node.statements
1109
+
1110
+ # Convert rescue clause(s)
1111
+ rescue_clause = begin_node.rescue_clause
1112
+ while rescue_clause
1113
+ rescue_nodes = convert_statements_body(rescue_clause.statements&.body, context)
1114
+ body_nodes.concat(rescue_nodes)
1115
+ rescue_clause = rescue_clause.subsequent
1116
+ end
1117
+
1118
+ # Convert else clause
1119
+ if begin_node.else_clause
1120
+ else_nodes = convert_statements_body(begin_node.else_clause.statements&.body, context)
1121
+ body_nodes.concat(else_nodes)
1122
+ end
1123
+
1124
+ # Convert ensure clause
1125
+ if begin_node.ensure_clause
1126
+ ensure_nodes = convert_statements_body(begin_node.ensure_clause.statements&.body, context)
1127
+ body_nodes.concat(ensure_nodes)
1128
+ end
1129
+
1130
+ body_nodes
1131
+ end
1132
+
1133
+ def convert_def(prism_node, context)
1134
+ def_context = context.fork(:method)
1135
+ def_context.current_method = prism_node.name.to_s
1136
+ def_context.in_singleton_method = prism_node.receiver.is_a?(Prism::SelfNode)
1137
+
1138
+ # Convert parameters
1139
+ params = []
1140
+ if prism_node.parameters
1141
+ parameters_node = prism_node.parameters
1142
+
1143
+ # Required parameters
1144
+ parameters_node.requireds&.each do |param|
1145
+ extract_param_nodes(param, :required, def_context).each do |param_node|
1146
+ params << param_node
1147
+ end
1148
+ end
1149
+
1150
+ # Optional parameters
1151
+ parameters_node.optionals&.each do |param|
1152
+ default_node = convert(param.value, def_context)
1153
+ param_node = IR::ParamNode.new(
1154
+ name: param.name,
1155
+ kind: :optional,
1156
+ default_value: default_node,
1157
+ called_methods: [],
1158
+ loc: convert_loc(param.location)
1159
+ )
1160
+ params << param_node
1161
+ def_context.register_variable(param.name, param_node)
1162
+ end
1163
+
1164
+ # Rest parameter (*args)
1165
+ if parameters_node.rest.is_a?(Prism::RestParameterNode)
1166
+ rest = parameters_node.rest
1167
+ param_node = IR::ParamNode.new(
1168
+ name: rest.name || :*,
1169
+ kind: :rest,
1170
+ default_value: nil,
1171
+ called_methods: [],
1172
+ loc: convert_loc(rest.location)
1173
+ )
1174
+ params << param_node
1175
+ def_context.register_variable(rest.name, param_node) if rest.name
1176
+ end
1177
+
1178
+ # Required keyword parameters (name:)
1179
+ parameters_node.keywords&.each do |kw|
1180
+ case kw
1181
+ when Prism::RequiredKeywordParameterNode
1182
+ param_node = IR::ParamNode.new(
1183
+ name: kw.name,
1184
+ kind: :keyword_required,
1185
+ default_value: nil,
1186
+ called_methods: [],
1187
+ loc: convert_loc(kw.location)
1188
+ )
1189
+ params << param_node
1190
+ def_context.register_variable(kw.name, param_node)
1191
+ when Prism::OptionalKeywordParameterNode
1192
+ default_node = convert(kw.value, def_context)
1193
+ param_node = IR::ParamNode.new(
1194
+ name: kw.name,
1195
+ kind: :keyword_optional,
1196
+ default_value: default_node,
1197
+ called_methods: [],
1198
+ loc: convert_loc(kw.location)
1199
+ )
1200
+ params << param_node
1201
+ def_context.register_variable(kw.name, param_node)
1202
+ end
1203
+ end
1204
+
1205
+ # Keyword rest parameter (**kwargs)
1206
+ if parameters_node.keyword_rest.is_a?(Prism::KeywordRestParameterNode)
1207
+ kwrest = parameters_node.keyword_rest
1208
+ param_node = IR::ParamNode.new(
1209
+ name: kwrest.name || :**,
1210
+ kind: :keyword_rest,
1211
+ default_value: nil,
1212
+ called_methods: [],
1213
+ loc: convert_loc(kwrest.location)
1214
+ )
1215
+ params << param_node
1216
+ def_context.register_variable(kwrest.name, param_node) if kwrest.name
1217
+ elsif parameters_node.keyword_rest.is_a?(Prism::ForwardingParameterNode)
1218
+ # Forwarding parameter (...)
1219
+ fwd = parameters_node.keyword_rest
1220
+ param_node = IR::ParamNode.new(
1221
+ name: :"...",
1222
+ kind: :forwarding,
1223
+ default_value: nil,
1224
+ called_methods: [],
1225
+ loc: convert_loc(fwd.location)
1226
+ )
1227
+ params << param_node
1228
+ end
1229
+
1230
+ # Block parameter (&block)
1231
+ if parameters_node.block
1232
+ block = parameters_node.block
1233
+ param_node = IR::ParamNode.new(
1234
+ name: block.name || :&,
1235
+ kind: :block,
1236
+ default_value: nil,
1237
+ called_methods: [],
1238
+ loc: convert_loc(block.location)
1239
+ )
1240
+ params << param_node
1241
+ def_context.register_variable(block.name, param_node) if block.name
1242
+ end
1243
+ end
1244
+
1245
+ # Convert method body - collect all body nodes
1246
+ body_nodes = []
1247
+
1248
+ if prism_node.body.is_a?(Prism::StatementsNode)
1249
+ prism_node.body.body.each do |stmt|
1250
+ node = convert(stmt, def_context)
1251
+ body_nodes << node if node
1252
+ end
1253
+ elsif prism_node.body.is_a?(Prism::BeginNode)
1254
+ # Method with rescue/ensure block
1255
+ begin_node = prism_node.body
1256
+ body_nodes = extract_begin_body_nodes(begin_node, def_context)
1257
+ elsif prism_node.body
1258
+ node = convert(prism_node.body, def_context)
1259
+ body_nodes << node if node
1260
+ end
1261
+
1262
+ # Collect all return points: explicit returns + implicit last expression
1263
+ return_node = compute_return_node(body_nodes, prism_node.name_loc)
1264
+
1265
+ IR::DefNode.new(
1266
+ name: prism_node.name,
1267
+ class_name: def_context.current_class_name,
1268
+ params: params,
1269
+ return_node: return_node,
1270
+ body_nodes: body_nodes,
1271
+ loc: convert_loc(prism_node.name_loc),
1272
+ singleton: prism_node.receiver.is_a?(Prism::SelfNode)
1273
+ )
1274
+ end
1275
+
1276
+ # Compute the return node for a method by collecting all return points
1277
+ # @param body_nodes [Array<IR::Node>] All nodes in the method body
1278
+ # @param loc [Prism::Location] Location for the MergeNode if needed
1279
+ # @return [IR::Node, nil] The return node (MergeNode if multiple returns)
1280
+ def compute_return_node(body_nodes, loc)
1281
+ return nil if body_nodes.empty?
1282
+
1283
+ # Collect all explicit returns from the body
1284
+ explicit_returns = collect_returns(body_nodes)
1285
+
1286
+ # The implicit return is the last non-ReturnNode in body
1287
+ implicit_return = body_nodes.reject { |n| n.is_a?(IR::ReturnNode) }.last
1288
+
1289
+ # Determine all return points
1290
+ return_points = explicit_returns.dup
1291
+ return_points << implicit_return if implicit_return && !last_node_returns?(body_nodes)
1292
+
1293
+ case return_points.size
1294
+ when 0
1295
+ nil
1296
+ when 1
1297
+ return_points.first
1298
+ else
1299
+ IR::MergeNode.new(
1300
+ branches: return_points,
1301
+ loc: convert_loc(loc)
1302
+ )
1303
+ end
1304
+ end
1305
+
1306
+ # Collect all ReturnNode instances from body nodes (recursive)
1307
+ # Searches inside MergeNode branches to find nested returns from if/case
1308
+ # @param nodes [Array<IR::Node>] Nodes to search
1309
+ # @return [Array<IR::ReturnNode>] All explicit return nodes
1310
+ def collect_returns(nodes)
1311
+ returns = []
1312
+ nodes.each do |node|
1313
+ case node
1314
+ when IR::ReturnNode
1315
+ returns << node
1316
+ when IR::MergeNode
1317
+ returns.concat(collect_returns(node.branches))
1318
+ end
1319
+ end
1320
+ returns
1321
+ end
1322
+
1323
+ # Check if the last node in body is a ReturnNode
1324
+ # @param body_nodes [Array<IR::Node>] Body nodes
1325
+ # @return [Boolean]
1326
+ def last_node_returns?(body_nodes)
1327
+ body_nodes.last.is_a?(IR::ReturnNode)
1328
+ end
1329
+
1330
+ def convert_constant_read(prism_node, context)
1331
+ name = case prism_node
1332
+ when Prism::ConstantReadNode
1333
+ prism_node.name.to_s
1334
+ when Prism::ConstantPathNode
1335
+ prism_node.slice
1336
+ else
1337
+ prism_node.to_s
1338
+ end
1339
+
1340
+ IR::ConstantNode.new(
1341
+ name: name,
1342
+ dependency: context.lookup_constant(name),
1343
+ loc: convert_loc(prism_node.location)
1344
+ )
1345
+ end
1346
+
1347
+ def convert_constant_write(prism_node, context)
1348
+ value_node = convert(prism_node.value, context)
1349
+ context.register_constant(prism_node.name.to_s, value_node)
1350
+ IR::ConstantNode.new(
1351
+ name: prism_node.name.to_s,
1352
+ dependency: value_node,
1353
+ loc: convert_loc(prism_node.location)
1354
+ )
1355
+ end
1356
+
1357
+ def merge_modified_variables(parent_context, then_context, else_context, then_node, else_node, location)
1358
+ # Skip non-returning branches (raise, fail, etc.)
1359
+ then_node = nil if non_returning?(then_node)
1360
+ else_node = nil if non_returning?(else_node)
1361
+
1362
+ # Track which variables were modified in each branch
1363
+ then_vars = then_context&.local_variables || []
1364
+ else_vars = else_context&.local_variables || []
1365
+
1366
+ # All variables modified in either branch
1367
+ modified_vars = (then_vars + else_vars).uniq
1368
+
1369
+ # Create MergeNode for each modified variable
1370
+ modified_vars.each do |var_name|
1371
+ then_val = then_context&.variables&.[](var_name)
1372
+ else_val = else_context&.variables&.[](var_name)
1373
+
1374
+ # Get the original value from parent context (before if statement)
1375
+ original_val = parent_context.lookup_variable(var_name)
1376
+
1377
+ # Determine branches for merge
1378
+ branches = []
1379
+ if then_val
1380
+ branches << then_val
1381
+ elsif original_val
1382
+ # Variable not modified in then branch, use original
1383
+ branches << original_val
1384
+ end
1385
+
1386
+ if else_val
1387
+ branches << else_val
1388
+ elsif original_val
1389
+ # Variable not modified in else branch, use original
1390
+ branches << original_val
1391
+ elsif then_val
1392
+ # Inline if/unless: no else branch and no original value
1393
+ # Add nil to represent "variable may not be assigned"
1394
+ nil_node = IR::LiteralNode.new(
1395
+ type: Types::ClassInstance.new("NilClass"),
1396
+ literal_value: nil,
1397
+ values: nil,
1398
+ loc: convert_loc(location)
1399
+ )
1400
+ branches << nil_node
1401
+ end
1402
+
1403
+ # Create MergeNode only if we have multiple branches
1404
+ if branches.size > 1
1405
+ merge_node = IR::MergeNode.new(
1406
+ branches: branches.uniq,
1407
+ loc: convert_loc(location)
1408
+ )
1409
+ parent_context.register_variable(var_name, merge_node)
1410
+ elsif branches.size == 1
1411
+ # Only one branch has a value, use it directly
1412
+ parent_context.register_variable(var_name, branches.first)
1413
+ end
1414
+ end
1415
+
1416
+ # Return MergeNode for the if expression value
1417
+ if then_node && else_node
1418
+ IR::MergeNode.new(
1419
+ branches: [then_node, else_node].compact,
1420
+ loc: convert_loc(location)
1421
+ )
1422
+ elsif then_node || else_node
1423
+ # Modifier form: one branch only → value or nil
1424
+ branch_node = then_node || else_node
1425
+ nil_node = IR::LiteralNode.new(
1426
+ type: Types::ClassInstance.new("NilClass"),
1427
+ literal_value: nil,
1428
+ values: nil,
1429
+ loc: convert_loc(location)
1430
+ )
1431
+ IR::MergeNode.new(
1432
+ branches: [branch_node, nil_node],
1433
+ loc: convert_loc(location)
1434
+ )
1435
+ end
1436
+ end
1437
+
1438
+ def merge_case_variables(parent_context, branch_contexts, branches, location)
1439
+ # Collect all variables modified in any branch
1440
+ all_modified_vars = branch_contexts.flat_map { |ctx| ctx&.local_variables || [] }.uniq
1441
+
1442
+ # Create MergeNode for each modified variable
1443
+ all_modified_vars.each do |var_name|
1444
+ # Collect values from all branches
1445
+ branch_contexts.map { |ctx| ctx&.variables&.[](var_name) }
1446
+
1447
+ # Get original value from parent context
1448
+ original_val = parent_context.lookup_variable(var_name)
1449
+
1450
+ # Build branches array
1451
+ merge_branches = branch_contexts.map.with_index do |ctx, _idx|
1452
+ ctx&.variables&.[](var_name) || original_val
1453
+ end.compact.uniq
1454
+
1455
+ # Create MergeNode if we have multiple different values
1456
+ if merge_branches.size > 1
1457
+ merge_node = IR::MergeNode.new(
1458
+ branches: merge_branches,
1459
+ loc: convert_loc(location)
1460
+ )
1461
+ parent_context.register_variable(var_name, merge_node)
1462
+ elsif merge_branches.size == 1
1463
+ parent_context.register_variable(var_name, merge_branches.first)
1464
+ end
1465
+ end
1466
+
1467
+ # Return MergeNode for the case expression value
1468
+ if branches.size > 1
1469
+ IR::MergeNode.new(
1470
+ branches: branches.compact.uniq,
1471
+ loc: convert_loc(location)
1472
+ )
1473
+ elsif branches.size == 1
1474
+ branches.first
1475
+ end
1476
+ end
1477
+
1478
+ def create_nil_literal(location)
1479
+ IR::LiteralNode.new(
1480
+ type: Types::ClassInstance.new("NilClass"),
1481
+ literal_value: nil,
1482
+ values: nil,
1483
+ loc: convert_loc(location)
1484
+ )
1485
+ end
1486
+
1487
+ # Check if a node represents a non-returning expression (raise, fail, exit, abort)
1488
+ # These should be excluded from branch type inference
1489
+ def non_returning?(node)
1490
+ return false unless node.is_a?(IR::CallNode)
1491
+
1492
+ node.receiver.nil? && %i[raise fail exit abort].include?(node.method)
1493
+ end
1494
+
1495
+ def convert_class_or_module(prism_node, context)
1496
+ # Get class/module name first
1497
+ name = case prism_node.constant_path
1498
+ when Prism::ConstantReadNode
1499
+ prism_node.constant_path.name.to_s
1500
+ when Prism::ConstantPathNode
1501
+ prism_node.constant_path.slice
1502
+ else
1503
+ "Anonymous"
1504
+ end
1505
+
1506
+ # Create a new context for class/module scope with the full class path
1507
+ class_context = context.fork(:class)
1508
+ parent_path = context.current_class_name
1509
+ full_name = parent_path ? "#{parent_path}::#{name}" : name
1510
+ class_context.current_class = full_name
1511
+
1512
+ # Collect all method definitions and nested classes from the body
1513
+ methods = []
1514
+ nested_classes = []
1515
+ if prism_node.body.is_a?(Prism::StatementsNode)
1516
+ prism_node.body.body.each do |stmt|
1517
+ node = convert(stmt, class_context)
1518
+ if node.is_a?(IR::DefNode)
1519
+ methods << node
1520
+ elsif node.is_a?(IR::ClassModuleNode)
1521
+ # Store nested class/module for separate indexing with proper scope
1522
+ nested_classes << node
1523
+ end
1524
+ end
1525
+ end
1526
+ # Store nested classes in methods array (RuntimeAdapter handles both types)
1527
+ methods.concat(nested_classes)
1528
+
1529
+ IR::ClassModuleNode.new(
1530
+ name: name,
1531
+ methods: methods,
1532
+ loc: convert_loc(prism_node.constant_path&.location || prism_node.location)
1533
+ )
1534
+ end
1535
+
1536
+ def convert_singleton_class(prism_node, context)
1537
+ # Create a new context for singleton class scope
1538
+ singleton_context = context.fork(:class)
1539
+
1540
+ # Generate singleton class name in format: <Class:ParentName>
1541
+ parent_name = context.current_class_name || "Object"
1542
+ singleton_name = "<Class:#{parent_name}>"
1543
+ singleton_context.current_class = singleton_name
1544
+
1545
+ # Collect all method definitions from the body
1546
+ methods = []
1547
+ if prism_node.body.is_a?(Prism::StatementsNode)
1548
+ prism_node.body.body.each do |stmt|
1549
+ node = convert(stmt, singleton_context)
1550
+ methods << node if node.is_a?(IR::DefNode)
1551
+ end
1552
+ end
1553
+
1554
+ IR::ClassModuleNode.new(
1555
+ name: singleton_name,
1556
+ methods: methods,
1557
+ loc: convert_loc(prism_node.location)
1558
+ )
1559
+ end
1560
+
1561
+ def infer_array_element_type(array_node)
1562
+ return Types::ArrayType.new if array_node.elements.empty?
1563
+
1564
+ element_types = array_node.elements.filter_map do |elem|
1565
+ infer_literal_type(elem) unless elem.nil?
1566
+ end
1567
+
1568
+ return Types::ArrayType.new if element_types.empty?
1569
+
1570
+ # Deduplicate types
1571
+ unique_types = element_types.uniq
1572
+
1573
+ element_type = if unique_types.size == 1
1574
+ unique_types.first
1575
+ else
1576
+ Types::Union.new(unique_types)
1577
+ end
1578
+
1579
+ Types::ArrayType.new(element_type)
1580
+ end
1581
+
1582
+ def infer_hash_element_types(hash_node)
1583
+ return Types::HashType.new if hash_node.elements.empty?
1584
+
1585
+ # Check if all keys are symbols for HashShape
1586
+ all_symbol_keys = hash_node.elements.all? do |elem|
1587
+ elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
1588
+ end
1589
+
1590
+ if all_symbol_keys
1591
+ # Build HashShape with field types
1592
+ fields = {}
1593
+ hash_node.elements.each do |elem|
1594
+ next unless elem.is_a?(Prism::AssocNode) && elem.key.is_a?(Prism::SymbolNode)
1595
+
1596
+ field_name = elem.key.value.to_sym
1597
+ field_type = infer_literal_type(elem.value)
1598
+ fields[field_name] = field_type
1599
+ end
1600
+ Types::HashShape.new(fields)
1601
+ else
1602
+ # Non-symbol keys or mixed keys - return HashType
1603
+ key_types = []
1604
+ value_types = []
1605
+
1606
+ hash_node.elements.each do |elem|
1607
+ case elem
1608
+ when Prism::AssocNode
1609
+ key_types << infer_literal_type(elem.key) if elem.key
1610
+ value_types << infer_literal_type(elem.value) if elem.value
1611
+ end
1612
+ end
1613
+
1614
+ return Types::HashType.new if key_types.empty? && value_types.empty?
1615
+
1616
+ # Deduplicate types
1617
+ unique_key_types = key_types.uniq
1618
+ unique_value_types = value_types.uniq
1619
+
1620
+ key_type = if unique_key_types.size == 1
1621
+ unique_key_types.first
1622
+ elsif unique_key_types.empty?
1623
+ Types::Unknown.instance
1624
+ else
1625
+ Types::Union.new(unique_key_types)
1626
+ end
1627
+
1628
+ value_type = if unique_value_types.size == 1
1629
+ unique_value_types.first
1630
+ elsif unique_value_types.empty?
1631
+ Types::Unknown.instance
1632
+ else
1633
+ Types::Union.new(unique_value_types)
1634
+ end
1635
+
1636
+ Types::HashType.new(key_type, value_type)
1637
+ end
1638
+ end
1639
+
1640
+ def convert_loc(prism_location)
1641
+ IR::Loc.new(
1642
+ line: prism_location.start_line,
1643
+ col_range: (prism_location.start_column...prism_location.end_column)
1644
+ )
1645
+ end
1646
+ end
1647
+ end
1648
+ end
1649
+ end