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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +75 -0
- data/CONTRIBUTING.md +123 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/Rakefile +11 -0
- data/bin/konpeito +6 -0
- data/konpeito.gemspec +43 -0
- data/lib/konpeito/ast/typed_ast.rb +620 -0
- data/lib/konpeito/ast/visitor.rb +78 -0
- data/lib/konpeito/cache/cache_manager.rb +230 -0
- data/lib/konpeito/cache/dependency_graph.rb +192 -0
- data/lib/konpeito/cache.rb +8 -0
- data/lib/konpeito/cli/base_command.rb +187 -0
- data/lib/konpeito/cli/build_command.rb +220 -0
- data/lib/konpeito/cli/check_command.rb +104 -0
- data/lib/konpeito/cli/config.rb +231 -0
- data/lib/konpeito/cli/deps_command.rb +128 -0
- data/lib/konpeito/cli/doctor_command.rb +340 -0
- data/lib/konpeito/cli/fmt_command.rb +199 -0
- data/lib/konpeito/cli/init_command.rb +312 -0
- data/lib/konpeito/cli/lsp_command.rb +40 -0
- data/lib/konpeito/cli/run_command.rb +150 -0
- data/lib/konpeito/cli/test_command.rb +248 -0
- data/lib/konpeito/cli/watch_command.rb +212 -0
- data/lib/konpeito/cli.rb +301 -0
- data/lib/konpeito/codegen/builtin_methods.rb +229 -0
- data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
- data/lib/konpeito/codegen/debug_info.rb +352 -0
- data/lib/konpeito/codegen/inliner.rb +486 -0
- data/lib/konpeito/codegen/jvm_backend.rb +197 -0
- data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
- data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
- data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
- data/lib/konpeito/codegen/monomorphizer.rb +359 -0
- data/lib/konpeito/codegen/profile_runtime.c +341 -0
- data/lib/konpeito/codegen/profiler.rb +99 -0
- data/lib/konpeito/compiler.rb +592 -0
- data/lib/konpeito/dependency_resolver.rb +296 -0
- data/lib/konpeito/diagnostics/collector.rb +127 -0
- data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
- data/lib/konpeito/diagnostics/renderer.rb +144 -0
- data/lib/konpeito/formatter/formatter.rb +1214 -0
- data/lib/konpeito/hir/builder.rb +7167 -0
- data/lib/konpeito/hir/nodes.rb +2465 -0
- data/lib/konpeito/lsp/document_manager.rb +820 -0
- data/lib/konpeito/lsp/server.rb +183 -0
- data/lib/konpeito/lsp/transport.rb +38 -0
- data/lib/konpeito/parser/prism_adapter.rb +65 -0
- data/lib/konpeito/platform.rb +103 -0
- data/lib/konpeito/profile/report.rb +136 -0
- data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
- data/lib/konpeito/stdlib/compression/compression.rb +72 -0
- data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
- data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
- data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
- data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
- data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
- data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
- data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
- data/lib/konpeito/stdlib/http/extconf.rb +19 -0
- data/lib/konpeito/stdlib/http/http.rb +125 -0
- data/lib/konpeito/stdlib/http/http.rbs +57 -0
- data/lib/konpeito/stdlib/http/http_native.c +440 -0
- data/lib/konpeito/stdlib/json/extconf.rb +17 -0
- data/lib/konpeito/stdlib/json/json.rb +44 -0
- data/lib/konpeito/stdlib/json/json.rbs +33 -0
- data/lib/konpeito/stdlib/json/json_native.c +286 -0
- data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
- data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
- data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
- data/lib/konpeito/stdlib/ui/ui.rb +318 -0
- data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
- data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
- data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
- data/lib/konpeito/type_checker/inferrer.rb +565 -0
- data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
- data/lib/konpeito/type_checker/type_resolver.rb +276 -0
- data/lib/konpeito/type_checker/types.rb +1434 -0
- data/lib/konpeito/type_checker/unification.rb +323 -0
- data/lib/konpeito/ui/animation/animated_state.rb +80 -0
- data/lib/konpeito/ui/animation/easing.rb +59 -0
- data/lib/konpeito/ui/animation/value_tween.rb +66 -0
- data/lib/konpeito/ui/app.rb +379 -0
- data/lib/konpeito/ui/box.rb +38 -0
- data/lib/konpeito/ui/castella.rb +70 -0
- data/lib/konpeito/ui/castella_native.rb +76 -0
- data/lib/konpeito/ui/chart/area_chart.rb +305 -0
- data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
- data/lib/konpeito/ui/chart/base_chart.rb +210 -0
- data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
- data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
- data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
- data/lib/konpeito/ui/chart/line_chart.rb +289 -0
- data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
- data/lib/konpeito/ui/chart/scales.rb +77 -0
- data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
- data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
- data/lib/konpeito/ui/column.rb +271 -0
- data/lib/konpeito/ui/core.rb +2199 -0
- data/lib/konpeito/ui/dsl.rb +443 -0
- data/lib/konpeito/ui/frame.rb +171 -0
- data/lib/konpeito/ui/frame_native.rb +494 -0
- data/lib/konpeito/ui/markdown/ast.rb +124 -0
- data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
- data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
- data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
- data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
- data/lib/konpeito/ui/markdown/parser.rb +805 -0
- data/lib/konpeito/ui/markdown/renderer.rb +639 -0
- data/lib/konpeito/ui/markdown/theme.rb +165 -0
- data/lib/konpeito/ui/render_node.rb +260 -0
- data/lib/konpeito/ui/row.rb +207 -0
- data/lib/konpeito/ui/spacer.rb +18 -0
- data/lib/konpeito/ui/style.rb +799 -0
- data/lib/konpeito/ui/theme.rb +563 -0
- data/lib/konpeito/ui/themes/material.rb +35 -0
- data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
- data/lib/konpeito/ui/widgets/button.rb +103 -0
- data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
- data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
- data/lib/konpeito/ui/widgets/container.rb +91 -0
- data/lib/konpeito/ui/widgets/data_table.rb +667 -0
- data/lib/konpeito/ui/widgets/divider.rb +29 -0
- data/lib/konpeito/ui/widgets/image.rb +105 -0
- data/lib/konpeito/ui/widgets/input.rb +485 -0
- data/lib/konpeito/ui/widgets/markdown.rb +57 -0
- data/lib/konpeito/ui/widgets/modal.rb +163 -0
- data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
- data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
- data/lib/konpeito/ui/widgets/net_image.rb +100 -0
- data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
- data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
- data/lib/konpeito/ui/widgets/slider.rb +133 -0
- data/lib/konpeito/ui/widgets/switch.rb +84 -0
- data/lib/konpeito/ui/widgets/tabs.rb +157 -0
- data/lib/konpeito/ui/widgets/text.rb +110 -0
- data/lib/konpeito/ui/widgets/tree.rb +426 -0
- data/lib/konpeito/version.rb +5 -0
- data/lib/konpeito.rb +109 -0
- data/test_native_array.rb +172 -0
- data/test_native_array_class.rb +197 -0
- data/test_native_class.rb +151 -0
- data/tools/konpeito-asm/build.sh +65 -0
- data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
- data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
- data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
- data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
- 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
|