konpeito 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (180) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +75 -0
  4. data/CONTRIBUTING.md +123 -0
  5. data/LICENSE +21 -0
  6. data/README.md +257 -0
  7. data/Rakefile +11 -0
  8. data/bin/konpeito +6 -0
  9. data/konpeito.gemspec +43 -0
  10. data/lib/konpeito/ast/typed_ast.rb +620 -0
  11. data/lib/konpeito/ast/visitor.rb +78 -0
  12. data/lib/konpeito/cache/cache_manager.rb +230 -0
  13. data/lib/konpeito/cache/dependency_graph.rb +192 -0
  14. data/lib/konpeito/cache.rb +8 -0
  15. data/lib/konpeito/cli/base_command.rb +187 -0
  16. data/lib/konpeito/cli/build_command.rb +220 -0
  17. data/lib/konpeito/cli/check_command.rb +104 -0
  18. data/lib/konpeito/cli/config.rb +231 -0
  19. data/lib/konpeito/cli/deps_command.rb +128 -0
  20. data/lib/konpeito/cli/doctor_command.rb +340 -0
  21. data/lib/konpeito/cli/fmt_command.rb +199 -0
  22. data/lib/konpeito/cli/init_command.rb +312 -0
  23. data/lib/konpeito/cli/lsp_command.rb +40 -0
  24. data/lib/konpeito/cli/run_command.rb +150 -0
  25. data/lib/konpeito/cli/test_command.rb +248 -0
  26. data/lib/konpeito/cli/watch_command.rb +212 -0
  27. data/lib/konpeito/cli.rb +301 -0
  28. data/lib/konpeito/codegen/builtin_methods.rb +229 -0
  29. data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
  30. data/lib/konpeito/codegen/debug_info.rb +352 -0
  31. data/lib/konpeito/codegen/inliner.rb +486 -0
  32. data/lib/konpeito/codegen/jvm_backend.rb +197 -0
  33. data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
  34. data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
  35. data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
  36. data/lib/konpeito/codegen/monomorphizer.rb +359 -0
  37. data/lib/konpeito/codegen/profile_runtime.c +341 -0
  38. data/lib/konpeito/codegen/profiler.rb +99 -0
  39. data/lib/konpeito/compiler.rb +592 -0
  40. data/lib/konpeito/dependency_resolver.rb +296 -0
  41. data/lib/konpeito/diagnostics/collector.rb +127 -0
  42. data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
  43. data/lib/konpeito/diagnostics/renderer.rb +144 -0
  44. data/lib/konpeito/formatter/formatter.rb +1214 -0
  45. data/lib/konpeito/hir/builder.rb +7167 -0
  46. data/lib/konpeito/hir/nodes.rb +2465 -0
  47. data/lib/konpeito/lsp/document_manager.rb +820 -0
  48. data/lib/konpeito/lsp/server.rb +183 -0
  49. data/lib/konpeito/lsp/transport.rb +38 -0
  50. data/lib/konpeito/parser/prism_adapter.rb +65 -0
  51. data/lib/konpeito/platform.rb +103 -0
  52. data/lib/konpeito/profile/report.rb +136 -0
  53. data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
  54. data/lib/konpeito/stdlib/compression/compression.rb +72 -0
  55. data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
  56. data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
  57. data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
  58. data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
  59. data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
  60. data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
  61. data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
  62. data/lib/konpeito/stdlib/http/extconf.rb +19 -0
  63. data/lib/konpeito/stdlib/http/http.rb +125 -0
  64. data/lib/konpeito/stdlib/http/http.rbs +57 -0
  65. data/lib/konpeito/stdlib/http/http_native.c +440 -0
  66. data/lib/konpeito/stdlib/json/extconf.rb +17 -0
  67. data/lib/konpeito/stdlib/json/json.rb +44 -0
  68. data/lib/konpeito/stdlib/json/json.rbs +33 -0
  69. data/lib/konpeito/stdlib/json/json_native.c +286 -0
  70. data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
  71. data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
  72. data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
  73. data/lib/konpeito/stdlib/ui/ui.rb +318 -0
  74. data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
  75. data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
  76. data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
  77. data/lib/konpeito/type_checker/inferrer.rb +565 -0
  78. data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
  79. data/lib/konpeito/type_checker/type_resolver.rb +276 -0
  80. data/lib/konpeito/type_checker/types.rb +1434 -0
  81. data/lib/konpeito/type_checker/unification.rb +323 -0
  82. data/lib/konpeito/ui/animation/animated_state.rb +80 -0
  83. data/lib/konpeito/ui/animation/easing.rb +59 -0
  84. data/lib/konpeito/ui/animation/value_tween.rb +66 -0
  85. data/lib/konpeito/ui/app.rb +379 -0
  86. data/lib/konpeito/ui/box.rb +38 -0
  87. data/lib/konpeito/ui/castella.rb +70 -0
  88. data/lib/konpeito/ui/castella_native.rb +76 -0
  89. data/lib/konpeito/ui/chart/area_chart.rb +305 -0
  90. data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
  91. data/lib/konpeito/ui/chart/base_chart.rb +210 -0
  92. data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
  93. data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
  94. data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
  95. data/lib/konpeito/ui/chart/line_chart.rb +289 -0
  96. data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
  97. data/lib/konpeito/ui/chart/scales.rb +77 -0
  98. data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
  99. data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
  100. data/lib/konpeito/ui/column.rb +271 -0
  101. data/lib/konpeito/ui/core.rb +2199 -0
  102. data/lib/konpeito/ui/dsl.rb +443 -0
  103. data/lib/konpeito/ui/frame.rb +171 -0
  104. data/lib/konpeito/ui/frame_native.rb +494 -0
  105. data/lib/konpeito/ui/markdown/ast.rb +124 -0
  106. data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
  107. data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
  108. data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
  109. data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
  110. data/lib/konpeito/ui/markdown/parser.rb +805 -0
  111. data/lib/konpeito/ui/markdown/renderer.rb +639 -0
  112. data/lib/konpeito/ui/markdown/theme.rb +165 -0
  113. data/lib/konpeito/ui/render_node.rb +260 -0
  114. data/lib/konpeito/ui/row.rb +207 -0
  115. data/lib/konpeito/ui/spacer.rb +18 -0
  116. data/lib/konpeito/ui/style.rb +799 -0
  117. data/lib/konpeito/ui/theme.rb +563 -0
  118. data/lib/konpeito/ui/themes/material.rb +35 -0
  119. data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
  120. data/lib/konpeito/ui/widgets/button.rb +103 -0
  121. data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
  122. data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
  123. data/lib/konpeito/ui/widgets/container.rb +91 -0
  124. data/lib/konpeito/ui/widgets/data_table.rb +667 -0
  125. data/lib/konpeito/ui/widgets/divider.rb +29 -0
  126. data/lib/konpeito/ui/widgets/image.rb +105 -0
  127. data/lib/konpeito/ui/widgets/input.rb +485 -0
  128. data/lib/konpeito/ui/widgets/markdown.rb +57 -0
  129. data/lib/konpeito/ui/widgets/modal.rb +163 -0
  130. data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
  131. data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
  132. data/lib/konpeito/ui/widgets/net_image.rb +100 -0
  133. data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
  134. data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
  135. data/lib/konpeito/ui/widgets/slider.rb +133 -0
  136. data/lib/konpeito/ui/widgets/switch.rb +84 -0
  137. data/lib/konpeito/ui/widgets/tabs.rb +157 -0
  138. data/lib/konpeito/ui/widgets/text.rb +110 -0
  139. data/lib/konpeito/ui/widgets/tree.rb +426 -0
  140. data/lib/konpeito/version.rb +5 -0
  141. data/lib/konpeito.rb +109 -0
  142. data/test_native_array.rb +172 -0
  143. data/test_native_array_class.rb +197 -0
  144. data/test_native_class.rb +151 -0
  145. data/tools/konpeito-asm/build.sh +65 -0
  146. data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
  147. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
  148. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
  149. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
  150. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
  151. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
  152. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
  153. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
  154. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
  155. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
  156. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
  157. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
  158. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
  159. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
  160. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
  161. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
  162. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
  163. data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
  164. data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
  165. data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
  166. data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
  167. data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
  168. data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
  169. data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
  170. data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
  171. data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
  172. data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
  173. data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
  174. data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
  175. data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
  176. data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
  177. data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
  178. data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
  179. data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
  180. metadata +267 -0
