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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +89 -0
- data/lib/ruby_lsp/type_guessr/addon.rb +138 -0
- data/lib/ruby_lsp/type_guessr/config.rb +90 -0
- data/lib/ruby_lsp/type_guessr/debug_server.rb +861 -0
- data/lib/ruby_lsp/type_guessr/graph_builder.rb +349 -0
- data/lib/ruby_lsp/type_guessr/hover.rb +565 -0
- data/lib/ruby_lsp/type_guessr/runtime_adapter.rb +506 -0
- data/lib/ruby_lsp/type_guessr/type_inferrer.rb +200 -0
- data/lib/type-guessr.rb +28 -0
- data/lib/type_guessr/core/converter/prism_converter.rb +1649 -0
- data/lib/type_guessr/core/converter/rbs_converter.rb +88 -0
- data/lib/type_guessr/core/index/location_index.rb +72 -0
- data/lib/type_guessr/core/inference/resolver.rb +664 -0
- data/lib/type_guessr/core/inference/result.rb +41 -0
- data/lib/type_guessr/core/ir/nodes.rb +599 -0
- data/lib/type_guessr/core/logger.rb +43 -0
- data/lib/type_guessr/core/rbs_provider.rb +304 -0
- data/lib/type_guessr/core/registry/method_registry.rb +106 -0
- data/lib/type_guessr/core/registry/variable_registry.rb +87 -0
- data/lib/type_guessr/core/signature_provider.rb +101 -0
- data/lib/type_guessr/core/type_simplifier.rb +64 -0
- data/lib/type_guessr/core/types.rb +425 -0
- data/lib/type_guessr/version.rb +5 -0
- metadata +81 -0
|
@@ -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
|