@@ -0,0 +1,2565 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "types"
4
+ require_relative "unification"
5
+
6
+ module Konpeito
7
+ module TypeChecker
8
+ # Hindley-Milner style type inference
9
+ # Uses Algorithm W with constraint generation and unification
10
+ class HMInferrer
11
+ attr_reader :errors, :unifier, :node_types, :diagnostics, :ivar_types, :inference_errors,
12
+ :unresolved_type_warnings
13
+
14
+ def initialize(rbs_loader = nil, file_path: nil, source: nil)
15
+ @rbs_loader = rbs_loader
16
+ @file_path = file_path
17
+ @source = source
18
+ @errors = []
19
+ @diagnostics = [] # Diagnostic objects for rich error messages
20
+ @unifier = Unifier.new
21
+ @env = [{}] # Stack of name -> TypeScheme
22
+ @function_types = {} # Function name -> FunctionType
23
+ @class_init_types = {} # ClassName -> FunctionType for initialize
24
+ @node_types = {} # Node location -> Type (for AST building)
25
+ @current_class_name = nil # Current class context for ivar tracking
26
+ @current_method_name = nil # Current method name for super resolution
27
+ @class_parents = {} # ClassName -> ParentClassName (for super type propagation)
28
+ @ivar_types = {} # { "ClassName" => { "@name" => Type } }
29
+ @in_class_collect = false # Track if inside class during signature collection
30
+ @collect_class_name = nil # Current class name during signature collection
31
+ @inference_errors = [] # Collected type inference errors
32
+ @global_var_types = {} # Global variable type tracking
33
+ @cvar_types = {} # Class variable tracking: { "ClassName" => { "@@var" => Type } }
34
+ @deferred_constraints = [] # Deferred method resolution for TypeVar receivers
35
+ @keyword_param_vars = {} # func_key => { :param_name => TypeVar }
36
+ @unresolved_type_warnings = [] # Warnings for types that survived inference
37
+ end
38
+
39
+ # Main entry: infer types for a program AST
40
+ def analyze(node)
41
+ # First pass: collect function signatures
42
+ collect_function_signatures(node)
43
+
44
+ # Second pass: infer and unify
45
+ infer(node)
46
+
47
+ # Third pass: resolve deferred constraints (TypeVar receivers now resolved by call-sites)
48
+ resolve_deferred_constraints
49
+
50
+ # Fourth pass: resolve ivar types using unification results
51
+ resolve_ivar_types!
52
+
53
+ # Fifth pass: validate all types are resolved (Kotlin-style: no TypeVar survives to codegen)
54
+ validate_all_types_resolved!
55
+ end
56
+
57
+ # Infer type of an expression
58
+ # Infer type of a node
59
+ # @param node [Prism::Node] the AST node
60
+ # @param statement_position [Boolean] if true, the result of this node is not used
61
+ # (e.g., non-last statements in a block). In statement position, if/unless
62
+ # branches don't need to have consistent types.
63
+ def infer(node, statement_position: false)
64
+ return Types::UNTYPED unless node
65
+
66
+ method_name = :"infer_#{node_type(node)}"
67
+ type = if respond_to?(method_name, true)
68
+ # Pass statement_position to if/unless handlers
69
+ if [:infer_if, :infer_unless].include?(method_name)
70
+ send(method_name, node, statement_position: statement_position)
71
+ else
72
+ send(method_name, node)
73
+ end
74
+ else
75
+ Types::UNTYPED
76
+ end
77
+
78
+ # Record type for this node (by location key)
79
+ record_node_type(node, type)
80
+ type
81
+ rescue UnificationError => e
82
+ @errors << e
83
+ record_diagnostic_from_unification_error(e, node)
84
+ record_node_type(node, Types::UNTYPED)
85
+ Types::UNTYPED
86
+ end
87
+
88
+ # Record a diagnostic from a UnificationError
89
+ def record_diagnostic_from_unification_error(error, node)
90
+ return unless @file_path
91
+
92
+ actual_node = error.node || node
93
+ span = create_span_from_node(actual_node)
94
+ return unless span
95
+
96
+ diagnostic = Diagnostics::Diagnostic.type_mismatch(
97
+ expected: format_type(error.type1),
98
+ found: format_type(error.type2),
99
+ span: span
100
+ )
101
+ @diagnostics << diagnostic
102
+ end
103
+
104
+ # Create a SourceSpan from a Prism node
105
+ def create_span_from_node(node)
106
+ return nil unless node&.respond_to?(:location) && node.location
107
+
108
+ Diagnostics::SourceSpan.from_prism_location(
109
+ node.location,
110
+ file_path: @file_path,
111
+ source: @source
112
+ )
113
+ end
114
+
115
+ # Format a type for display
116
+ def format_type(type)
117
+ case type
118
+ when TypeVar
119
+ if type.instance
120
+ format_type(type.instance)
121
+ else
122
+ type.to_s
123
+ end
124
+ when FunctionType
125
+ params = type.param_types.map { |t| format_type(t) }.join(", ")
126
+ "(#{params}) -> #{format_type(type.return_type)}"
127
+ when Types::ClassInstance
128
+ if type.type_args.empty?
129
+ type.name.to_s
130
+ else
131
+ args = type.type_args.map { |a| format_type(a) }.join(", ")
132
+ "#{type.name}[#{args}]"
133
+ end
134
+ else
135
+ type.to_s
136
+ end
137
+ end
138
+
139
+ # Get the finalized type for a node
140
+ def type_for(node)
141
+ return Types::UNTYPED unless node
142
+
143
+ key = node_key(node)
144
+ type = @node_types[key]
145
+ type ? finalize(type) : Types::UNTYPED
146
+ end
147
+
148
+ # Get final type after applying all substitutions
149
+ def finalize(type)
150
+ @unifier.apply(type)
151
+ end
152
+
153
+ private
154
+
155
+ def node_type(node)
156
+ node.class.name.split("::").last.sub(/Node$/, "").gsub(/([a-z])([A-Z])/, '\1_\2').downcase
157
+ end
158
+
159
+ # Record type for a node (keyed by location)
160
+ def record_node_type(node, type)
161
+ key = node_key(node)
162
+ @node_types[key] = type if key
163
+ end
164
+
165
+ # Extract full constant path name from ConstantPathNode
166
+ # e.g., Java::Konpeito::Canvas::Canvas → "Java::Konpeito::Canvas::Canvas"
167
+ def extract_constant_path_name(node)
168
+ parts = []
169
+ current = node
170
+ while current.is_a?(Prism::ConstantPathNode)
171
+ parts.unshift(current.name.to_s)
172
+ current = current.parent
173
+ end
174
+ parts.unshift(current.name.to_s) if current.respond_to?(:name)
175
+ parts.join("::")
176
+ end
177
+
178
+ # Generate unique key for a node based on its location
179
+ def node_key(node)
180
+ # Use object_id for unique keys — avoids collisions across merged files
181
+ # (different files can have nodes at same byte offsets)
182
+ node.object_id
183
+ end
184
+
185
+ # Environment management
186
+ def push_env
187
+ @env.push({})
188
+ end
189
+
190
+ def pop_env
191
+ @env.pop
192
+ end
193
+
194
+ def lookup(name)
195
+ @env.reverse_each do |scope|
196
+ return scope[name.to_sym] if scope.key?(name.to_sym)
197
+ end
198
+ nil
199
+ end
200
+
201
+ def bind(name, type)
202
+ # For now, bind monomorphic types
203
+ # Full let-polymorphism would generalize here
204
+ @env.last[name.to_sym] = TypeScheme.new([], type)
205
+ end
206
+
207
+ def bind_scheme(name, scheme)
208
+ @env.last[name.to_sym] = scheme
209
+ end
210
+
211
+ # Collect function signatures (first pass)
212
+ # NOTE: Uses node_type() dispatch instead of case/when because
213
+ # Prism::Node === check returns false in Ruby 4.0 for merged ASTs
214
+ def collect_function_signatures(node)
215
+ return unless node
216
+
217
+ ntype = node_type(node)
218
+ case ntype
219
+ when "program"
220
+ node.statements.body.each { |n| collect_function_signatures(n) }
221
+ when "statements"
222
+ node.body.each { |n| collect_function_signatures(n) }
223
+ when "def"
224
+ # Try to get type signature from RBS first
225
+ actual_param_count = (node.parameters&.requireds || []).size
226
+ func_type = get_rbs_function_type(node.name.to_sym, class_name: @collect_class_name)
227
+
228
+ # Reject RBS type if param count doesn't match actual code
229
+ # (e.g., inherited Object#initialize has 0 params but user def has params)
230
+ if func_type && func_type.param_types.size != actual_param_count
231
+ func_type = nil
232
+ end
233
+ # Also reject RBS type if method has rest param but RBS doesn't account for it
234
+ # (ensures rest_param_type TypeVar gets created for *args methods)
235
+ has_rest = node.parameters&.rest.is_a?(Prism::RestParameterNode) && node.parameters.rest.name
236
+ if func_type && has_rest && !func_type.rest_param_type
237
+ func_type = nil
238
+ end
239
+
240
+ # Keep untyped return types as UNTYPED (not replaced with TypeVar).
241
+ # When RBS explicitly declares untyped, the return value may depend on
242
+ # runtime polymorphism (e.g., overridden methods) that HM can't track.
243
+
244
+ # Keep untyped param types as UNTYPED (not replaced with TypeVar).
245
+ # UNTYPED means "any type is acceptable" — method calls on UNTYPED receivers
246
+ # are allowed without constraint. TypeVar replacement would create unresolvable
247
+ # type variables when HM can't determine the type from method body alone
248
+ # (e.g., duck-typed painter/event params in UI frameworks).
249
+
250
+ unless func_type
251
+ # Create fresh type variables for parameters and return
252
+ param_types = (node.parameters&.requireds || []).map { TypeVar.new }
253
+ return_type = TypeVar.new
254
+ rest_param_type = nil
255
+ if node.parameters&.rest.is_a?(Prism::RestParameterNode) && node.parameters.rest.name
256
+ rest_param_type = TypeVar.new
257
+ end
258
+ func_type = FunctionType.new(param_types, return_type, rest_param_type: rest_param_type)
259
+ end
260
+
261
+ # Create TypeVars for keyword parameters (both required and optional)
262
+ keywords = node.parameters&.keywords || []
263
+ unless keywords.empty?
264
+ kw_vars = {}
265
+ keywords.each do |kw|
266
+ kw_vars[kw.name] = TypeVar.new(kw.name.to_s)
267
+ end
268
+ # Store with both qualified and unqualified keys
269
+ if @in_class_collect && @collect_class_name
270
+ @keyword_param_vars[:"#{@collect_class_name}##{node.name}"] = kw_vars
271
+ end
272
+ @keyword_param_vars[node.name.to_sym] = kw_vars
273
+ end
274
+
275
+ # Use class-qualified key for class methods to avoid cross-class collision
276
+ if @in_class_collect && @collect_class_name
277
+ qualified_key = :"#{@collect_class_name}##{node.name}"
278
+ @function_types[qualified_key] = func_type
279
+ @class_init_types[@collect_class_name] = func_type if node.name.to_sym == :initialize
280
+
281
+ # When overriding a parent method, unify parameter TypeVars
282
+ # so that call-site type info flows to all overrides
283
+ parent_cls = @class_parents[@collect_class_name]
284
+ while parent_cls
285
+ parent_key = :"#{parent_cls}##{node.name}"
286
+ parent_func = @function_types[parent_key]
287
+ if parent_func.is_a?(FunctionType)
288
+ parent_func.param_types.zip(func_type.param_types).each do |parent_t, child_t|
289
+ if parent_t && child_t
290
+ begin
291
+ @unifier.unify(parent_t, child_t)
292
+ rescue UnificationError
293
+ # Different param types — acceptable for polymorphic overrides
294
+ end
295
+ end
296
+ end
297
+ # Also unify rest_param_types if both have them
298
+ if parent_func.rest_param_type && func_type.rest_param_type
299
+ begin
300
+ @unifier.unify(parent_func.rest_param_type, func_type.rest_param_type)
301
+ rescue UnificationError
302
+ end
303
+ end
304
+ break
305
+ end
306
+ parent_cls = @class_parents[parent_cls]
307
+ end
308
+ end
309
+ @function_types[node.name.to_sym] = func_type
310
+
311
+ if @in_class_collect
312
+ # Class methods: monomorphic binding — call-site unification flows into method body
313
+ # This allows argument types (e.g., Array from call site) to be visible in method body
314
+ bind_scheme(node.name, TypeScheme.new([], func_type))
315
+ else
316
+ # Top-level functions: generalize to type scheme (all type vars are quantified)
317
+ all_vars = collect_type_vars(func_type)
318
+ bind_scheme(node.name, TypeScheme.new(all_vars, func_type))
319
+ end
320
+ when "class"
321
+ old_in_class = @in_class_collect
322
+ old_class_name = @collect_class_name
323
+ @in_class_collect = true
324
+ @collect_class_name = (node.constant_path.name.to_s rescue nil)
325
+ # Track parent class for super type propagation
326
+ parent_name = nil
327
+ if @collect_class_name && node.superclass
328
+ parent_name = node.superclass.name.to_s rescue nil
329
+ @class_parents[@collect_class_name] = parent_name if parent_name
330
+ end
331
+ # Register user-defined class hierarchy for subtype checking
332
+ if @collect_class_name
333
+ ancestors = []
334
+ current_parent = parent_name
335
+ while current_parent
336
+ ancestors << current_parent.to_sym
337
+ current_parent = @class_parents[current_parent]
338
+ end
339
+ ancestors << :Object unless ancestors.include?(:Object)
340
+ Types::ClassInstance.register_class_hierarchy(@collect_class_name, ancestors)
341
+ end
342
+ collect_function_signatures(node.body) if node.body
343
+ @in_class_collect = old_in_class
344
+ @collect_class_name = old_class_name
345
+ end
346
+ end
347
+
348
+ # Get function type from RBS if available
349
+ def get_rbs_function_type(method_name, class_name: nil)
350
+ return nil unless @rbs_loader&.loaded?
351
+
352
+ # Try class-specific lookup first (for methods defined inside a class)
353
+ if class_name
354
+ begin
355
+ method_types = @rbs_loader.direct_method_type(class_name.to_sym, method_name)
356
+ if method_types && !method_types.empty?
357
+ return convert_rbs_method_type(method_types.first)
358
+ end
359
+ rescue
360
+ # Ignore errors from class-specific lookup
361
+ end
362
+ end
363
+
364
+ # Try TopLevel using direct lookup (avoids inheritance conflicts)
365
+ method_types = @rbs_loader.direct_method_type(:TopLevel, method_name)
366
+ if method_types && !method_types.empty?
367
+ return convert_rbs_method_type(method_types.first)
368
+ end
369
+
370
+ # Fall back to Object and Kernel using full definition builder
371
+ %i[Object Kernel].each do |klass|
372
+ begin
373
+ method_types = @rbs_loader.method_type(klass, method_name)
374
+ next unless method_types && !method_types.empty?
375
+
376
+ return convert_rbs_method_type(method_types.first)
377
+ rescue RBS::DuplicatedMethodDefinitionError
378
+ # Skip classes with method conflicts (e.g., Object/Kernel from stdlib)
379
+ next
380
+ end
381
+ end
382
+
383
+ nil
384
+ end
385
+
386
+ # Convert RBS method type to internal FunctionType
387
+ def convert_rbs_method_type(method_type)
388
+ # Guard against UntypedFunction
389
+ if method_type.type.is_a?(RBS::Types::UntypedFunction)
390
+ return Types::FunctionType.new([], Types::UNTYPED)
391
+ end
392
+
393
+ param_types = method_type.type.required_positionals.map do |param|
394
+ rbs_type_to_internal(param.type)
395
+ end
396
+
397
+ return_type = rbs_type_to_internal(method_type.type.return_type)
398
+
399
+ # Extract rest_param_type from RBS rest_positionals (*args)
400
+ rest_param_type = nil
401
+ if method_type.type.rest_positionals
402
+ rest_param_type = rbs_type_to_internal(method_type.type.rest_positionals.type)
403
+ end
404
+
405
+ FunctionType.new(param_types, return_type, rest_param_type: rest_param_type)
406
+ end
407
+
408
+ # Convert RBS type to internal type
409
+ def rbs_type_to_internal(rbs_type)
410
+ case rbs_type
411
+ when RBS::Types::ClassInstance
412
+ name = rbs_type.name.name
413
+ args = rbs_type.args.map { |a| rbs_type_to_internal(a) }
414
+ Types::ClassInstance.new(name, args)
415
+ when RBS::Types::Variable
416
+ TypeVar.new(rbs_type.name.to_s)
417
+ when RBS::Types::Bases::Nil, RBS::Types::Bases::Void
418
+ Types::NIL
419
+ when RBS::Types::Bases::Bool
420
+ Types::BOOL
421
+ when RBS::Types::Bases::Any, RBS::Types::Bases::Top
422
+ Types::UNTYPED
423
+ when RBS::Types::Optional
424
+ inner = rbs_type_to_internal(rbs_type.type)
425
+ Types.optional(inner)
426
+ when RBS::Types::Union
427
+ types = rbs_type.types.map { |t| rbs_type_to_internal(t) }
428
+ Types.union(*types)
429
+ when RBS::Types::Alias
430
+ case rbs_type.name.name
431
+ when :string then Types::STRING
432
+ when :int then Types::INTEGER
433
+ when :float then Types::FLOAT
434
+ when :bool then Types::BOOL
435
+ else Types::UNTYPED
436
+ end
437
+ else
438
+ Types::UNTYPED
439
+ end
440
+ end
441
+
442
+ # Collect type variables from a type
443
+ def collect_type_vars(type)
444
+ vars = []
445
+ case type
446
+ when TypeVar
447
+ vars << type
448
+ when FunctionType
449
+ type.param_types.each { |t| vars.concat(collect_type_vars(t)) }
450
+ vars.concat(collect_type_vars(type.rest_param_type)) if type.rest_param_type
451
+ vars.concat(collect_type_vars(type.return_type))
452
+ when Types::ClassInstance
453
+ type.type_args.each { |t| vars.concat(collect_type_vars(t)) }
454
+ end
455
+ vars.uniq
456
+ end
457
+
458
+ # Literals
459
+ def infer_integer(_node)
460
+ Types::INTEGER
461
+ end
462
+
463
+ def infer_float(_node)
464
+ Types::FLOAT
465
+ end
466
+
467
+ def infer_string(_node)
468
+ Types::STRING
469
+ end
470
+
471
+ def infer_symbol(_node)
472
+ Types::SYMBOL
473
+ end
474
+
475
+ def infer_interpolated_string(node)
476
+ # Infer types for embedded expressions (for side effects and error detection)
477
+ node.parts.each do |part|
478
+ case part
479
+ when Prism::EmbeddedStatementsNode
480
+ infer(part.statements) if part.statements
481
+ end
482
+ end
483
+ Types::STRING
484
+ end
485
+
486
+ def infer_interpolated_symbol(node)
487
+ # Infer types for embedded expressions
488
+ node.parts.each do |part|
489
+ case part
490
+ when Prism::EmbeddedStatementsNode
491
+ infer(part.statements) if part.statements
492
+ end
493
+ end
494
+ Types::SYMBOL
495
+ end
496
+
497
+ def infer_regular_expression(_node)
498
+ Types::REGEXP
499
+ end
500
+
501
+ # Constant lookup (e.g., NativeHash, Array, etc.)
502
+ # Returns a special ClassType to represent the class itself (for class method calls)
503
+ def infer_constant_read(node)
504
+ class_name = node.name.to_sym
505
+
506
+ # Check if this constant was bound as a value constant (e.g., EXPANDING = 2).
507
+ # Value constants return their concrete type (Integer, Float, etc.).
508
+ # Class constants (Column, Widget, etc.) are not bound via infer_constant_write,
509
+ # so lookup returns nil and we fall through to ClassSingleton.
510
+ scheme = lookup(class_name)
511
+ if scheme
512
+ result = scheme.instantiate
513
+ return result unless result.is_a?(FunctionType)
514
+ end
515
+
516
+ # Return a singleton type that represents the class (not an instance)
517
+ # This will be used to look up class methods (singleton methods)
518
+ # Works both with and without RBS - user-defined classes are always ClassSingleton
519
+ Types::ClassSingleton.new(class_name)
520
+ end
521
+
522
+ # ConstantPathNode: Java::Konpeito::Canvas::Canvas
523
+ def infer_constant_path(node)
524
+ name = extract_constant_path_name(node)
525
+ Types::ClassSingleton.new(name.to_sym)
526
+ end
527
+
528
+ def infer_constant_write(node)
529
+ value_type = infer(node.value)
530
+ # Track the alias in env for later lookups
531
+ bind(node.name.to_s, value_type)
532
+ value_type
533
+ end
534
+
535
+ def infer_interpolated_regular_expression(node)
536
+ # Infer types for embedded expressions
537
+ node.parts.each do |part|
538
+ case part
539
+ when Prism::EmbeddedStatementsNode
540
+ infer(part.statements) if part.statements
541
+ end
542
+ end
543
+ Types::REGEXP
544
+ end
545
+
546
+ def infer_true(_node)
547
+ Types::TRUE_CLASS
548
+ end
549
+
550
+ def infer_false(_node)
551
+ Types::FALSE_CLASS
552
+ end
553
+
554
+ def infer_nil(_node)
555
+ Types::NIL
556
+ end
557
+
558
+ # Variables
559
+ def infer_local_variable_read(node)
560
+ scheme = lookup(node.name)
561
+ if scheme
562
+ scheme.instantiate
563
+ else
564
+ # Unknown variable, create fresh type var
565
+ tv = TypeVar.new(node.name.to_s)
566
+ bind(node.name, tv)
567
+ tv
568
+ end
569
+ end
570
+
571
+ # it block parameter (Ruby 3.4+) - reads the implicit _it_param
572
+ def infer_it_local_variable_read(_node)
573
+ scheme = lookup(:_it_param)
574
+ if scheme
575
+ scheme.instantiate
576
+ else
577
+ tv = TypeVar.new("_it_param")
578
+ bind(:_it_param, tv)
579
+ tv
580
+ end
581
+ end
582
+
583
+ def infer_local_variable_write(node)
584
+ value_type = infer(node.value)
585
+ existing = lookup(node.name)
586
+
587
+ if existing
588
+ existing_type = existing.instantiate
589
+ if existing_type.is_a?(FunctionType)
590
+ # Local variable shadows a method name (e.g., `bg = value` shadows `def bg(c)`)
591
+ # In Ruby, local variables always take precedence over methods.
592
+ # Create a new local variable binding instead of unifying with the method type.
593
+ bind(node.name, value_type)
594
+ else
595
+ # Variable already exists, unify with new value
596
+ @unifier.unify(existing_type, value_type)
597
+ end
598
+ value_type
599
+ else
600
+ # New variable
601
+ bind(node.name, value_type)
602
+ value_type
603
+ end
604
+ end
605
+
606
+ # Array
607
+ def infer_array(node)
608
+ if node.elements.empty?
609
+ Types.array(TypeVar.new)
610
+ else
611
+ element_type = TypeVar.new
612
+ heterogeneous = false
613
+ has_unresolved_typevar = false
614
+ node.elements.each do |elem|
615
+ elem_type = infer(elem)
616
+ # Don't unify unresolved TypeVars (e.g., from deferred constraints) with
617
+ # concrete element types. This prevents contamination: in [i, line, 0],
618
+ # if line is a deferred TypeVar, unifying it with Integer (from i/0) would
619
+ # incorrectly set line's type to Integer when it should be String.
620
+ resolved_elem = @unifier.apply(elem_type)
621
+ if resolved_elem.is_a?(TypeVar)
622
+ has_unresolved_typevar = true
623
+ next
624
+ end
625
+ begin
626
+ @unifier.unify(element_type, elem_type)
627
+ rescue UnificationError
628
+ # Heterogeneous array (e.g., [widget, point]) — use untyped element
629
+ heterogeneous = true
630
+ end
631
+ end
632
+ if heterogeneous || has_unresolved_typevar
633
+ Types.array(Types::UNTYPED)
634
+ else
635
+ Types.array(@unifier.apply(element_type))
636
+ end
637
+ end
638
+ end
639
+
640
+ # Hash
641
+ def infer_hash(node)
642
+ if node.elements.empty?
643
+ Types.hash_type(TypeVar.new, TypeVar.new)
644
+ else
645
+ key_type = TypeVar.new
646
+ value_type = TypeVar.new
647
+
648
+ node.elements.each do |elem|
649
+ if elem.is_a?(Prism::AssocNode)
650
+ @unifier.unify(key_type, infer(elem.key))
651
+ @unifier.unify(value_type, infer(elem.value))
652
+ end
653
+ end
654
+
655
+ Types.hash_type(@unifier.apply(key_type), @unifier.apply(value_type))
656
+ end
657
+ end
658
+
659
+ # Unify keyword args from call site with function's keyword param TypeVars
660
+ # Unify call-site argument types with function parameter types,
661
+ # including excess args against rest_param_type (for *args methods).
662
+ def unify_call_args(func_type, arg_types)
663
+ # Unify required params
664
+ func_type.param_types.zip(arg_types).each do |param_t, arg_t|
665
+ @unifier.unify(param_t, arg_t) if arg_t && param_t
666
+ end
667
+ # Unify excess args with rest_param_type
668
+ if func_type.rest_param_type && arg_types.size > func_type.param_types.size
669
+ arg_types[func_type.param_types.size..].each do |arg_t|
670
+ @unifier.unify(func_type.rest_param_type, arg_t) if arg_t
671
+ end
672
+ end
673
+ end
674
+
675
+ def unify_keyword_args(node, func_key)
676
+ return unless node.arguments
677
+
678
+ kw_vars = @keyword_param_vars[func_key]
679
+ return unless kw_vars
680
+
681
+ node.arguments.arguments.each do |arg|
682
+ next unless arg.is_a?(Prism::KeywordHashNode)
683
+
684
+ arg.elements.each do |elem|
685
+ next unless elem.is_a?(Prism::AssocNode)
686
+
687
+ # Extract keyword name from the key
688
+ key_name = case elem.key
689
+ when Prism::SymbolNode then elem.key.unescaped.to_sym
690
+ else next
691
+ end
692
+
693
+ param_var = kw_vars[key_name]
694
+ next unless param_var
695
+
696
+ value_type = infer(elem.value)
697
+ begin
698
+ @unifier.unify(param_var, value_type)
699
+ rescue UnificationError
700
+ # Type mismatch for keyword arg — continue
701
+ end
702
+ end
703
+ end
704
+ end
705
+
706
+ # Method call - this is where unification shines
707
+ def infer_call(node)
708
+ # Get receiver type
709
+ receiver_type = node.receiver ? infer(node.receiver) : Types::UNTYPED
710
+ receiver_type = @unifier.apply(receiver_type)
711
+
712
+ # Get argument types
713
+ arg_types = []
714
+ if node.arguments
715
+ arg_types = node.arguments.arguments.map { |arg| infer(arg) }
716
+ end
717
+
718
+ method_name = node.name.to_sym
719
+
720
+ # Try to find function in our environment (for self-calls / top-level functions only)
721
+ # Do NOT use bare lookup for receiver calls — it would match wrong class methods
722
+ unless node.receiver
723
+ # For self-calls in a class, prefer class-qualified lookup first
724
+ # Walk up class hierarchy to find inherited methods
725
+ scheme = nil
726
+ if @current_class_name
727
+ cls = @current_class_name
728
+ while cls
729
+ func_type = @function_types[:"#{cls}##{method_name}"]
730
+ if func_type.is_a?(FunctionType)
731
+ # Monomorphic binding — self-calls share TypeVars with the method definition
732
+ scheme = TypeScheme.new([], func_type)
733
+ break
734
+ end
735
+ cls = @class_parents[cls]
736
+ end
737
+ end
738
+ scheme ||= lookup(method_name)
739
+ unless scheme
740
+ # Fallback: look up in @function_types (unqualified)
741
+ func_type = @function_types[method_name]
742
+ if func_type.is_a?(FunctionType)
743
+ scheme = TypeScheme.new([], func_type)
744
+ end
745
+ end
746
+ if scheme
747
+ func_type = scheme.instantiate
748
+ if func_type.is_a?(FunctionType)
749
+ unify_call_args(func_type, arg_types)
750
+ # For generalized schemes with deferred constraints, propagate
751
+ # call-site arg types to resolve body's deferred method calls.
752
+ # Returns the resolved return type directly (without modifying original TypeVars).
753
+ resolved_ret = nil
754
+ if !scheme.type_vars.empty? && !@deferred_constraints.empty?
755
+ resolved_ret = propagate_call_site_types(scheme, arg_types)
756
+ end
757
+ # Unify keyword args from call site
758
+ unify_keyword_args(node, method_name)
759
+ if @current_class_name
760
+ unify_keyword_args(node, :"#{@current_class_name}##{method_name}")
761
+ end
762
+ infer_block_body_without_rbs(node.block) if node.block
763
+ return resolved_ret || @unifier.apply(func_type.return_type)
764
+ end
765
+ end
766
+ end
767
+
768
+ # Receiver-based call on user class: look up class-qualified method type
769
+ # Walk up class hierarchy to find inherited methods
770
+ # Call-site unification flows argument types into method body (monomorphic binding)
771
+ if node.receiver && receiver_type.is_a?(Types::ClassInstance)
772
+ class_name = receiver_type.name.to_s
773
+ cls = class_name
774
+ func_type = nil
775
+ while cls
776
+ func_type = @function_types[:"#{cls}##{method_name}"]
777
+ break if func_type.is_a?(FunctionType)
778
+ cls = @class_parents[cls]
779
+ end
780
+ if func_type.is_a?(FunctionType)
781
+ # Directly unify — monomorphic binding allows call-site types to flow into method body
782
+ unify_call_args(func_type, arg_types)
783
+ # Unify keyword args from call site
784
+ unify_keyword_args(node, :"#{class_name}##{method_name}")
785
+ infer_block_body_without_rbs(node.block) if node.block
786
+ return @unifier.apply(func_type.return_type)
787
+ end
788
+ end
789
+
790
+ # NOTE: Structural type resolution heuristic (resolve_typevar_receiver) was removed.
791
+ # TypeVar receivers now fall through to the deferred constraint path below,
792
+ # which resolves them after call-sites provide concrete types (Kotlin-style).
793
+
794
+ # Built-in operators (check BEFORE RBS lookup to ensure correct types)
795
+ # This ensures comparison operators return bool, not Numeric from RBS
796
+ result = infer_builtin_method(receiver_type, method_name, arg_types)
797
+ return result if result
798
+
799
+ # Try RBS lookup for receiver's methods
800
+ if @rbs_loader&.loaded? && receiver_type.is_a?(Types::ClassInstance)
801
+ rbs_result = infer_from_rbs(receiver_type, method_name, arg_types, node)
802
+ return rbs_result if rbs_result
803
+ end
804
+
805
+ # JVM interop: look up method signatures from @jvm_classes (classpath introspection)
806
+ # Must be checked BEFORE infer_singleton_method to avoid matching inherited Kernel#open etc.
807
+ if @rbs_loader && receiver_type.is_a?(Types::ClassSingleton)
808
+ jvm_result = infer_jvm_class_method(receiver_type.name, method_name, arg_types)
809
+ return jvm_result if jvm_result
810
+ end
811
+
812
+ # JVM interop: instance method lookup on Java class instances
813
+ if @rbs_loader && receiver_type.is_a?(Types::ClassInstance)
814
+ jvm_result = infer_jvm_instance_method(receiver_type.name, method_name, arg_types)
815
+ return jvm_result if jvm_result
816
+ end
817
+
818
+ # Try RBS lookup for singleton methods (class methods like NativeHash.new)
819
+ # Only for generic types, not for built-in types like NativeArray
820
+ if @rbs_loader&.loaded? && receiver_type.is_a?(Types::ClassSingleton)
821
+ # Skip for known native types that are handled specially by HIR builder
822
+ unless %i[NativeArray NativeClass].include?(receiver_type.name)
823
+ rbs_result = infer_singleton_method(receiver_type.name, method_name, arg_types, node)
824
+ return rbs_result if rbs_result
825
+ end
826
+ end
827
+
828
+ # Standard library singleton method type inference
829
+ if receiver_type.is_a?(Types::ClassSingleton)
830
+ stdlib_result = infer_stdlib_singleton_method(receiver_type.name, method_name, arg_types)
831
+ return stdlib_result if stdlib_result
832
+ end
833
+
834
+ # User-defined class .new → returns ClassInstance of that class
835
+ # Unify argument types with initialize parameter types (like Kotlin constructor calls)
836
+ if receiver_type.is_a?(Types::ClassSingleton) && method_name == :new
837
+ init_type = @class_init_types[receiver_type.name.to_s]
838
+ if init_type
839
+ unify_call_args(init_type, arg_types)
840
+ end
841
+ return Types::ClassInstance.new(receiver_type.name)
842
+ end
843
+
844
+ # User-defined class methods (def self.xxx) — look up in @function_types
845
+ if receiver_type.is_a?(Types::ClassSingleton)
846
+ class_name = receiver_type.name.to_s
847
+ func_key = :"#{class_name}##{method_name}"
848
+ func_type = @function_types[func_key]
849
+ if func_type.is_a?(FunctionType)
850
+ unify_call_args(func_type, arg_types)
851
+ return func_type.return_type
852
+ end
853
+ end
854
+
855
+ # User-defined instance method calls — look up in @function_types
856
+ if receiver_type.is_a?(Types::ClassInstance)
857
+ class_name = receiver_type.name.to_s
858
+ func_key = :"#{class_name}##{method_name}"
859
+ func_type = @function_types[func_key]
860
+ # Also check parent classes
861
+ unless func_type
862
+ parent = @class_parents[class_name]
863
+ while parent && !func_type
864
+ func_type = @function_types[:"#{parent}##{method_name}"]
865
+ parent = @class_parents[parent]
866
+ end
867
+ end
868
+ if func_type.is_a?(FunctionType)
869
+ unify_call_args(func_type, arg_types)
870
+ return func_type.return_type
871
+ end
872
+ end
873
+
874
+ # Built-in methods that don't need a specific receiver type
875
+ return Types::BOOL if method_name == :block_given? && !node.receiver
876
+
877
+ # UNTYPED receiver: any method call is allowed (RBS escape hatch).
878
+ # This handles duck-typed params like painter, event objects in UI frameworks.
879
+ if receiver_type == Types::UNTYPED
880
+ return Types::UNTYPED
881
+ end
882
+
883
+ # Deferred constraint: if receiver is an unresolved TypeVar, defer resolution
884
+ # until after all call-sites have unified their types (Kotlin-style deferred resolution)
885
+ resolved_receiver = @unifier.apply(receiver_type)
886
+ # UNTYPED after resolution (TypeVar unified with UNTYPED)
887
+ return Types::UNTYPED if resolved_receiver == Types::UNTYPED
888
+ if resolved_receiver.is_a?(TypeVar)
889
+ result_var = TypeVar.new
890
+ @deferred_constraints << {
891
+ receiver: resolved_receiver,
892
+ method_name: method_name,
893
+ arg_types: arg_types,
894
+ result_var: result_var,
895
+ node: node
896
+ }
897
+ return result_var
898
+ end
899
+
900
+ # Unknown method on concrete type — immediate error
901
+ @inference_errors ||= []
902
+ recv_desc = resolved_receiver.to_s
903
+ @inference_errors << "Cannot resolve method '#{method_name}' on #{recv_desc}"
904
+ TypeVar.new
905
+ end
906
+
907
+ # Attempt to resolve a method call on a now-concrete receiver type.
908
+ # Used by deferred constraint resolution after TypeVars are unified.
909
+ # Returns the resolved return type, or nil if unresolvable.
910
+ def try_resolve_method(receiver_type, method_name, arg_types, node = nil)
911
+ # UNTYPED receiver: any method call is allowed
912
+ return Types::UNTYPED if receiver_type == Types::UNTYPED
913
+
914
+ # Built-in operators
915
+ result = infer_builtin_method(receiver_type, method_name, arg_types)
916
+ return result if result
917
+
918
+ # RBS lookup
919
+ if @rbs_loader&.loaded? && receiver_type.is_a?(Types::ClassInstance)
920
+ rbs_result = infer_from_rbs(receiver_type, method_name, arg_types, node)
921
+ return rbs_result if rbs_result
922
+ end
923
+
924
+ # JVM interop
925
+ if @rbs_loader && receiver_type.is_a?(Types::ClassInstance)
926
+ jvm_result = infer_jvm_instance_method(receiver_type.name, method_name, arg_types)
927
+ return jvm_result if jvm_result
928
+ end
929
+
930
+ # User-defined instance methods
931
+ if receiver_type.is_a?(Types::ClassInstance)
932
+ class_name = receiver_type.name.to_s
933
+ cls = class_name
934
+ func_type = nil
935
+ while cls
936
+ func_type = @function_types[:"#{cls}##{method_name}"]
937
+ break if func_type.is_a?(FunctionType)
938
+ cls = @class_parents[cls]
939
+ end
940
+ if func_type.is_a?(FunctionType)
941
+ unify_call_args(func_type, arg_types)
942
+ return @unifier.apply(func_type.return_type)
943
+ end
944
+ end
945
+
946
+ nil
947
+ end
948
+
949
+ # Propagate call-site argument types into a generalized function's deferred constraints.
950
+ # Uses a LOCAL solutions map (no modification of original TypeVars) to preserve polymorphism.
951
+ # Returns the resolved return type, or nil if unresolvable.
952
+ def propagate_call_site_types(scheme, arg_types)
953
+ original_type = scheme.type
954
+ return nil unless original_type.is_a?(FunctionType)
955
+
956
+ # Build solutions: original param TypeVar ID → concrete type from call site
957
+ solutions = {}
958
+ original_type.param_types.zip(arg_types).each do |orig_param, arg_t|
959
+ orig = orig_param
960
+ orig = orig.prune if orig.is_a?(TypeVar)
961
+ if orig.is_a?(TypeVar) && arg_t && !arg_t.is_a?(TypeVar)
962
+ solutions[orig.id] = arg_t
963
+ end
964
+ end
965
+ # Collect excess args for rest_param_type
966
+ if original_type.rest_param_type
967
+ rest_tv = original_type.rest_param_type
968
+ rest_tv = rest_tv.prune if rest_tv.is_a?(TypeVar)
969
+ if rest_tv.is_a?(TypeVar)
970
+ excess = arg_types[original_type.param_types.size..]
971
+ if excess && !excess.empty?
972
+ concrete = excess.find { |t| !t.is_a?(TypeVar) }
973
+ solutions[rest_tv.id] = concrete if concrete
974
+ end
975
+ end
976
+ end
977
+ return nil if solutions.empty?
978
+
979
+ # Iteratively resolve deferred constraints using local solutions map
980
+ 5.times do
981
+ changed = false
982
+ @deferred_constraints.each do |constraint|
983
+ receiver = resolve_with_solutions(constraint[:receiver], solutions)
984
+ next if receiver.is_a?(TypeVar)
985
+
986
+ # Skip if already resolved in this pass
987
+ result_var = constraint[:result_var]
988
+ pruned_result = result_var.is_a?(TypeVar) ? result_var.prune : result_var
989
+ result_id = pruned_result.is_a?(TypeVar) ? pruned_result.id : nil
990
+ next if result_id && solutions[result_id]
991
+
992
+ resolved_args = constraint[:arg_types].map { |t| resolve_with_solutions(t, solutions) }
993
+ result_type = try_resolve_method(receiver, constraint[:method_name], resolved_args, constraint[:node])
994
+ if result_type
995
+ solutions[result_id] = result_type if result_id
996
+ changed = true
997
+ end
998
+ end
999
+ break unless changed
1000
+ end
1001
+
1002
+ # Return the resolved return type (without modifying original TypeVars)
1003
+ orig_ret = original_type.return_type
1004
+ orig_ret = orig_ret.prune if orig_ret.is_a?(TypeVar)
1005
+ resolved_ret = resolve_with_solutions(orig_ret, solutions)
1006
+ resolved_ret.is_a?(TypeVar) ? nil : resolved_ret
1007
+ end
1008
+
1009
+ # Resolve a type using a local solutions map, falling back to unifier.
1010
+ def resolve_with_solutions(type, solutions)
1011
+ if type.is_a?(TypeVar)
1012
+ pruned = type.prune
1013
+ return pruned unless pruned.is_a?(TypeVar)
1014
+ return solutions[pruned.id] || pruned
1015
+ end
1016
+ type
1017
+ end
1018
+
1019
+ # Resolve deferred constraints iteratively until fixed-point.
1020
+ # After all call-sites have unified TypeVars, deferred method calls
1021
+ # on now-concrete receivers can be resolved (Kotlin-style deferred resolution).
1022
+ def resolve_deferred_constraints
1023
+ max_iterations = 10 # Safety limit to prevent infinite loops
1024
+ iteration = 0
1025
+
1026
+ while iteration < max_iterations
1027
+ changed = false
1028
+ iteration += 1
1029
+
1030
+ @deferred_constraints.reject! do |constraint|
1031
+ receiver = @unifier.apply(constraint[:receiver])
1032
+ next false if receiver.is_a?(TypeVar) # Still unresolved
1033
+
1034
+ # Receiver is now concrete — try to resolve the method
1035
+ resolved_args = constraint[:arg_types].map { |t| @unifier.apply(t) }
1036
+ result_type = try_resolve_method(
1037
+ receiver,
1038
+ constraint[:method_name],
1039
+ resolved_args,
1040
+ constraint[:node]
1041
+ )
1042
+
1043
+ if result_type
1044
+ # Successfully resolved — unify with the placeholder result TypeVar
1045
+ begin
1046
+ @unifier.unify(constraint[:result_var], result_type)
1047
+ rescue UnificationError
1048
+ # Type mismatch — record as error
1049
+ @inference_errors << "Type mismatch resolving deferred '#{constraint[:method_name]}' on #{receiver}"
1050
+ end
1051
+ changed = true
1052
+ true # Remove from deferred list
1053
+ else
1054
+ # Method not found even on concrete type — record error and remove
1055
+ @inference_errors << "Cannot resolve method '#{constraint[:method_name]}' on #{receiver}"
1056
+ changed = true
1057
+ true # Remove
1058
+ end
1059
+ end
1060
+
1061
+ break unless changed
1062
+ end
1063
+
1064
+ # Any remaining deferred constraints (receiver still TypeVar) → errors
1065
+ @deferred_constraints.each do |constraint|
1066
+ receiver = @unifier.apply(constraint[:receiver])
1067
+ @inference_errors << "Cannot resolve method '#{constraint[:method_name]}' on #{receiver}"
1068
+ end
1069
+ @deferred_constraints.clear
1070
+
1071
+ # Path-compress all function types: ensure TypeVars resolved through
1072
+ # deferred constraints are visible via the unification chain.
1073
+ @function_types.each do |_key, func_type|
1074
+ next unless func_type.is_a?(FunctionType)
1075
+ func_type.param_types.each { |pt| @unifier.apply(pt) }
1076
+ @unifier.apply(func_type.rest_param_type) if func_type.rest_param_type
1077
+ @unifier.apply(func_type.return_type)
1078
+ end
1079
+ end
1080
+
1081
+ # Look up method type from JVM class registry (for classpath-introspected methods)
1082
+ def infer_jvm_class_method(class_name, method_name, arg_types)
1083
+ return nil unless @rbs_loader.respond_to?(:jvm_classes)
1084
+
1085
+ jvm_info = @rbs_loader.jvm_classes[class_name.to_s]
1086
+ return nil unless jvm_info
1087
+
1088
+ # .new → returns ClassInstance of this class
1089
+ if method_name == :new && jvm_info[:constructor_params]
1090
+ ctor_params = jvm_info[:constructor_params].map { |t| jvm_tag_to_hm_type(t) }
1091
+ ctor_params.zip(arg_types).each do |param_t, arg_t|
1092
+ @unifier.unify(param_t, arg_t) if arg_t && param_t
1093
+ end
1094
+ return Types::ClassInstance.new(class_name)
1095
+ end
1096
+
1097
+ method_info = jvm_info[:static_methods]&.dig(method_name.to_s)
1098
+ return nil unless method_info
1099
+
1100
+ param_types = (method_info[:params] || []).map { |t| jvm_tag_to_hm_type(t) }
1101
+ return_type = jvm_tag_to_hm_type(method_info[:return] || :void)
1102
+
1103
+ # Unify argument types with parameter types
1104
+ param_types.zip(arg_types).each do |param_t, arg_t|
1105
+ @unifier.unify(param_t, arg_t) if arg_t && param_t
1106
+ end
1107
+
1108
+ return_type
1109
+ end
1110
+
1111
+ # Look up instance method type from JVM class registry
1112
+ def infer_jvm_instance_method(class_name, method_name, arg_types)
1113
+ return nil unless @rbs_loader.respond_to?(:jvm_classes)
1114
+
1115
+ jvm_info = @rbs_loader.jvm_classes[class_name.to_s]
1116
+ return nil unless jvm_info
1117
+
1118
+ method_info = jvm_info[:methods]&.dig(method_name.to_s)
1119
+ return nil unless method_info
1120
+
1121
+ param_types = (method_info[:params] || []).map { |t| jvm_tag_to_hm_type(t) }
1122
+ return_type = jvm_tag_to_hm_type(method_info[:return] || :void)
1123
+
1124
+ param_types.zip(arg_types).each do |param_t, arg_t|
1125
+ @unifier.unify(param_t, arg_t) if arg_t && param_t
1126
+ end
1127
+
1128
+ return_type
1129
+ end
1130
+
1131
+ # Convert JVM type tag to HM type
1132
+ def jvm_tag_to_hm_type(tag)
1133
+ case tag
1134
+ when :i64 then Types::INTEGER
1135
+ when :double then Types::FLOAT
1136
+ when :string then Types::STRING
1137
+ when :i8 then Types::BOOL
1138
+ when :void then Types::UNTYPED
1139
+ when :value then Types::UNTYPED
1140
+ else Types::UNTYPED
1141
+ end
1142
+ end
1143
+
1144
+ # Infer types for standard library class methods
1145
+ def infer_stdlib_singleton_method(class_name, method_name, arg_types)
1146
+ case class_name
1147
+ when :File
1148
+ case method_name
1149
+ when :read, :readlines then Types::STRING
1150
+ when :write then Types::INTEGER
1151
+ when :exist?, :exists?, :file?, :directory?, :symlink?, :readable?, :writable?
1152
+ Types::BOOL
1153
+ when :delete, :unlink then Types::INTEGER
1154
+ when :open then Types::UNTYPED
1155
+ when :basename, :dirname, :extname, :expand_path, :join, :absolute_path
1156
+ Types::STRING
1157
+ when :size then Types::INTEGER
1158
+ when :mtime, :atime, :ctime then Types::TIME
1159
+ end
1160
+ when :Dir
1161
+ case method_name
1162
+ when :glob then Types.array(Types::STRING)
1163
+ when :mkdir then Types::INTEGER
1164
+ when :exist?, :exists? then Types::BOOL
1165
+ when :pwd, :getwd, :home then Types::STRING
1166
+ when :entries, :children then Types.array(Types::STRING)
1167
+ end
1168
+ when :Time
1169
+ case method_name
1170
+ when :now, :new, :at, :mktime, :local, :utc, :gm then Types::TIME
1171
+ end
1172
+ when :ENV
1173
+ case method_name
1174
+ when :[], :fetch then Types::STRING
1175
+ when :keys, :values then Types.array(Types::STRING)
1176
+ when :has_key?, :key?, :include? then Types::BOOL
1177
+ end
1178
+ end
1179
+ end
1180
+
1181
+ # Look up singleton method (class method) type from RBS
1182
+ def infer_singleton_method(class_name, method_name, arg_types, node)
1183
+ # Get singleton method types from RBS
1184
+ method_types = @rbs_loader.method_type(class_name, method_name, singleton: true)
1185
+ return nil unless method_types && !method_types.empty?
1186
+
1187
+ # Select best overload based on argument types
1188
+ method_type = select_overload(method_types, arg_types)
1189
+
1190
+ # Get class type parameters from RBS for substitution
1191
+ substitution = build_singleton_method_substitution(class_name, method_type)
1192
+
1193
+ # Guard against UntypedFunction (which lacks required_positionals)
1194
+ return Types::UNTYPED if method_type.type.is_a?(RBS::Types::UntypedFunction)
1195
+
1196
+ # Unify argument types with parameter types from RBS
1197
+ rbs_params = method_type.type.required_positionals + method_type.type.optional_positionals
1198
+ arg_types.each_with_index do |arg_type, i|
1199
+ next unless rbs_params[i]
1200
+ expected_type = substitute_rbs_type(rbs_params[i].type, substitution)
1201
+ @unifier.unify(arg_type, expected_type)
1202
+ end
1203
+
1204
+ # Convert and return the return type
1205
+ return_type = substitute_rbs_type(method_type.type.return_type, substitution)
1206
+
1207
+ # Handle block if present
1208
+ if node.block && method_type.block
1209
+ infer_block_for_rbs(node.block, method_type.block, substitution)
1210
+ end
1211
+
1212
+ @unifier.apply(return_type)
1213
+ end
1214
+
1215
+ # Build substitution map for singleton method type parameters
1216
+ def build_singleton_method_substitution(class_name, method_type)
1217
+ substitution = {}
1218
+
1219
+ # Get class type parameters from RBS
1220
+ type_name = RBS::TypeName.new(name: class_name, namespace: RBS::Namespace.root)
1221
+ class_decl = @rbs_loader.environment.class_decls[type_name]
1222
+
1223
+ if class_decl
1224
+ params = class_decl.decls.first.decl.type_params
1225
+ params.each do |param|
1226
+ # For singleton methods, type params are not bound to instances
1227
+ # They stay as type variables unless specified in return type
1228
+ substitution[param.name] = TypeVar.new(param.name.to_s)
1229
+ end
1230
+ end
1231
+
1232
+ # Handle method-level type parameters
1233
+ if method_type.type_params && !method_type.type_params.empty?
1234
+ method_type.type_params.each do |param|
1235
+ substitution[param.name] = TypeVar.new(param.name.to_s)
1236
+ end
1237
+ end
1238
+
1239
+ substitution
1240
+ end
1241
+
1242
+ # Look up method type from RBS and instantiate type parameters
1243
+ def infer_from_rbs(receiver_type, method_name, arg_types, node)
1244
+ class_name = receiver_type.name
1245
+ type_args = receiver_type.type_args
1246
+
1247
+ # Get method types from RBS
1248
+ method_types = @rbs_loader.method_type(class_name, method_name)
1249
+ return nil unless method_types && !method_types.empty?
1250
+
1251
+ # Select best overload based on argument types
1252
+ method_type = select_overload(method_types, arg_types)
1253
+
1254
+ # Build substitution map from type parameters to actual type arguments
1255
+ substitution = build_type_substitution(class_name, type_args)
1256
+ substitution[:__self__] = receiver_type
1257
+
1258
+ # Handle method-level type parameters (like `map: [U] { (T) -> U } -> Array[U]`)
1259
+ if method_type.type_params && !method_type.type_params.empty?
1260
+ method_type.type_params.each do |param|
1261
+ # Create fresh type variable for each method type parameter
1262
+ substitution[param.name] = TypeVar.new(param.name.to_s)
1263
+ end
1264
+ end
1265
+
1266
+ # Guard against UntypedFunction (which lacks required_positionals)
1267
+ return Types::UNTYPED if method_type.type.is_a?(RBS::Types::UntypedFunction)
1268
+
1269
+ # Unify argument types with parameter types from RBS
1270
+ # This allows us to infer argument types from method signatures
1271
+ rbs_params = method_type.type.required_positionals + method_type.type.optional_positionals
1272
+ arg_types.each_with_index do |arg_type, i|
1273
+ next unless rbs_params[i]
1274
+ expected_type = substitute_rbs_type(rbs_params[i].type, substitution)
1275
+ @unifier.unify(arg_type, expected_type)
1276
+ end
1277
+
1278
+ # Substitute and convert return type
1279
+ return_type = substitute_rbs_type(method_type.type.return_type, substitution)
1280
+
1281
+ # Handle block if present (for methods like map)
1282
+ if node.block && method_type.block
1283
+ infer_block_for_rbs(node.block, method_type.block, substitution)
1284
+ end
1285
+
1286
+ @unifier.apply(return_type)
1287
+ end
1288
+
1289
+ # Build substitution map from class type parameters to actual types
1290
+ def build_type_substitution(class_name, type_args)
1291
+ substitution = {}
1292
+
1293
+ # Get class type parameters from RBS
1294
+ type_name = RBS::TypeName.new(name: class_name, namespace: RBS::Namespace.root)
1295
+ class_decl = @rbs_loader.environment.class_decls[type_name]
1296
+
1297
+ if class_decl
1298
+ params = class_decl.decls.first.decl.type_params
1299
+ params.each_with_index do |param, i|
1300
+ if type_args[i]
1301
+ substitution[param.name] = type_args[i]
1302
+ else
1303
+ substitution[param.name] = TypeVar.new(param.name.to_s)
1304
+ end
1305
+ end
1306
+ end
1307
+
1308
+ substitution
1309
+ end
1310
+
1311
+ # Convert RBS type to our internal type, applying substitution
1312
+ def substitute_rbs_type(rbs_type, substitution)
1313
+ case rbs_type
1314
+ when RBS::Types::Variable
1315
+ # Type variable - look up in substitution
1316
+ substitution[rbs_type.name] || TypeVar.new(rbs_type.name.to_s)
1317
+
1318
+ when RBS::Types::Alias
1319
+ # Type alias - resolve common ones
1320
+ case rbs_type.name.name
1321
+ when :string then Types::STRING
1322
+ when :int then Types::INTEGER
1323
+ when :float then Types::FLOAT
1324
+ when :bool, :boolish then Types::BOOL
1325
+ when :array then Types.array(Types::UNTYPED)
1326
+ when :hash then Types.hash_type(Types::UNTYPED, Types::UNTYPED)
1327
+ else Types::UNTYPED
1328
+ end
1329
+
1330
+ when RBS::Types::ClassInstance
1331
+ name = rbs_type.name.name
1332
+ # Check if this is a type parameter reference (looks like ClassInstance but is actually a type variable)
1333
+ # This happens when RBS parses K, V in NativeHash[K, V] as ClassInstance instead of Variable
1334
+ if substitution.key?(name)
1335
+ substitution[name]
1336
+ else
1337
+ # Use fully qualified name for nested classes (e.g., Ractor::Port instead of Port)
1338
+ qualified_name = rbs_qualified_name(rbs_type.name)
1339
+ # Recursively substitute type arguments
1340
+ args = rbs_type.args.map { |a| substitute_rbs_type(a, substitution) }
1341
+ Types::ClassInstance.new(qualified_name, args)
1342
+ end
1343
+
1344
+ when RBS::Types::Bases::Nil, RBS::Types::Bases::Void
1345
+ Types::NIL
1346
+
1347
+ when RBS::Types::Bases::Bool
1348
+ Types::BOOL
1349
+
1350
+ when RBS::Types::Bases::Any, RBS::Types::Bases::Top
1351
+ Types::UNTYPED
1352
+
1353
+ when RBS::Types::Bases::Bottom
1354
+ Types::BOTTOM
1355
+
1356
+ when RBS::Types::Bases::Self
1357
+ # Resolve to receiver type when available
1358
+ substitution[:__self__] || Types::UNTYPED
1359
+
1360
+ when RBS::Types::Optional
1361
+ # T? becomes Union[T, nil]
1362
+ inner = substitute_rbs_type(rbs_type.type, substitution)
1363
+ Types.optional(inner)
1364
+
1365
+ when RBS::Types::Union
1366
+ types = rbs_type.types.map { |t| substitute_rbs_type(t, substitution) }
1367
+ Types.union(*types)
1368
+
1369
+ when RBS::Types::Tuple
1370
+ types = rbs_type.types.map { |t| substitute_rbs_type(t, substitution) }
1371
+ Types::Tuple.new(types)
1372
+
1373
+ when RBS::Types::Literal
1374
+ # For integer literals, preserve the value (important for StaticArray[T, N] where N is a literal)
1375
+ # For other literals, convert to the base type
1376
+ case rbs_type.literal
1377
+ when Integer
1378
+ Types::Literal.new(rbs_type.literal)
1379
+ when Float then Types::FLOAT
1380
+ when String then Types::STRING
1381
+ when Symbol then Types::SYMBOL
1382
+ when true then Types::TRUE_CLASS
1383
+ when false then Types::FALSE_CLASS
1384
+ else Types::UNTYPED
1385
+ end
1386
+
1387
+ else
1388
+ Types::UNTYPED
1389
+ end
1390
+ end
1391
+
1392
+ # Convert RBS::TypeName to a qualified symbol (e.g., :"Ractor::Port" for nested classes)
1393
+ def rbs_qualified_name(type_name)
1394
+ ns = type_name.namespace
1395
+ if ns.path.empty?
1396
+ type_name.name
1397
+ else
1398
+ :"#{ns.path.map(&:to_s).join("::")}::#{type_name.name}"
1399
+ end
1400
+ end
1401
+
1402
+ # Select the best overload based on argument types
1403
+ def select_overload(method_types, arg_types)
1404
+ return method_types.first if method_types.size == 1
1405
+
1406
+ # Score each overload by how well it matches argument types
1407
+ scored = method_types.map do |mt|
1408
+ score = 0
1409
+ next [mt, 0] if mt.type.is_a?(RBS::Types::UntypedFunction)
1410
+
1411
+ rbs_params = mt.type.required_positionals + mt.type.optional_positionals
1412
+
1413
+ arg_types.each_with_index do |arg_type, i|
1414
+ next unless rbs_params[i]
1415
+ rbs_param_type = rbs_params[i].type
1416
+ arg_type = @unifier.apply(arg_type)
1417
+
1418
+ # Check if argument type matches parameter type
1419
+ score += overload_match_score(arg_type, rbs_param_type)
1420
+ end
1421
+
1422
+ [mt, score]
1423
+ end
1424
+
1425
+ # Return the overload with highest score
1426
+ scored.max_by { |_, score| score }&.first || method_types.first
1427
+ end
1428
+
1429
+ # Score how well an argument type matches an RBS parameter type
1430
+ def overload_match_score(arg_type, rbs_param_type)
1431
+ case rbs_param_type
1432
+ when RBS::Types::ClassInstance
1433
+ param_name = rbs_param_type.name.name
1434
+ if arg_type.is_a?(Types::ClassInstance)
1435
+ # Exact match
1436
+ return 10 if arg_type.name == param_name
1437
+ # Check for common numeric hierarchy
1438
+ return 5 if numeric_compatible?(arg_type.name, param_name)
1439
+ end
1440
+ when RBS::Types::Bases::Any
1441
+ return 1 # Any matches anything but with low priority
1442
+ end
1443
+
1444
+ 0
1445
+ end
1446
+
1447
+ def numeric_compatible?(arg_name, param_name)
1448
+ numerics = %i[Integer Float Rational Complex Numeric]
1449
+ numerics.include?(arg_name) && numerics.include?(param_name)
1450
+ end
1451
+
1452
+ # Infer block type for methods like map
1453
+ def infer_block_for_rbs(block_node, rbs_block, substitution)
1454
+ # The block's return type should unify with the RBS block's return type
1455
+ # For example: map's block { (Elem) -> U } means block returns U
1456
+
1457
+ push_env
1458
+
1459
+ # Bind block parameters (skip if UntypedFunction)
1460
+ if block_node.parameters && rbs_block.type.respond_to?(:required_positionals)
1461
+ if block_node.parameters.is_a?(Prism::NumberedParametersNode)
1462
+ # Numbered block parameters (_1, _2, ...)
1463
+ rbs_positionals = rbs_block.type.required_positionals
1464
+ block_node.parameters.maximum.times do |i|
1465
+ if rbs_positionals[i]
1466
+ param_type = substitute_rbs_type(rbs_positionals[i].type, substitution)
1467
+ bind(:"_#{i + 1}", param_type)
1468
+ end
1469
+ end
1470
+ elsif block_node.parameters.is_a?(Prism::ItParametersNode)
1471
+ # it block parameter (Ruby 3.4+)
1472
+ rbs_positionals = rbs_block.type.required_positionals
1473
+ if rbs_positionals[0]
1474
+ param_type = substitute_rbs_type(rbs_positionals[0].type, substitution)
1475
+ bind(:_it_param, param_type)
1476
+ end
1477
+ else
1478
+ block_node.parameters.parameters&.requireds&.zip(
1479
+ rbs_block.type.required_positionals
1480
+ )&.each do |param, rbs_param|
1481
+ if param.respond_to?(:name) && rbs_param
1482
+ param_type = substitute_rbs_type(rbs_param.type, substitution)
1483
+ bind(param.name, param_type)
1484
+ end
1485
+ end
1486
+ end
1487
+ end
1488
+
1489
+ # Infer block body
1490
+ body_type = block_node.body ? infer(block_node.body) : Types::NIL
1491
+
1492
+ # Unify with expected return type (skip if UntypedFunction or void)
1493
+ if rbs_block.type.respond_to?(:return_type)
1494
+ rbs_ret = rbs_block.type.return_type
1495
+ # RBS void means "return value is irrelevant" — don't constrain block return type
1496
+ unless rbs_ret.is_a?(RBS::Types::Bases::Void)
1497
+ expected_return = substitute_rbs_type(rbs_ret, substitution)
1498
+ @unifier.unify(expected_return, body_type)
1499
+ end
1500
+ end
1501
+
1502
+ pop_env
1503
+ end
1504
+
1505
+ # Infer block body without RBS block type info.
1506
+ # Block parameters become fresh TypeVars; captured variables retain outer scope types.
1507
+ def infer_block_body_without_rbs(block_node)
1508
+ push_env
1509
+ if block_node.parameters
1510
+ if block_node.parameters.is_a?(Prism::NumberedParametersNode)
1511
+ block_node.parameters.maximum.times do |i|
1512
+ bind(:"_#{i + 1}", TypeVar.new)
1513
+ end
1514
+ elsif block_node.parameters.is_a?(Prism::ItParametersNode)
1515
+ bind(:it, TypeVar.new)
1516
+ elsif block_node.parameters.respond_to?(:parameters) && block_node.parameters.parameters
1517
+ block_node.parameters.parameters.requireds&.each do |param|
1518
+ if param.respond_to?(:name) && param.name
1519
+ bind(param.name.to_sym, TypeVar.new)
1520
+ end
1521
+ end
1522
+ end
1523
+ end
1524
+ infer(block_node.body) if block_node.body
1525
+ pop_env
1526
+ end
1527
+
1528
+ def infer_builtin_method(receiver_type, method_name, arg_types)
1529
+ receiver_type = @unifier.apply(receiver_type)
1530
+ arg_type = arg_types.first ? @unifier.apply(arg_types.first) : nil
1531
+
1532
+ case method_name
1533
+ when :call
1534
+ # .call on any receiver (block/proc/lambda stored in ivar) returns UNTYPED
1535
+ # This handles patterns like @click_handler.call, @callback.call(args)
1536
+ return Types::UNTYPED
1537
+ when :+, :-, :*, :/, :%
1538
+ # If receiver is a type variable and arg is concrete, unify
1539
+ if receiver_type.is_a?(TypeVar) && arg_type
1540
+ if arg_type == Types::INTEGER || arg_type == Types::FLOAT
1541
+ @unifier.unify(receiver_type, arg_type)
1542
+ return arg_type
1543
+ elsif arg_type == Types::STRING && method_name == :+
1544
+ @unifier.unify(receiver_type, Types::STRING)
1545
+ return Types::STRING
1546
+ end
1547
+ end
1548
+
1549
+ if receiver_type == Types::INTEGER
1550
+ if arg_type == Types::FLOAT
1551
+ return Types::FLOAT
1552
+ end
1553
+ # Unify arg with Integer if it's a type variable
1554
+ if arg_type.is_a?(TypeVar)
1555
+ @unifier.unify(arg_type, Types::INTEGER)
1556
+ end
1557
+ return Types::INTEGER
1558
+ elsif receiver_type == Types::FLOAT
1559
+ # Unify arg with Float if it's a type variable
1560
+ if arg_type.is_a?(TypeVar)
1561
+ @unifier.unify(arg_type, Types::FLOAT)
1562
+ end
1563
+ return Types::FLOAT
1564
+ elsif receiver_type == Types::STRING && method_name == :+
1565
+ # String#+ requires String argument
1566
+ if arg_type.is_a?(TypeVar)
1567
+ @unifier.unify(arg_type, Types::STRING)
1568
+ end
1569
+ return Types::STRING
1570
+ end
1571
+ when :==, :!=, :<, :>, :<=, :>=
1572
+ # If receiver is a type variable and arg is concrete, unify
1573
+ if receiver_type.is_a?(TypeVar) && arg_type
1574
+ if arg_type == Types::INTEGER
1575
+ @unifier.unify(receiver_type, Types::INTEGER)
1576
+ elsif arg_type == Types::FLOAT
1577
+ @unifier.unify(receiver_type, Types::FLOAT)
1578
+ elsif arg_type == Types::STRING
1579
+ @unifier.unify(receiver_type, Types::STRING)
1580
+ end
1581
+ end
1582
+ # If arg is a type variable and receiver is concrete, unify
1583
+ if arg_type && arg_type.is_a?(TypeVar)
1584
+ if receiver_type == Types::INTEGER
1585
+ @unifier.unify(arg_type, Types::INTEGER)
1586
+ elsif receiver_type == Types::FLOAT
1587
+ @unifier.unify(arg_type, Types::FLOAT)
1588
+ elsif receiver_type == Types::STRING
1589
+ @unifier.unify(arg_type, Types::STRING)
1590
+ end
1591
+ end
1592
+ return Types::BOOL
1593
+ when :!
1594
+ return Types::BOOL
1595
+ when :to_s, :inspect
1596
+ return Types::STRING
1597
+ when :to_i
1598
+ return Types::INTEGER
1599
+ when :to_f
1600
+ return Types::FLOAT
1601
+ when :size, :length, :count
1602
+ return Types::INTEGER
1603
+ # String methods
1604
+ when :split, :chars
1605
+ return Types.array(Types::STRING) if receiver_type == Types::STRING
1606
+ when :bytes
1607
+ return Types.array(Types::INTEGER) if receiver_type == Types::STRING
1608
+ when :strip, :lstrip, :rstrip, :upcase, :downcase, :capitalize,
1609
+ :gsub, :sub, :chomp, :chop, :squeeze, :reverse, :tr, :delete,
1610
+ :encode, :freeze, :dup, :clone
1611
+ return Types::STRING if receiver_type == Types::STRING
1612
+ when :include?, :start_with?, :end_with?, :match?, :empty?,
1613
+ :ascii_only?, :frozen?, :nil?, :is_a?, :kind_of?,
1614
+ :even?, :odd?, :zero?, :positive?, :negative?
1615
+ return Types::BOOL
1616
+ when :abs
1617
+ return Types::INTEGER if receiver_type == Types::INTEGER
1618
+ return Types::FLOAT if receiver_type == Types::FLOAT
1619
+ when :index, :rindex, :ord
1620
+ return Types::INTEGER if receiver_type == Types::STRING
1621
+ when :scan
1622
+ return Types.array(Types::STRING) if receiver_type == Types::STRING
1623
+ when :match
1624
+ return Types::MATCH_DATA if receiver_type == Types::STRING || receiver_type == Types::REGEXP
1625
+ when :[], :slice
1626
+ return Types::STRING if receiver_type == Types::STRING
1627
+ # Array element access returns the element type
1628
+ if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Array
1629
+ return receiver_type.type_args&.first || Types::UNTYPED
1630
+ end
1631
+ # Array methods
1632
+ when :first, :last, :sample, :min, :max, :pop, :shift
1633
+ # Return element type if known, otherwise untyped
1634
+ if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Array
1635
+ return receiver_type.type_args&.first || Types::UNTYPED
1636
+ end
1637
+ when :flatten, :compact, :uniq, :sort, :reverse, :rotate, :shuffle
1638
+ return receiver_type if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Array
1639
+ when :push, :<<, :append, :unshift, :prepend
1640
+ if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Array
1641
+ # Unify the array's element TypeVar with the pushed value's type
1642
+ elem_type = receiver_type.type_args&.first
1643
+ if elem_type && arg_type
1644
+ @unifier.unify(elem_type, arg_type)
1645
+ end
1646
+ return receiver_type
1647
+ end
1648
+ when :join
1649
+ return Types::STRING if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Array
1650
+ when :sum
1651
+ return Types::INTEGER if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Array
1652
+ # Hash methods
1653
+ when :keys
1654
+ if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Hash
1655
+ key_type = receiver_type.type_args&.first || Types::UNTYPED
1656
+ return Types.array(key_type)
1657
+ end
1658
+ when :values
1659
+ if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Hash
1660
+ val_type = receiver_type.type_args&.[](1) || Types::UNTYPED
1661
+ return Types.array(val_type)
1662
+ end
1663
+ when :has_key?, :key?, :has_value?, :value?
1664
+ return Types::BOOL if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Hash
1665
+ when :merge
1666
+ return receiver_type if receiver_type.is_a?(Types::ClassInstance) && receiver_type.name == :Hash
1667
+ # MatchData methods
1668
+ when :captures
1669
+ return Types.array(Types::STRING) if receiver_type == Types::MATCH_DATA
1670
+ when :pre_match, :post_match
1671
+ return Types::STRING if receiver_type == Types::MATCH_DATA
1672
+ end
1673
+
1674
+ nil
1675
+ end
1676
+
1677
+ # Method definition
1678
+ def infer_def(node)
1679
+ push_env
1680
+ old_method_name = @current_method_name
1681
+ @current_method_name = node.name.to_s
1682
+
1683
+ # Bind parameters with their type variables
1684
+ # Use class-qualified key first to avoid cross-class collision
1685
+ func_type = nil
1686
+ if @current_class_name
1687
+ func_type = @function_types[:"#{@current_class_name}##{node.name}"]
1688
+ end
1689
+ func_type ||= @function_types[node.name.to_sym]
1690
+
1691
+ if func_type && node.parameters
1692
+ requireds = node.parameters.requireds || []
1693
+ requireds.zip(func_type.param_types).each do |param, param_type|
1694
+ if param.respond_to?(:name)
1695
+ # Record the parameter type for HIR generation
1696
+ record_node_type(param, param_type)
1697
+ bind(param.name, param_type)
1698
+ end
1699
+ end
1700
+ end
1701
+
1702
+ # Rest parameter (*args) is Array — use FunctionType's rest_param_type if available
1703
+ # so call-site arg types flow through to the rest param element type
1704
+ if node.parameters&.rest.is_a?(Prism::RestParameterNode) && node.parameters.rest.name
1705
+ rest_element_type = func_type&.rest_param_type || TypeVar.new
1706
+ rest_type = Types.array(rest_element_type)
1707
+ record_node_type(node.parameters.rest, rest_type)
1708
+ bind(node.parameters.rest.name, rest_type)
1709
+ end
1710
+
1711
+ # Keyword parameters (required and optional)
1712
+ keywords = node.parameters&.keywords || []
1713
+ func_key = @current_class_name ? :"#{@current_class_name}##{node.name}" : node.name.to_sym
1714
+ kw_vars = @keyword_param_vars[func_key] || {}
1715
+ keywords.each do |kw|
1716
+ kw_type = kw_vars[kw.name] || TypeVar.new(kw.name.to_s)
1717
+ record_node_type(kw, kw_type)
1718
+ bind(kw.name, kw_type)
1719
+
1720
+ # For optional keyword params, infer default value type and unify
1721
+ if kw.respond_to?(:value) && kw.value
1722
+ default_type = infer(kw.value)
1723
+ begin
1724
+ @unifier.unify(kw_type, default_type)
1725
+ rescue UnificationError
1726
+ # Default value type conflicts — keep the TypeVar
1727
+ end
1728
+ end
1729
+ end
1730
+
1731
+ # Keyword rest parameter (**kwargs) is always Hash
1732
+ if node.parameters&.keyword_rest.is_a?(Prism::KeywordRestParameterNode) && node.parameters.keyword_rest.name
1733
+ kwrest_type = Types.hash_type(Types::SYMBOL, TypeVar.new)
1734
+ record_node_type(node.parameters.keyword_rest, kwrest_type)
1735
+ bind(node.parameters.keyword_rest.name, kwrest_type)
1736
+ end
1737
+
1738
+ # Infer body type
1739
+ body_type = if node.body
1740
+ infer(node.body)
1741
+ else
1742
+ Types::NIL
1743
+ end
1744
+
1745
+ # Unify with return type
1746
+ if func_type
1747
+ @unifier.unify(func_type.return_type, body_type)
1748
+ end
1749
+
1750
+ @current_method_name = old_method_name
1751
+ pop_env
1752
+ Types::SYMBOL
1753
+ end
1754
+
1755
+ # ==============================================
1756
+ # Flow-sensitive type narrowing
1757
+ # ==============================================
1758
+
1759
+ # Analyze a predicate expression to determine what type narrowing to apply
1760
+ # Returns a hash describing the narrowing, or nil if no narrowing applies
1761
+ def analyze_predicate(node)
1762
+ case node
1763
+ when Prism::LocalVariableReadNode
1764
+ # Simple truthiness check: `if x`
1765
+ { var_name: node.name.to_sym, narrowing: :truthy }
1766
+ when Prism::CallNode
1767
+ analyze_call_predicate(node)
1768
+ when Prism::AndNode
1769
+ # `if a && b` - both conditions must be true in then-branch
1770
+ left = analyze_predicate(node.left)
1771
+ right = analyze_predicate(node.right)
1772
+ { type: :and, left: left, right: right }
1773
+ when Prism::OrNode
1774
+ # `if a || b` - either condition may be true (conservative)
1775
+ left = analyze_predicate(node.left)
1776
+ right = analyze_predicate(node.right)
1777
+ { type: :or, left: left, right: right }
1778
+ else
1779
+ nil
1780
+ end
1781
+ end
1782
+
1783
+ # Analyze a method call predicate (e.g., `x == nil`, `x.nil?`)
1784
+ def analyze_call_predicate(node)
1785
+ # Handle `x == nil`, `x != nil`, `x.nil?`
1786
+ if node.receiver.is_a?(Prism::LocalVariableReadNode)
1787
+ var_name = node.receiver.name.to_sym
1788
+
1789
+ case node.name.to_sym
1790
+ when :==
1791
+ # `x == nil` -> then: x is nil, else: x is non-nil
1792
+ if nil_literal?(node.arguments&.arguments&.first)
1793
+ return { var_name: var_name, narrowing: :nil_check }
1794
+ end
1795
+ when :!=
1796
+ # `x != nil` -> then: x is non-nil, else: x is nil
1797
+ if nil_literal?(node.arguments&.arguments&.first)
1798
+ return { var_name: var_name, narrowing: :not_nil_check }
1799
+ end
1800
+ when :nil?
1801
+ # `x.nil?` -> then: x is nil, else: x is non-nil
1802
+ return { var_name: var_name, narrowing: :nil_check }
1803
+ end
1804
+ end
1805
+
1806
+ # Handle `nil == x`, `nil != x`
1807
+ if nil_literal?(node.receiver) && node.arguments&.arguments&.first.is_a?(Prism::LocalVariableReadNode)
1808
+ var_name = node.arguments.arguments.first.name.to_sym
1809
+ case node.name.to_sym
1810
+ when :==
1811
+ return { var_name: var_name, narrowing: :nil_check }
1812
+ when :!=
1813
+ return { var_name: var_name, narrowing: :not_nil_check }
1814
+ end
1815
+ end
1816
+
1817
+ nil
1818
+ end
1819
+
1820
+ # Check if a node is a nil literal
1821
+ def nil_literal?(node)
1822
+ node.is_a?(Prism::NilNode)
1823
+ end
1824
+
1825
+ # Apply narrowing for the then-branch
1826
+ # Returns an array of narrowing records for restoration
1827
+ def apply_then_narrowing(pred_info)
1828
+ return [] unless pred_info
1829
+
1830
+ case pred_info[:type]
1831
+ when :and
1832
+ # Both conditions are true: apply both narrowings
1833
+ left_narrowings = apply_then_narrowing(pred_info[:left])
1834
+ right_narrowings = apply_then_narrowing(pred_info[:right])
1835
+ left_narrowings + right_narrowings
1836
+ when :or
1837
+ # Either condition may be true: conservative, no narrowing
1838
+ []
1839
+ else
1840
+ apply_single_narrowing(pred_info, :then)
1841
+ end
1842
+ end
1843
+
1844
+ # Apply narrowing for the else-branch
1845
+ # Returns an array of narrowing records for restoration
1846
+ def apply_else_narrowing(pred_info)
1847
+ return [] unless pred_info
1848
+
1849
+ case pred_info[:type]
1850
+ when :and
1851
+ # At least one condition is false: conservative, no narrowing
1852
+ []
1853
+ when :or
1854
+ # Both conditions are false: apply opposite narrowings
1855
+ left_narrowings = apply_else_narrowing(pred_info[:left])
1856
+ right_narrowings = apply_else_narrowing(pred_info[:right])
1857
+ left_narrowings + right_narrowings
1858
+ else
1859
+ apply_single_narrowing(pred_info, :else)
1860
+ end
1861
+ end
1862
+
1863
+ # Apply a single narrowing based on the predicate info and branch
1864
+ def apply_single_narrowing(pred_info, branch)
1865
+ var_name = pred_info[:var_name]
1866
+ return [] unless var_name
1867
+
1868
+ scheme = lookup(var_name)
1869
+ return [] unless scheme
1870
+
1871
+ current_type = scheme.is_a?(TypeScheme) ? scheme.type : scheme
1872
+ return [] unless narrowable_type?(current_type)
1873
+
1874
+ narrowed_type = case [pred_info[:narrowing], branch]
1875
+ when [:truthy, :then], [:not_nil_check, :then], [:nil_check, :else]
1876
+ # Variable is non-nil
1877
+ remove_nil_from_type(current_type)
1878
+ when [:nil_check, :then], [:not_nil_check, :else], [:truthy, :else]
1879
+ # For truthy else: variable could be nil or false
1880
+ # For nil_check then: variable is definitely nil
1881
+ if pred_info[:narrowing] == :nil_check && branch == :then
1882
+ Types::NIL
1883
+ else
1884
+ # Conservative: don't narrow for truthy else (could be nil or false)
1885
+ return []
1886
+ end
1887
+ else
1888
+ return []
1889
+ end
1890
+
1891
+ return [] if narrowed_type == current_type
1892
+
1893
+ original = scheme
1894
+ bind(var_name, narrowed_type)
1895
+ [{ var_name: var_name, original: original }]
1896
+ end
1897
+
1898
+ # Check if a type can be narrowed (has nil in it)
1899
+ def narrowable_type?(type)
1900
+ type = type.type if type.is_a?(TypeScheme)
1901
+ return true if type.is_a?(Types::Union) && type.types.include?(Types::NIL)
1902
+ false
1903
+ end
1904
+
1905
+ # Remove nil from a union type
1906
+ def remove_nil_from_type(type)
1907
+ type = type.type if type.is_a?(TypeScheme)
1908
+ return type unless type.is_a?(Types::Union)
1909
+
1910
+ non_nil_types = type.types.reject { |t| t == Types::NIL }
1911
+ return type if non_nil_types.size == type.types.size # No nil to remove
1912
+
1913
+ if non_nil_types.size == 1
1914
+ non_nil_types.first
1915
+ else
1916
+ Types::Union.new(non_nil_types)
1917
+ end
1918
+ end
1919
+
1920
+ # Restore narrowed variables to their original types
1921
+ def restore_narrowings(narrowings)
1922
+ return unless narrowings
1923
+
1924
+ narrowings.each do |record|
1925
+ if record[:original].is_a?(TypeScheme)
1926
+ bind_scheme(record[:var_name], record[:original])
1927
+ else
1928
+ bind(record[:var_name], record[:original])
1929
+ end
1930
+ end
1931
+ end
1932
+
1933
+ # Control flow
1934
+ # @param statement_position [Boolean] if true, the if expression's result is not used,
1935
+ # so we don't require type consistency between branches
1936
+ def infer_if(node, statement_position: false)
1937
+ # Infer predicate type (for side effects like type errors)
1938
+ infer(node.predicate)
1939
+
1940
+ # Analyze predicate for type narrowing
1941
+ pred_info = analyze_predicate(node.predicate)
1942
+
1943
+ # Apply narrowing for then-branch
1944
+ then_narrowings = apply_then_narrowing(pred_info)
1945
+
1946
+ # In statement position, propagate to child statements
1947
+ then_type = if node.statements
1948
+ infer_statements_node(node.statements, all_statement_position: statement_position)
1949
+ else
1950
+ Types::NIL
1951
+ end
1952
+
1953
+ # Restore and apply else-branch narrowing
1954
+ restore_narrowings(then_narrowings)
1955
+ else_narrowings = apply_else_narrowing(pred_info)
1956
+
1957
+ else_type = if node.subsequent
1958
+ infer_subsequent(node.subsequent, statement_position: statement_position)
1959
+ else
1960
+ Types::NIL
1961
+ end
1962
+
1963
+ # Restore else narrowings
1964
+ restore_narrowings(else_narrowings)
1965
+
1966
+ if statement_position
1967
+ # In statement position, the result is not used,
1968
+ # so we don't require type consistency between branches.
1969
+ # Just return nil as the statement's "result".
1970
+ Types::NIL
1971
+ else
1972
+ # In expression position, both branches must have compatible types
1973
+ result_type = TypeVar.new
1974
+ @unifier.unify(result_type, then_type)
1975
+ @unifier.unify(result_type, else_type)
1976
+ @unifier.apply(result_type)
1977
+ end
1978
+ end
1979
+
1980
+ # unless is the opposite of if: swap then/else narrowing
1981
+ # @param statement_position [Boolean] if true, the unless expression's result is not used
1982
+ def infer_unless(node, statement_position: false)
1983
+ # Infer predicate type (for side effects like type errors)
1984
+ infer(node.predicate)
1985
+
1986
+ # Analyze predicate for type narrowing
1987
+ pred_info = analyze_predicate(node.predicate)
1988
+
1989
+ # For unless, the body executes when predicate is falsy
1990
+ # So apply else-branch narrowing for the body
1991
+ body_narrowings = apply_else_narrowing(pred_info)
1992
+
1993
+ # In statement position, propagate to child statements
1994
+ body_type = if node.statements
1995
+ infer_statements_node(node.statements, all_statement_position: statement_position)
1996
+ else
1997
+ Types::NIL
1998
+ end
1999
+
2000
+ # Restore and apply then-branch narrowing for else (consequent)
2001
+ restore_narrowings(body_narrowings)
2002
+ else_narrowings = apply_then_narrowing(pred_info)
2003
+
2004
+ else_type = if node.else_clause
2005
+ infer_else(node.else_clause, statement_position: statement_position)
2006
+ else
2007
+ Types::NIL
2008
+ end
2009
+
2010
+ # Restore else narrowings
2011
+ restore_narrowings(else_narrowings)
2012
+
2013
+ if statement_position
2014
+ # In statement position, the result is not used
2015
+ Types::NIL
2016
+ else
2017
+ # Both branches must have compatible types
2018
+ result_type = TypeVar.new
2019
+ @unifier.unify(result_type, body_type)
2020
+ @unifier.unify(result_type, else_type)
2021
+ @unifier.apply(result_type)
2022
+ end
2023
+ end
2024
+
2025
+ # Infer type for else/elsif clause, with statement_position propagation
2026
+ def infer_subsequent(node, statement_position: false)
2027
+ case node
2028
+ when Prism::ElseNode
2029
+ infer_else(node, statement_position: statement_position)
2030
+ when Prism::IfNode
2031
+ # elsif case - recursively process as if
2032
+ infer_if(node, statement_position: statement_position)
2033
+ else
2034
+ infer(node)
2035
+ end
2036
+ end
2037
+
2038
+ def infer_else(node, statement_position: false)
2039
+ if node.statements
2040
+ infer_statements_node(node.statements, all_statement_position: statement_position)
2041
+ else
2042
+ Types::NIL
2043
+ end
2044
+ end
2045
+
2046
+ def infer_and(node)
2047
+ left_type = infer(node.left)
2048
+ right_type = infer(node.right)
2049
+ # a && b: if a is falsy, result is a (nil/false); otherwise result is b
2050
+ # Return right_type as it's the most common use case
2051
+ right_type
2052
+ end
2053
+
2054
+ def infer_or(node)
2055
+ left_type = infer(node.left)
2056
+ right_type = infer(node.right)
2057
+ # a || b: if a is truthy, result is a; otherwise result is b
2058
+ # Return left_type as it's the most common use case
2059
+ left_type
2060
+ end
2061
+
2062
+ # Compound assignment operators
2063
+ def infer_local_variable_operator_write(node)
2064
+ existing = lookup(node.name)
2065
+ var_type = existing ? existing.instantiate : TypeVar.new
2066
+ value_type = infer(node.value)
2067
+ # The result type is the return type of the operator call
2068
+ # For simplicity, use var_type (e.g., Integer += Integer => Integer)
2069
+ var_type
2070
+ end
2071
+
2072
+ def infer_local_variable_or_write(node)
2073
+ existing = lookup(node.name)
2074
+ var_type = existing ? existing.instantiate : TypeVar.new
2075
+ value_type = infer(node.value)
2076
+ # x ||= val: result is either existing value or val
2077
+ var_type
2078
+ end
2079
+
2080
+ def infer_local_variable_and_write(node)
2081
+ existing = lookup(node.name)
2082
+ var_type = existing ? existing.instantiate : TypeVar.new
2083
+ value_type = infer(node.value)
2084
+ value_type
2085
+ end
2086
+
2087
+ def infer_instance_variable_write(node)
2088
+ value_type = infer(node.value)
2089
+ if @current_class_name
2090
+ @ivar_types[@current_class_name] ||= {}
2091
+ existing = @ivar_types[@current_class_name][node.name.to_s]
2092
+ if existing && existing != Types::UNTYPED
2093
+ if value_type == Types::NIL
2094
+ # Keep existing type when assigning nil — nilableを意味する
2095
+ elsif existing == Types::NIL || (existing.is_a?(TypeVar) && existing.prune.is_a?(TypeVar))
2096
+ # Existing is nil or unresolved TypeVar → replace with concrete type
2097
+ if existing.is_a?(TypeVar)
2098
+ # Unify TypeVar with concrete type so all references update
2099
+ @unifier.unify(existing, value_type)
2100
+ else
2101
+ @ivar_types[@current_class_name][node.name.to_s] = value_type
2102
+ end
2103
+ else
2104
+ # Existing is concrete type — unify (supports subtype-aware unification)
2105
+ begin
2106
+ @unifier.unify(existing, value_type)
2107
+ rescue UnificationError
2108
+ @inference_errors ||= []
2109
+ @inference_errors << "Instance variable #{node.name} in #{@current_class_name} has conflicting types: #{existing} vs #{value_type}"
2110
+ end
2111
+ end
2112
+ else
2113
+ # First assignment to this ivar
2114
+ if value_type == Types::NIL
2115
+ # Store TypeVar for nil — will be unified when concrete type is assigned
2116
+ @ivar_types[@current_class_name][node.name.to_s] = TypeVar.new
2117
+ else
2118
+ @ivar_types[@current_class_name][node.name.to_s] = value_type
2119
+ end
2120
+ end
2121
+ end
2122
+ value_type
2123
+ end
2124
+
2125
+ def infer_instance_variable_read(node)
2126
+ if @current_class_name
2127
+ # Check current class and parent classes for ivar types
2128
+ cls = @current_class_name
2129
+ while cls
2130
+ if @ivar_types[cls]
2131
+ type = @ivar_types[cls][node.name.to_s]
2132
+ return type if type && type != Types::UNTYPED
2133
+ end
2134
+ cls = @class_parents[cls]
2135
+ end
2136
+ end
2137
+ # RBS fallback (check current class and parent classes)
2138
+ if @rbs_loader && @current_class_name
2139
+ cls = @current_class_name
2140
+ while cls
2141
+ native_type = @rbs_loader.native_class_type(cls) rescue nil
2142
+ if native_type&.respond_to?(:fields)
2143
+ field_sym = node.name.to_s.sub(/^@/, "").to_sym
2144
+ ftype = native_type.fields[field_sym]
2145
+ return rbs_field_to_type(ftype) if ftype
2146
+ end
2147
+ cls = @class_parents[cls]
2148
+ end
2149
+ end
2150
+ # Return TypeVar so that later write can resolve this
2151
+ if @current_class_name
2152
+ @ivar_types[@current_class_name] ||= {}
2153
+ tv = TypeVar.new
2154
+ @ivar_types[@current_class_name][node.name.to_s] ||= tv
2155
+ @ivar_types[@current_class_name][node.name.to_s]
2156
+ else
2157
+ Types::UNTYPED
2158
+ end
2159
+ end
2160
+
2161
+ def infer_instance_variable_operator_write(node)
2162
+ value_type = infer(node.value)
2163
+ # Read existing ivar type, the operator result should have the same type
2164
+ existing = nil
2165
+ if @current_class_name && @ivar_types[@current_class_name]
2166
+ existing = @ivar_types[@current_class_name][node.name.to_s]
2167
+ end
2168
+ existing || value_type
2169
+ end
2170
+
2171
+ def infer_instance_variable_or_write(node)
2172
+ value_type = infer(node.value)
2173
+ if @current_class_name
2174
+ @ivar_types[@current_class_name] ||= {}
2175
+ existing = @ivar_types[@current_class_name][node.name.to_s]
2176
+ if existing && existing != Types::UNTYPED
2177
+ existing
2178
+ else
2179
+ @ivar_types[@current_class_name][node.name.to_s] = value_type
2180
+ value_type
2181
+ end
2182
+ else
2183
+ value_type
2184
+ end
2185
+ end
2186
+
2187
+ def infer_instance_variable_and_write(node)
2188
+ value_type = infer(node.value)
2189
+ value_type
2190
+ end
2191
+
2192
+ def infer_class_variable_write(node)
2193
+ value_type = infer(node.value)
2194
+ cls = @current_class_name || "__toplevel__"
2195
+ @cvar_types[cls] ||= {}
2196
+ existing = @cvar_types[cls][node.name.to_s]
2197
+ if existing && existing != Types::UNTYPED && existing != Types::NIL
2198
+ # Unify with existing type (ignore nil assignments)
2199
+ unless value_type == Types::NIL
2200
+ begin
2201
+ @unifier.unify(existing, value_type)
2202
+ rescue UnificationError
2203
+ # Keep existing type on conflict
2204
+ end
2205
+ end
2206
+ else
2207
+ @cvar_types[cls][node.name.to_s] = value_type == Types::NIL ? TypeVar.new : value_type
2208
+ end
2209
+ value_type
2210
+ end
2211
+
2212
+ def infer_class_variable_read(node)
2213
+ cls = @current_class_name || "__toplevel__"
2214
+ # Check current class
2215
+ if @cvar_types[cls]
2216
+ type = @cvar_types[cls][node.name.to_s]
2217
+ return type if type && type != Types::UNTYPED
2218
+ end
2219
+ # Check parent classes
2220
+ if @current_class_name
2221
+ parent = @class_parents[@current_class_name]
2222
+ while parent
2223
+ if @cvar_types[parent]
2224
+ type = @cvar_types[parent][node.name.to_s]
2225
+ return type if type && type != Types::UNTYPED
2226
+ end
2227
+ parent = @class_parents[parent]
2228
+ end
2229
+ end
2230
+ # Check __toplevel__ (class variables defined outside class body)
2231
+ if cls != "__toplevel__" && @cvar_types["__toplevel__"]
2232
+ type = @cvar_types["__toplevel__"][node.name.to_s]
2233
+ return type if type && type != Types::UNTYPED
2234
+ end
2235
+ TypeVar.new
2236
+ end
2237
+
2238
+ def infer_class_variable_operator_write(node)
2239
+ value_type = infer(node.value)
2240
+ cls = @current_class_name || "__toplevel__"
2241
+ @cvar_types[cls] ||= {}
2242
+ existing = @cvar_types[cls][node.name.to_s]
2243
+ existing || value_type
2244
+ end
2245
+
2246
+ def infer_class_variable_or_write(node)
2247
+ value_type = infer(node.value)
2248
+ cls = @current_class_name || "__toplevel__"
2249
+ @cvar_types[cls] ||= {}
2250
+ existing = @cvar_types[cls][node.name.to_s]
2251
+ if existing && existing != Types::UNTYPED
2252
+ existing
2253
+ else
2254
+ @cvar_types[cls][node.name.to_s] = value_type
2255
+ value_type
2256
+ end
2257
+ end
2258
+
2259
+ def infer_class_variable_and_write(node)
2260
+ value_type = infer(node.value)
2261
+ value_type
2262
+ end
2263
+
2264
+ def infer_until(node)
2265
+ infer(node.predicate)
2266
+ infer_statements_node(node.statements, all_statement_position: true) if node.statements
2267
+ Types::NIL
2268
+ end
2269
+
2270
+ def infer_break(node)
2271
+ if node.arguments&.arguments&.any?
2272
+ infer(node.arguments.arguments.first)
2273
+ end
2274
+ Types::BOTTOM
2275
+ end
2276
+
2277
+ def infer_next(node)
2278
+ if node.arguments&.arguments&.any?
2279
+ infer(node.arguments.arguments.first)
2280
+ end
2281
+ Types::BOTTOM
2282
+ end
2283
+
2284
+ def infer_case(node)
2285
+ # Infer the predicate (value being matched)
2286
+ infer(node.predicate) if node.predicate
2287
+
2288
+ # Collect types from all when branches
2289
+ branch_types = []
2290
+ node.conditions&.each do |when_clause|
2291
+ # Infer condition expressions (for side effects / type checking)
2292
+ when_clause.conditions&.each { |cond| infer(cond) }
2293
+ # Infer body type
2294
+ if when_clause.statements
2295
+ body_type = infer_statements_node(when_clause.statements)
2296
+ branch_types << body_type
2297
+ end
2298
+ end
2299
+
2300
+ # Infer else clause
2301
+ if node.else_clause&.statements
2302
+ else_type = infer_statements_node(node.else_clause.statements)
2303
+ branch_types << else_type
2304
+ end
2305
+
2306
+ return Types::NIL if branch_types.empty?
2307
+
2308
+ # Unify all branch types
2309
+ result_type = branch_types.first
2310
+ branch_types[1..].each do |bt|
2311
+ begin
2312
+ @unifier.unify(result_type, bt)
2313
+ rescue UnificationError
2314
+ # Different branch types — return the first one
2315
+ # (like if/else with different types)
2316
+ end
2317
+ end
2318
+ @unifier.apply(result_type)
2319
+ end
2320
+
2321
+ def infer_range(node)
2322
+ infer(node.left) if node.left
2323
+ infer(node.right) if node.right
2324
+ Types::RANGE
2325
+ end
2326
+
2327
+ def infer_global_variable_read(node)
2328
+ @global_var_types ||= {}
2329
+ existing = @global_var_types[node.name.to_s]
2330
+ if existing
2331
+ existing
2332
+ else
2333
+ # Create TypeVar placeholder — will be unified when the global var is written later
2334
+ tv = TypeVar.new(node.name.to_s)
2335
+ @global_var_types[node.name.to_s] = tv
2336
+ tv
2337
+ end
2338
+ end
2339
+
2340
+ def infer_global_variable_write(node)
2341
+ value_type = infer(node.value)
2342
+ @global_var_types ||= {}
2343
+ existing = @global_var_types[node.name.to_s]
2344
+ if existing
2345
+ # Unify with existing type (might be a forward-reference TypeVar)
2346
+ begin
2347
+ @unifier.unify(existing, value_type)
2348
+ rescue UnificationError
2349
+ # Conflicting types — overwrite with new value
2350
+ @global_var_types[node.name.to_s] = value_type
2351
+ end
2352
+ else
2353
+ @global_var_types[node.name.to_s] = value_type
2354
+ end
2355
+ value_type
2356
+ end
2357
+
2358
+ def infer_multi_write(node)
2359
+ value_type = infer(node.value) if node.value
2360
+ # Multi-write returns the RHS value
2361
+ value_type || Types::NIL
2362
+ end
2363
+
2364
+ def infer_super(node)
2365
+ arg_types = (node.arguments&.arguments || []).map { |arg| infer(arg) }
2366
+
2367
+ # Unify argument types with parent class's method parameter types
2368
+ if @current_class_name && @current_method_name
2369
+ parent_class = @class_parents[@current_class_name]
2370
+ if parent_class
2371
+ parent_key = :"#{parent_class}##{@current_method_name}"
2372
+ parent_func = @function_types[parent_key]
2373
+ if parent_func
2374
+ unify_call_args(parent_func, arg_types) if parent_func.is_a?(FunctionType)
2375
+ resolved = @unifier.prune(parent_func.return_type)
2376
+ return resolved unless resolved.is_a?(TypeVar)
2377
+ end
2378
+ end
2379
+ end
2380
+
2381
+ Types::UNTYPED
2382
+ end
2383
+
2384
+ def infer_forwarding_super(_node)
2385
+ # Bare `super` forwards all parameters from the current method to the parent
2386
+ if @current_class_name && @current_method_name
2387
+ parent_class = @class_parents[@current_class_name]
2388
+ if parent_class
2389
+ current_key = :"#{@current_class_name}##{@current_method_name}"
2390
+ current_func = @function_types[current_key]
2391
+ parent_key = :"#{parent_class}##{@current_method_name}"
2392
+ parent_func = @function_types[parent_key]
2393
+ if current_func && parent_func
2394
+ parent_func.param_types.zip(current_func.param_types).each do |parent_t, current_t|
2395
+ @unifier.unify(parent_t, current_t) if parent_t && current_t
2396
+ end
2397
+ resolved = @unifier.prune(parent_func.return_type)
2398
+ return resolved unless resolved.is_a?(TypeVar)
2399
+ end
2400
+ end
2401
+ end
2402
+
2403
+ Types::UNTYPED
2404
+ end
2405
+
2406
+ def infer_while(node)
2407
+ infer(node.predicate)
2408
+ # While loop body's result is never used (while returns nil),
2409
+ # so treat all statements as statement_position
2410
+ infer_statements_node(node.statements, all_statement_position: true) if node.statements
2411
+ Types::NIL
2412
+ end
2413
+
2414
+ def infer_return(node)
2415
+ if node.arguments&.arguments&.any?
2416
+ infer(node.arguments.arguments.first)
2417
+ else
2418
+ Types::NIL
2419
+ end
2420
+ end
2421
+
2422
+ # Handler for ProgramNode (called via infer dispatcher)
2423
+ def infer_program(node)
2424
+ infer_statements_node(node.statements)
2425
+ end
2426
+
2427
+ def infer_statements(node)
2428
+ infer_statements_node(node)
2429
+ end
2430
+
2431
+ # Infer types for a statements node
2432
+ # @param node [Prism::StatementsNode] the statements node
2433
+ # @param all_statement_position [Boolean] if true, treat ALL statements
2434
+ # (including the last) as statement position. Use this when the
2435
+ # parent construct doesn't use the result (e.g., if in statement position)
2436
+ def infer_statements_node(node, all_statement_position: false)
2437
+ return Types::NIL unless node&.body&.any?
2438
+
2439
+ statements = node.body
2440
+
2441
+ if all_statement_position
2442
+ # All statements are in statement position (result not used)
2443
+ statements.each do |stmt|
2444
+ infer(stmt, statement_position: true)
2445
+ end
2446
+ Types::NIL
2447
+ else
2448
+ # All statements except the last are in "statement position"
2449
+ # (their result is not used), so they don't need type consistency
2450
+ # for if/unless branches.
2451
+ statements[0..-2].each do |stmt|
2452
+ infer(stmt, statement_position: true)
2453
+ end
2454
+
2455
+ # The last statement's type is the result of the block
2456
+ infer(statements.last)
2457
+ end
2458
+ end
2459
+
2460
+ def infer_class(node)
2461
+ old_class = @current_class_name
2462
+ @current_class_name = (node.constant_path.name.to_s rescue nil)
2463
+ # Track parent class for super type propagation
2464
+ if @current_class_name && node.superclass
2465
+ parent_name = node.superclass.name.to_s rescue nil
2466
+ @class_parents[@current_class_name] = parent_name if parent_name
2467
+ end
2468
+ push_env
2469
+ infer(node.body) if node.body
2470
+ pop_env
2471
+ @current_class_name = old_class
2472
+ Types::NIL
2473
+ end
2474
+
2475
+ def infer_singleton_class(node)
2476
+ push_env
2477
+ infer(node.body) if node.body
2478
+ pop_env
2479
+ Types::NIL
2480
+ end
2481
+
2482
+ def infer_self(node)
2483
+ if @current_class_name
2484
+ type = Types::ClassInstance.new(@current_class_name.to_sym)
2485
+ else
2486
+ type = Types::UNTYPED
2487
+ end
2488
+ record_node_type(node, type)
2489
+ type
2490
+ end
2491
+
2492
+ def infer_parentheses(node)
2493
+ node.body ? infer(node.body) : Types::NIL
2494
+ end
2495
+
2496
+ def rbs_field_to_type(ftype)
2497
+ case ftype.to_s.to_sym
2498
+ when :Int64, :Integer then Types::INTEGER
2499
+ when :Float64, :Float then Types::FLOAT
2500
+ when :Bool then Types::BOOL
2501
+ when :String then Types::STRING
2502
+ else Types::UNTYPED
2503
+ end
2504
+ end
2505
+
2506
+ def resolve_ivar_types!
2507
+ @ivar_types.each do |class_name, ivars|
2508
+ ivars.each do |ivar_name, type|
2509
+ next unless type
2510
+ resolved = @unifier.apply(type)
2511
+ @ivar_types[class_name][ivar_name] = resolved
2512
+ end
2513
+ end
2514
+ end
2515
+
2516
+ # Kotlin-style validation: ensure all types are resolved after inference.
2517
+ # Any surviving TypeVar indicates incomplete type resolution.
2518
+ def validate_all_types_resolved!
2519
+ @unresolved_type_warnings = []
2520
+
2521
+ # Check function param/return types
2522
+ @function_types.each do |func_name, func_type|
2523
+ next unless func_type.is_a?(FunctionType)
2524
+
2525
+ func_type.param_types.each_with_index do |pt, i|
2526
+ resolved = @unifier.apply(pt)
2527
+ if unresolved_typevar?(resolved)
2528
+ @unresolved_type_warnings << {
2529
+ kind: :param,
2530
+ function: func_name.to_s,
2531
+ index: i,
2532
+ typevar: resolved.to_s
2533
+ }
2534
+ end
2535
+ end
2536
+
2537
+ ret = @unifier.apply(func_type.return_type)
2538
+ if unresolved_typevar?(ret)
2539
+ @unresolved_type_warnings << {
2540
+ kind: :return,
2541
+ function: func_name.to_s,
2542
+ typevar: ret.to_s
2543
+ }
2544
+ end
2545
+ end
2546
+ end
2547
+
2548
+ # Check if a type contains an unresolved TypeVar
2549
+ def unresolved_typevar?(type)
2550
+ case type
2551
+ when TypeVar
2552
+ pruned = type.prune
2553
+ pruned.is_a?(TypeVar) && !pruned.instance
2554
+ when FunctionType
2555
+ type.param_types.any? { |t| unresolved_typevar?(t) } ||
2556
+ unresolved_typevar?(type.return_type)
2557
+ when Types::ClassInstance
2558
+ type.type_args.any? { |t| unresolved_typevar?(t) }
2559
+ else
2560
+ false
2561
+ end
2562
+ end
2563
+ end
2564
+ end
2565
+ end