rigortype 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +373 -0
- data/README.md +152 -0
- data/exe/rigor +9 -0
- data/lib/rigor/analysis/check_rules.rb +503 -0
- data/lib/rigor/analysis/diagnostic.rb +35 -0
- data/lib/rigor/analysis/fact_store.rb +133 -0
- data/lib/rigor/analysis/result.rb +29 -0
- data/lib/rigor/analysis/runner.rb +119 -0
- data/lib/rigor/ast/type_node.rb +41 -0
- data/lib/rigor/ast.rb +22 -0
- data/lib/rigor/cli/type_of_command.rb +160 -0
- data/lib/rigor/cli/type_of_renderer.rb +88 -0
- data/lib/rigor/cli/type_scan_command.rb +160 -0
- data/lib/rigor/cli/type_scan_renderer.rb +165 -0
- data/lib/rigor/cli/type_scan_report.rb +32 -0
- data/lib/rigor/cli.rb +195 -0
- data/lib/rigor/configuration.rb +49 -0
- data/lib/rigor/environment/class_registry.rb +141 -0
- data/lib/rigor/environment/rbs_hierarchy.rb +64 -0
- data/lib/rigor/environment/rbs_loader.rb +244 -0
- data/lib/rigor/environment.rb +177 -0
- data/lib/rigor/inference/acceptance.rb +444 -0
- data/lib/rigor/inference/block_parameter_binder.rb +198 -0
- data/lib/rigor/inference/closure_escape_analyzer.rb +191 -0
- data/lib/rigor/inference/coverage_scanner.rb +85 -0
- data/lib/rigor/inference/expression_typer.rb +831 -0
- data/lib/rigor/inference/fallback.rb +35 -0
- data/lib/rigor/inference/fallback_tracer.rb +64 -0
- data/lib/rigor/inference/method_dispatcher/constant_folding.rb +102 -0
- data/lib/rigor/inference/method_dispatcher/overload_selector.rb +169 -0
- data/lib/rigor/inference/method_dispatcher/rbs_dispatch.rb +421 -0
- data/lib/rigor/inference/method_dispatcher/shape_dispatch.rb +336 -0
- data/lib/rigor/inference/method_dispatcher.rb +213 -0
- data/lib/rigor/inference/method_parameter_binder.rb +257 -0
- data/lib/rigor/inference/multi_target_binder.rb +143 -0
- data/lib/rigor/inference/narrowing.rb +1008 -0
- data/lib/rigor/inference/rbs_type_translator.rb +219 -0
- data/lib/rigor/inference/scope_indexer.rb +468 -0
- data/lib/rigor/inference/statement_evaluator.rb +1017 -0
- data/lib/rigor/rbs_extended.rb +98 -0
- data/lib/rigor/scope.rb +340 -0
- data/lib/rigor/source/node_locator.rb +104 -0
- data/lib/rigor/source/node_walker.rb +37 -0
- data/lib/rigor/source.rb +15 -0
- data/lib/rigor/testing.rb +65 -0
- data/lib/rigor/trinary.rb +108 -0
- data/lib/rigor/type/accepts_result.rb +109 -0
- data/lib/rigor/type/bot.rb +57 -0
- data/lib/rigor/type/combinator.rb +148 -0
- data/lib/rigor/type/constant.rb +90 -0
- data/lib/rigor/type/dynamic.rb +60 -0
- data/lib/rigor/type/hash_shape.rb +246 -0
- data/lib/rigor/type/nominal.rb +83 -0
- data/lib/rigor/type/singleton.rb +65 -0
- data/lib/rigor/type/top.rb +56 -0
- data/lib/rigor/type/tuple.rb +84 -0
- data/lib/rigor/type/union.rb +65 -0
- data/lib/rigor/type.rb +23 -0
- data/lib/rigor/version.rb +5 -0
- data/lib/rigor.rb +29 -0
- data/sig/rigor/analysis/fact_store.rbs +51 -0
- data/sig/rigor/ast.rbs +11 -0
- data/sig/rigor/environment.rbs +59 -0
- data/sig/rigor/inference.rbs +151 -0
- data/sig/rigor/rbs_extended.rbs +22 -0
- data/sig/rigor/scope.rbs +49 -0
- data/sig/rigor/source.rbs +20 -0
- data/sig/rigor/testing.rbs +9 -0
- data/sig/rigor/trinary.rbs +29 -0
- data/sig/rigor/type.rbs +171 -0
- data/sig/rigor.rbs +70 -0
- metadata +260 -0
|
@@ -0,0 +1,831 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "prism"
|
|
4
|
+
|
|
5
|
+
require_relative "../type"
|
|
6
|
+
require_relative "../ast"
|
|
7
|
+
require_relative "block_parameter_binder"
|
|
8
|
+
require_relative "fallback"
|
|
9
|
+
require_relative "method_dispatcher"
|
|
10
|
+
|
|
11
|
+
module Rigor
|
|
12
|
+
module Inference
|
|
13
|
+
# Translates AST nodes into Rigor::Type values, consulting the surrounding
|
|
14
|
+
# Rigor::Scope for local-variable bindings and the environment registry
|
|
15
|
+
# for nominal-type resolution. Pure: never mutates the receiver scope.
|
|
16
|
+
#
|
|
17
|
+
# Accepts both real Prism nodes and synthetic Rigor::AST::Node
|
|
18
|
+
# instances; the synthetic family lets callers and plugins ask
|
|
19
|
+
# "what would the analyzer infer if a value of type T appeared here?"
|
|
20
|
+
# without building a real Prism expression.
|
|
21
|
+
#
|
|
22
|
+
# Slice 1 recognises literal expressions, local-variable reads/writes,
|
|
23
|
+
# shallow Array literals, and Rigor::AST::TypeNode. Slice 2 adds
|
|
24
|
+
# Prism::CallNode (routed through Rigor::Inference::MethodDispatcher),
|
|
25
|
+
# Prism::ArgumentsNode (a non-value position whose children are typed
|
|
26
|
+
# individually by the CallNode handler), constant references resolved
|
|
27
|
+
# through Rigor::Environment::ClassRegistry, hash and interpolated
|
|
28
|
+
# string/symbol literals, definition expressions (def/class/module),
|
|
29
|
+
# and explicit handlers for parameter, block, splat, instance/class/
|
|
30
|
+
# global-variable, and self positions. Many of those handlers return
|
|
31
|
+
# Dynamic[Top] silently because they are non-value or out-of-scope
|
|
32
|
+
# positions for Slice 2; later slices refine them in place.
|
|
33
|
+
#
|
|
34
|
+
# Slice 4 phase 2b types bare-constant references (`Foo`, `Foo::Bar`)
|
|
35
|
+
# as `Singleton[Foo]` rather than `Nominal[Foo]`, so that method
|
|
36
|
+
# dispatch on the constant correctly looks up *class* methods. The
|
|
37
|
+
# corresponding instance type is reachable through `Foo.new` and the
|
|
38
|
+
# value-lattice projections.
|
|
39
|
+
#
|
|
40
|
+
# Every other node falls back to Dynamic[Top] per the fail-soft
|
|
41
|
+
# policy in docs/internal-spec/inference-engine.md. The optional
|
|
42
|
+
# tracer is a Rigor::Inference::FallbackTracer (or any object
|
|
43
|
+
# answering #record_fallback) that receives a Fallback event for
|
|
44
|
+
# each fallback; the tracer MUST NOT change the return value of
|
|
45
|
+
# type_of.
|
|
46
|
+
# rubocop:disable Metrics/ClassLength
|
|
47
|
+
class ExpressionTyper
|
|
48
|
+
# Hash-based dispatch keeps `type_of` linear and lets future slices add
|
|
49
|
+
# node kinds without growing a single case statement past RuboCop's
|
|
50
|
+
# cyclomatic budget. Anonymous Prism subclasses are not expected.
|
|
51
|
+
PRISM_DISPATCH = {
|
|
52
|
+
# Literals
|
|
53
|
+
Prism::IntegerNode => :type_of_literal_value,
|
|
54
|
+
Prism::FloatNode => :type_of_literal_value,
|
|
55
|
+
Prism::SymbolNode => :symbol_type_for,
|
|
56
|
+
Prism::StringNode => :string_type_for,
|
|
57
|
+
Prism::TrueNode => :type_of_true,
|
|
58
|
+
Prism::FalseNode => :type_of_false,
|
|
59
|
+
Prism::NilNode => :type_of_nil,
|
|
60
|
+
# Locals
|
|
61
|
+
Prism::LocalVariableReadNode => :local_read,
|
|
62
|
+
Prism::LocalVariableWriteNode => :type_of_assignment_write,
|
|
63
|
+
# Containers and pass-throughs
|
|
64
|
+
Prism::ArrayNode => :array_type_for,
|
|
65
|
+
Prism::ParenthesesNode => :parentheses_type_for,
|
|
66
|
+
Prism::StatementsNode => :type_of_statements_node,
|
|
67
|
+
Prism::ProgramNode => :type_of_program,
|
|
68
|
+
# Calls
|
|
69
|
+
Prism::CallNode => :call_type_for,
|
|
70
|
+
Prism::ArgumentsNode => :type_of_non_value,
|
|
71
|
+
# Constants
|
|
72
|
+
Prism::ConstantReadNode => :type_of_constant_read,
|
|
73
|
+
Prism::ConstantPathNode => :type_of_constant_path,
|
|
74
|
+
Prism::ConstantWriteNode => :type_of_assignment_write,
|
|
75
|
+
Prism::ConstantPathWriteNode => :type_of_assignment_write,
|
|
76
|
+
Prism::ConstantOperatorWriteNode => :type_of_assignment_write,
|
|
77
|
+
Prism::ConstantOrWriteNode => :type_of_assignment_write,
|
|
78
|
+
Prism::ConstantAndWriteNode => :type_of_assignment_write,
|
|
79
|
+
Prism::ConstantPathOperatorWriteNode => :type_of_assignment_write,
|
|
80
|
+
Prism::ConstantPathOrWriteNode => :type_of_assignment_write,
|
|
81
|
+
Prism::ConstantPathAndWriteNode => :type_of_assignment_write,
|
|
82
|
+
# Self and instance/class/global variables
|
|
83
|
+
Prism::SelfNode => :type_of_self_node,
|
|
84
|
+
Prism::InstanceVariableReadNode => :type_of_instance_variable_read,
|
|
85
|
+
Prism::InstanceVariableWriteNode => :type_of_assignment_write,
|
|
86
|
+
Prism::InstanceVariableOperatorWriteNode => :type_of_assignment_write,
|
|
87
|
+
Prism::InstanceVariableOrWriteNode => :type_of_assignment_write,
|
|
88
|
+
Prism::InstanceVariableAndWriteNode => :type_of_assignment_write,
|
|
89
|
+
Prism::ClassVariableReadNode => :type_of_class_variable_read,
|
|
90
|
+
Prism::ClassVariableWriteNode => :type_of_assignment_write,
|
|
91
|
+
Prism::ClassVariableOperatorWriteNode => :type_of_assignment_write,
|
|
92
|
+
Prism::ClassVariableOrWriteNode => :type_of_assignment_write,
|
|
93
|
+
Prism::ClassVariableAndWriteNode => :type_of_assignment_write,
|
|
94
|
+
Prism::GlobalVariableReadNode => :type_of_global_variable_read,
|
|
95
|
+
Prism::GlobalVariableWriteNode => :type_of_assignment_write,
|
|
96
|
+
Prism::GlobalVariableOperatorWriteNode => :type_of_assignment_write,
|
|
97
|
+
Prism::GlobalVariableOrWriteNode => :type_of_assignment_write,
|
|
98
|
+
Prism::GlobalVariableAndWriteNode => :type_of_assignment_write,
|
|
99
|
+
# Compound writes that share the .value rvalue protocol
|
|
100
|
+
Prism::LocalVariableOperatorWriteNode => :type_of_assignment_write,
|
|
101
|
+
Prism::LocalVariableOrWriteNode => :type_of_assignment_write,
|
|
102
|
+
Prism::LocalVariableAndWriteNode => :type_of_assignment_write,
|
|
103
|
+
Prism::IndexOperatorWriteNode => :type_of_assignment_write,
|
|
104
|
+
Prism::IndexOrWriteNode => :type_of_assignment_write,
|
|
105
|
+
Prism::IndexAndWriteNode => :type_of_assignment_write,
|
|
106
|
+
Prism::MultiWriteNode => :type_of_assignment_write,
|
|
107
|
+
Prism::LocalVariableTargetNode => :type_of_non_value,
|
|
108
|
+
# Hashes and interpolation
|
|
109
|
+
Prism::HashNode => :type_of_hash,
|
|
110
|
+
Prism::KeywordHashNode => :type_of_hash,
|
|
111
|
+
Prism::AssocNode => :type_of_non_value,
|
|
112
|
+
Prism::AssocSplatNode => :type_of_non_value,
|
|
113
|
+
Prism::InterpolatedStringNode => :type_of_interpolated_string,
|
|
114
|
+
Prism::InterpolatedSymbolNode => :type_of_interpolated_symbol,
|
|
115
|
+
Prism::EmbeddedStatementsNode => :type_of_embedded_statements,
|
|
116
|
+
Prism::EmbeddedVariableNode => :type_of_dynamic_top,
|
|
117
|
+
# Definitions
|
|
118
|
+
Prism::DefNode => :type_of_def,
|
|
119
|
+
Prism::ClassNode => :type_of_class_or_module,
|
|
120
|
+
Prism::ModuleNode => :type_of_class_or_module,
|
|
121
|
+
Prism::SingletonClassNode => :type_of_class_or_module,
|
|
122
|
+
Prism::AliasMethodNode => :type_of_nil_value,
|
|
123
|
+
Prism::AliasGlobalVariableNode => :type_of_nil_value,
|
|
124
|
+
Prism::UndefNode => :type_of_nil_value,
|
|
125
|
+
Prism::ForwardingSuperNode => :type_of_dynamic_top,
|
|
126
|
+
Prism::BlockArgumentNode => :type_of_non_value,
|
|
127
|
+
# Parameters and blocks (non-value positions)
|
|
128
|
+
Prism::ParametersNode => :type_of_non_value,
|
|
129
|
+
Prism::RequiredParameterNode => :type_of_non_value,
|
|
130
|
+
Prism::OptionalParameterNode => :type_of_non_value,
|
|
131
|
+
Prism::RequiredKeywordParameterNode => :type_of_non_value,
|
|
132
|
+
Prism::OptionalKeywordParameterNode => :type_of_non_value,
|
|
133
|
+
Prism::KeywordRestParameterNode => :type_of_non_value,
|
|
134
|
+
Prism::RestParameterNode => :type_of_non_value,
|
|
135
|
+
Prism::BlockParameterNode => :type_of_non_value,
|
|
136
|
+
Prism::BlockParametersNode => :type_of_non_value,
|
|
137
|
+
Prism::ForwardingParameterNode => :type_of_non_value,
|
|
138
|
+
Prism::NoKeywordsParameterNode => :type_of_non_value,
|
|
139
|
+
Prism::ImplicitRestNode => :type_of_non_value,
|
|
140
|
+
Prism::BlockNode => :type_of_dynamic_top,
|
|
141
|
+
Prism::SplatNode => :type_of_non_value,
|
|
142
|
+
# Control flow (Slice 3 phase 1): branch types are unioned, jumps
|
|
143
|
+
# type as Bot, loops type as Constant[nil].
|
|
144
|
+
Prism::IfNode => :type_of_if,
|
|
145
|
+
Prism::UnlessNode => :type_of_unless,
|
|
146
|
+
Prism::ElseNode => :type_of_else,
|
|
147
|
+
Prism::AndNode => :type_of_and_or,
|
|
148
|
+
Prism::OrNode => :type_of_and_or,
|
|
149
|
+
Prism::CaseNode => :type_of_case,
|
|
150
|
+
Prism::CaseMatchNode => :type_of_case,
|
|
151
|
+
Prism::WhenNode => :type_of_when_or_in,
|
|
152
|
+
Prism::InNode => :type_of_when_or_in,
|
|
153
|
+
Prism::BeginNode => :type_of_begin,
|
|
154
|
+
Prism::RescueNode => :type_of_rescue,
|
|
155
|
+
Prism::RescueModifierNode => :type_of_rescue_modifier,
|
|
156
|
+
Prism::EnsureNode => :type_of_ensure,
|
|
157
|
+
Prism::ReturnNode => :type_of_jump,
|
|
158
|
+
Prism::BreakNode => :type_of_jump,
|
|
159
|
+
Prism::NextNode => :type_of_jump,
|
|
160
|
+
Prism::RetryNode => :type_of_jump,
|
|
161
|
+
Prism::RedoNode => :type_of_jump,
|
|
162
|
+
Prism::YieldNode => :type_of_dynamic_top,
|
|
163
|
+
Prism::SuperNode => :type_of_dynamic_top,
|
|
164
|
+
Prism::ForwardingArgumentsNode => :type_of_non_value,
|
|
165
|
+
Prism::WhileNode => :type_of_loop,
|
|
166
|
+
Prism::UntilNode => :type_of_loop,
|
|
167
|
+
Prism::ForNode => :type_of_dynamic_top,
|
|
168
|
+
Prism::DefinedNode => :type_of_dynamic_top,
|
|
169
|
+
Prism::MatchPredicateNode => :type_of_dynamic_top,
|
|
170
|
+
Prism::MatchRequiredNode => :type_of_dynamic_top,
|
|
171
|
+
Prism::MatchWriteNode => :type_of_dynamic_top,
|
|
172
|
+
# Literal containers
|
|
173
|
+
Prism::LambdaNode => :type_of_lambda,
|
|
174
|
+
Prism::RangeNode => :type_of_range,
|
|
175
|
+
Prism::RegularExpressionNode => :type_of_regexp,
|
|
176
|
+
Prism::InterpolatedRegularExpressionNode => :type_of_regexp
|
|
177
|
+
}.freeze
|
|
178
|
+
private_constant :PRISM_DISPATCH
|
|
179
|
+
|
|
180
|
+
def initialize(scope:, tracer: nil)
|
|
181
|
+
@scope = scope
|
|
182
|
+
@tracer = tracer
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def type_of(node)
|
|
186
|
+
# Slice A-declarations. ScopeIndexer pre-fills
|
|
187
|
+
# `scope.declared_types` for declaration-position nodes
|
|
188
|
+
# (`module Foo` / `class Bar` headers) with the qualified
|
|
189
|
+
# `Singleton` type so the header itself does not fall
|
|
190
|
+
# through to `Dynamic[Top]`. The override is consulted
|
|
191
|
+
# before any other dispatch and bypasses fail-soft
|
|
192
|
+
# tracing on a recognised match.
|
|
193
|
+
declared = scope.declared_types[node]
|
|
194
|
+
return declared if declared
|
|
195
|
+
|
|
196
|
+
return type_of_virtual(node) if node.is_a?(AST::Node)
|
|
197
|
+
|
|
198
|
+
handler = PRISM_DISPATCH[node.class]
|
|
199
|
+
return send(handler, node) if handler
|
|
200
|
+
|
|
201
|
+
fallback_for(node, family: :prism)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
private
|
|
205
|
+
|
|
206
|
+
attr_reader :scope, :tracer
|
|
207
|
+
|
|
208
|
+
def dynamic_top
|
|
209
|
+
Type::Combinator.untyped
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def type_of_literal_value(node)
|
|
213
|
+
Type::Combinator.constant_of(node.value)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
def type_of_true(_node)
|
|
217
|
+
Type::Combinator.constant_of(true)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def type_of_false(_node)
|
|
221
|
+
Type::Combinator.constant_of(false)
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def type_of_nil(_node)
|
|
225
|
+
Type::Combinator.constant_of(nil)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
# All `*WriteNode` flavours expose a `.value` rvalue child. Their type
|
|
229
|
+
# is the type of that rvalue. Binding the result back into the scope
|
|
230
|
+
# is the responsibility of the statement-level evaluator (Slice 3),
|
|
231
|
+
# never of `type_of` itself.
|
|
232
|
+
def type_of_assignment_write(node)
|
|
233
|
+
type_of(node.value)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Slice 7 phase 1 — instance/class/global variable reads.
|
|
237
|
+
# Each lookup returns the type currently bound in the
|
|
238
|
+
# surrounding scope's per-kind binding map (populated by
|
|
239
|
+
# `StatementEvaluator` write handlers within the same
|
|
240
|
+
# method body), falling through to `Dynamic[Top]` when no
|
|
241
|
+
# binding is recorded. Cross-method ivar/cvar inference is
|
|
242
|
+
# a follow-up slice; the read handlers MUST NOT raise on a
|
|
243
|
+
# missing binding and MUST NOT record a fallback event in
|
|
244
|
+
# either branch — the absence of a binding is a recognised
|
|
245
|
+
# semantic outcome, not a fail-soft compromise.
|
|
246
|
+
def type_of_instance_variable_read(node)
|
|
247
|
+
scope.ivar(node.name) || dynamic_top
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
def type_of_class_variable_read(node)
|
|
251
|
+
scope.cvar(node.name) || dynamic_top
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def type_of_global_variable_read(node)
|
|
255
|
+
scope.global(node.name) || dynamic_top
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def type_of_statements_node(node)
|
|
259
|
+
statements_type_for(node)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def type_of_program(node)
|
|
263
|
+
statements_type_for(node.statements)
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Recognised position that does not produce a value: parameter lists
|
|
267
|
+
# and individual parameter declarations, splats inside argument
|
|
268
|
+
# lists, key-value pairs in hashes, and the implicit-rest token
|
|
269
|
+
# inside destructuring. Returning Dynamic[Top] silently keeps these
|
|
270
|
+
# off the unrecognised list without faking a value type.
|
|
271
|
+
def type_of_non_value(_node)
|
|
272
|
+
dynamic_top
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# Recognised value-bearing position the Slice 2 engine does not yet
|
|
276
|
+
# narrow: self, instance/class/global variable reads, block bodies.
|
|
277
|
+
# Slice 3+ refines these in place; for now we acknowledge the node
|
|
278
|
+
# class so the coverage scanner stops flagging it without recording
|
|
279
|
+
# a fail-soft event for every occurrence.
|
|
280
|
+
# Slice A-engine. `Prism::SelfNode` resolves to the scope's
|
|
281
|
+
# `self_type` when one has been injected (by
|
|
282
|
+
# `StatementEvaluator` at class-body and method-body
|
|
283
|
+
# boundaries) or `Dynamic[Top]` at the top level. Class-body
|
|
284
|
+
# `self` is `Singleton[<class>]`; instance-method `self` is
|
|
285
|
+
# `Nominal[<class>]`; singleton-method `self` is
|
|
286
|
+
# `Singleton[<class>]`.
|
|
287
|
+
def type_of_self_node(_node)
|
|
288
|
+
scope.self_type || dynamic_top
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def type_of_dynamic_top(_node)
|
|
292
|
+
dynamic_top
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# The expression `Foo` evaluates to the *class object* `Foo`, not
|
|
296
|
+
# an instance. From Slice 4 phase 2b on we therefore type a
|
|
297
|
+
# bare-constant reference as `Singleton[Foo]`; method dispatch on
|
|
298
|
+
# that receiver looks up class methods (`Foo.new`, `Foo.bar`, ...).
|
|
299
|
+
#
|
|
300
|
+
# Slice A constant-walk: when the literal name does not resolve,
|
|
301
|
+
# we try a lexical walk based on the surrounding class context
|
|
302
|
+
# exposed through `scope.self_type` so a reference like
|
|
303
|
+
# `Inference::FallbackTracer` from inside `Rigor::CLI::Foo`
|
|
304
|
+
# resolves to `Rigor::Inference::FallbackTracer`.
|
|
305
|
+
def type_of_constant_read(node)
|
|
306
|
+
resolve_constant_name(node.name.to_s) || fallback_for(node, family: :prism)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def type_of_constant_path(node)
|
|
310
|
+
full_name = build_constant_path_name(node)
|
|
311
|
+
return fallback_for(node, family: :prism) if full_name.nil?
|
|
312
|
+
|
|
313
|
+
resolve_constant_name(full_name) || fallback_for(node, family: :prism)
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Try the literal name first, then walk Ruby's lexical lookup by
|
|
317
|
+
# progressively prefixing the surrounding class path (peeled
|
|
318
|
+
# one `::segment` at a time). For each candidate the lookup
|
|
319
|
+
# consults `Environment#singleton_for_name` (a class object)
|
|
320
|
+
# and then `Environment#constant_for_name` (a non-class
|
|
321
|
+
# constant value such as `BUCKETS: Array[Symbol]`).
|
|
322
|
+
# Returns the matched `Rigor::Type` or nil; the caller decides
|
|
323
|
+
# whether to fall back.
|
|
324
|
+
def resolve_constant_name(name)
|
|
325
|
+
env = scope.environment
|
|
326
|
+
discovered = scope.discovered_classes
|
|
327
|
+
in_source = scope.in_source_constants
|
|
328
|
+
lexical_constant_candidates(name).each do |candidate|
|
|
329
|
+
singleton = env.singleton_for_name(candidate)
|
|
330
|
+
return singleton if singleton
|
|
331
|
+
|
|
332
|
+
in_source_class = discovered[candidate]
|
|
333
|
+
return in_source_class if in_source_class
|
|
334
|
+
|
|
335
|
+
# In-source value-bearing constants take precedence
|
|
336
|
+
# over RBS constant decls because user code is the
|
|
337
|
+
# authoritative source for its own constants.
|
|
338
|
+
in_source_value = in_source[candidate]
|
|
339
|
+
return in_source_value if in_source_value
|
|
340
|
+
|
|
341
|
+
value = env.constant_for_name(candidate)
|
|
342
|
+
return value if value
|
|
343
|
+
end
|
|
344
|
+
nil
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# The candidate qualified names to try, in Ruby's lexical
|
|
348
|
+
# order: most-qualified first (the surrounding class path
|
|
349
|
+
# joined to `name`), then progressively less-qualified, then
|
|
350
|
+
# the bare `name`. Top-level scopes (no `self_type`) yield
|
|
351
|
+
# only `[name]`, preserving the pre-walk behaviour.
|
|
352
|
+
def lexical_constant_candidates(name)
|
|
353
|
+
prefix = enclosing_class_path
|
|
354
|
+
candidates = []
|
|
355
|
+
while prefix && !prefix.empty?
|
|
356
|
+
candidates << "#{prefix}::#{name}"
|
|
357
|
+
prefix = prefix.rpartition("::").first
|
|
358
|
+
prefix = nil if prefix.empty?
|
|
359
|
+
end
|
|
360
|
+
candidates << name
|
|
361
|
+
candidates
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# Pulls the enclosing qualified class name out of
|
|
365
|
+
# `scope.self_type` when one is set. `Nominal[T]` and
|
|
366
|
+
# `Singleton[T]` both expose `class_name`. Returns nil when
|
|
367
|
+
# no class context is available (top-level).
|
|
368
|
+
def enclosing_class_path
|
|
369
|
+
st = scope.self_type
|
|
370
|
+
case st
|
|
371
|
+
when Type::Nominal, Type::Singleton then st.class_name
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
# Builds the dotted-colon name for a `Foo`, `Foo::Bar`, or `::Foo`
|
|
376
|
+
# path. Returns nil when an inner segment is not itself a constant
|
|
377
|
+
# reference (for example `expr::Foo`), so the caller can fall back.
|
|
378
|
+
def build_constant_path_name(node)
|
|
379
|
+
case node
|
|
380
|
+
when Prism::ConstantReadNode
|
|
381
|
+
node.name.to_s
|
|
382
|
+
when Prism::ConstantPathNode
|
|
383
|
+
parent = node.parent
|
|
384
|
+
return node.name.to_s if parent.nil?
|
|
385
|
+
|
|
386
|
+
parent_name = build_constant_path_name(parent)
|
|
387
|
+
return nil if parent_name.nil?
|
|
388
|
+
|
|
389
|
+
"#{parent_name}::#{node.name}"
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Slice 5 phase 1 upgrades hash literals to `HashShape{...}`
|
|
394
|
+
# when every entry is a static `AssocNode` whose key is a
|
|
395
|
+
# `SymbolNode` or `StringNode` with a known value (covering the
|
|
396
|
+
# `{ a: 1, "b" => 2 }` pattern and falling back to the generic
|
|
397
|
+
# `Hash[K, V]` form otherwise). Splatted entries
|
|
398
|
+
# (`{ **other }`) and dynamic keys widen to the underlying
|
|
399
|
+
# `Hash[K, V]` form by unioning the types each entry exposes;
|
|
400
|
+
# when no concrete pair survives we fall back to the raw `Hash`
|
|
401
|
+
# so callers stay backward compatible.
|
|
402
|
+
def type_of_hash(node)
|
|
403
|
+
elements = node.respond_to?(:elements) ? node.elements : []
|
|
404
|
+
return Type::Combinator.nominal_of(Hash) if elements.empty?
|
|
405
|
+
|
|
406
|
+
shape = static_hash_shape_for(elements)
|
|
407
|
+
return shape if shape
|
|
408
|
+
|
|
409
|
+
keys, values = generic_hash_pairs_for(elements)
|
|
410
|
+
return Type::Combinator.nominal_of(Hash) if keys.empty? || values.empty?
|
|
411
|
+
|
|
412
|
+
Type::Combinator.nominal_of(
|
|
413
|
+
Hash,
|
|
414
|
+
type_args: [Type::Combinator.union(*keys), Type::Combinator.union(*values)]
|
|
415
|
+
)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Builds `HashShape{...}` when every entry is an `AssocNode`
|
|
419
|
+
# whose key is a static Symbol or String literal. Returns nil
|
|
420
|
+
# otherwise so the caller falls back to the generic shape.
|
|
421
|
+
def static_hash_shape_for(elements)
|
|
422
|
+
pairs = {}
|
|
423
|
+
elements.each do |entry|
|
|
424
|
+
return nil unless entry.is_a?(Prism::AssocNode)
|
|
425
|
+
|
|
426
|
+
key = static_hash_key(entry.key)
|
|
427
|
+
return nil if key.nil?
|
|
428
|
+
return nil if pairs.key?(key)
|
|
429
|
+
|
|
430
|
+
pairs[key] = type_of(entry.value)
|
|
431
|
+
end
|
|
432
|
+
return nil if pairs.empty?
|
|
433
|
+
|
|
434
|
+
Type::Combinator.hash_shape_of(pairs)
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Returns the static (Symbol|String) literal carried by a hash
|
|
438
|
+
# key node, or nil when the key is dynamic. We only treat
|
|
439
|
+
# SymbolNode#value and StringNode#unescaped as static when they
|
|
440
|
+
# are non-nil (interpolation produces a nil unescaped).
|
|
441
|
+
def static_hash_key(node)
|
|
442
|
+
case node
|
|
443
|
+
when Prism::SymbolNode
|
|
444
|
+
raw = node.value
|
|
445
|
+
raw&.to_sym
|
|
446
|
+
when Prism::StringNode
|
|
447
|
+
node.unescaped
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def generic_hash_pairs_for(elements)
|
|
452
|
+
keys = []
|
|
453
|
+
values = []
|
|
454
|
+
elements.each do |entry|
|
|
455
|
+
next unless entry.is_a?(Prism::AssocNode)
|
|
456
|
+
|
|
457
|
+
keys << type_of(entry.key)
|
|
458
|
+
values << type_of(entry.value)
|
|
459
|
+
end
|
|
460
|
+
[keys, values]
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
def type_of_interpolated_string(_node)
|
|
464
|
+
Type::Combinator.nominal_of(String)
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
def type_of_interpolated_symbol(_node)
|
|
468
|
+
Type::Combinator.nominal_of(Symbol)
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def type_of_embedded_statements(node)
|
|
472
|
+
statements_type_for(node.statements)
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def type_of_def(node)
|
|
476
|
+
Type::Combinator.constant_of(node.name)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
# `class Foo; body; end`, `module Foo; body; end`, and `class << x;
|
|
480
|
+
# body; end` evaluate to the value of the body's last expression,
|
|
481
|
+
# or `nil` when the body is empty. We do not track class/module
|
|
482
|
+
# scope yet, so the body is typed in the surrounding scope and
|
|
483
|
+
# that result is returned.
|
|
484
|
+
def type_of_class_or_module(node)
|
|
485
|
+
body = node.body
|
|
486
|
+
return Type::Combinator.constant_of(nil) if body.nil?
|
|
487
|
+
|
|
488
|
+
type_of(body)
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
# `alias x y`, `alias $x $y`, and `undef foo` all evaluate to nil at
|
|
492
|
+
# runtime; the constant carrier captures that exactly.
|
|
493
|
+
def type_of_nil_value(_node)
|
|
494
|
+
Type::Combinator.constant_of(nil)
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
# `if c; t; (elsif c2; ...; )* else; e; end`. Prism nests `elsif`
|
|
498
|
+
# branches as `IfNode#subsequent`. Slice 3 phase 1 types both
|
|
499
|
+
# branches in the receiver scope and returns their union; scope
|
|
500
|
+
# rebinding is the StatementEvaluator's job (Slice 3 phase 2).
|
|
501
|
+
# Without an else clause the branch's implicit value is nil, which
|
|
502
|
+
# is included in the union.
|
|
503
|
+
def type_of_if(node)
|
|
504
|
+
then_type = statements_or_nil(node.statements)
|
|
505
|
+
else_type =
|
|
506
|
+
if node.subsequent
|
|
507
|
+
type_of(node.subsequent)
|
|
508
|
+
else
|
|
509
|
+
Type::Combinator.constant_of(nil)
|
|
510
|
+
end
|
|
511
|
+
Type::Combinator.union(then_type, else_type)
|
|
512
|
+
end
|
|
513
|
+
|
|
514
|
+
# `unless c; t; else; e; end`. Prism uses `else_clause` here (no
|
|
515
|
+
# `elsif` chain).
|
|
516
|
+
def type_of_unless(node)
|
|
517
|
+
then_type = statements_or_nil(node.statements)
|
|
518
|
+
else_type =
|
|
519
|
+
if node.else_clause
|
|
520
|
+
type_of(node.else_clause)
|
|
521
|
+
else
|
|
522
|
+
Type::Combinator.constant_of(nil)
|
|
523
|
+
end
|
|
524
|
+
Type::Combinator.union(then_type, else_type)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def type_of_else(node)
|
|
528
|
+
statements_or_nil(node.statements)
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
# `a && b` and `a || b` short-circuit. Without a truthy/falsy
|
|
532
|
+
# narrowing model (Slice 6), the result of either side is reachable
|
|
533
|
+
# so the type is the union of the operand types.
|
|
534
|
+
def type_of_and_or(node)
|
|
535
|
+
Type::Combinator.union(type_of(node.left), type_of(node.right))
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def type_of_case(node)
|
|
539
|
+
branch_types = node.conditions.map { |branch| type_of(branch) }
|
|
540
|
+
else_type =
|
|
541
|
+
if node.else_clause
|
|
542
|
+
type_of(node.else_clause)
|
|
543
|
+
else
|
|
544
|
+
Type::Combinator.constant_of(nil)
|
|
545
|
+
end
|
|
546
|
+
Type::Combinator.union(*branch_types, else_type)
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# `when` clauses for `case` and `in` clauses for `case ... in` have
|
|
550
|
+
# the same body shape; we reuse one handler for both Prism node
|
|
551
|
+
# classes.
|
|
552
|
+
def type_of_when_or_in(node)
|
|
553
|
+
statements_or_nil(node.statements)
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# `begin; body; rescue R => e; r1; rescue; r2; else; e; ensure; f; end`.
|
|
557
|
+
# The result is the union of every value-producing branch: the body
|
|
558
|
+
# (or the else-clause when present, since it replaces the body's
|
|
559
|
+
# value when no exception fires), plus each rescue body in the
|
|
560
|
+
# rescue chain. The ensure clause runs but does not contribute to
|
|
561
|
+
# the begin's value.
|
|
562
|
+
def type_of_begin(node)
|
|
563
|
+
rescue_clause = node.rescue_clause
|
|
564
|
+
else_clause = node.else_clause
|
|
565
|
+
|
|
566
|
+
primary_type =
|
|
567
|
+
if else_clause
|
|
568
|
+
type_of(else_clause)
|
|
569
|
+
elsif node.statements
|
|
570
|
+
statements_or_nil(node.statements)
|
|
571
|
+
else
|
|
572
|
+
Type::Combinator.constant_of(nil)
|
|
573
|
+
end
|
|
574
|
+
|
|
575
|
+
rescue_types = rescue_chain_types(rescue_clause)
|
|
576
|
+
Type::Combinator.union(primary_type, *rescue_types)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def rescue_chain_types(rescue_node)
|
|
580
|
+
types = []
|
|
581
|
+
current = rescue_node
|
|
582
|
+
while current
|
|
583
|
+
types << statements_or_nil(current.statements)
|
|
584
|
+
current = current.subsequent
|
|
585
|
+
end
|
|
586
|
+
types
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def type_of_rescue(node)
|
|
590
|
+
statements_or_nil(node.statements)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
# `expr rescue fallback` is RescueModifierNode in Prism. The result
|
|
594
|
+
# is `expr`'s type when no exception is raised and `fallback`'s
|
|
595
|
+
# type otherwise; both paths are reachable, so the result is their
|
|
596
|
+
# union.
|
|
597
|
+
def type_of_rescue_modifier(node)
|
|
598
|
+
Type::Combinator.union(type_of(node.expression), type_of(node.rescue_expression))
|
|
599
|
+
end
|
|
600
|
+
|
|
601
|
+
def type_of_ensure(node)
|
|
602
|
+
statements_or_nil(node.statements)
|
|
603
|
+
end
|
|
604
|
+
|
|
605
|
+
# `return`, `break`, `next`, `retry`, and `redo` all transfer
|
|
606
|
+
# control instead of producing a value. Their type is Bot, the
|
|
607
|
+
# empty type that absorbs cleanly under union (e.g.
|
|
608
|
+
# `Constant[1] | Bot == Constant[1]`), so the surrounding
|
|
609
|
+
# control-flow handlers collapse correctly when one branch jumps.
|
|
610
|
+
def type_of_jump(_node)
|
|
611
|
+
Type::Combinator.bot
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
# `while` and `until` loops produce nil unless interrupted by
|
|
615
|
+
# `break VALUE`, which Slice 3 phase 1 does not yet model.
|
|
616
|
+
# Returning Constant[nil] is safe and matches Ruby semantics for
|
|
617
|
+
# the common case.
|
|
618
|
+
def type_of_loop(_node)
|
|
619
|
+
Type::Combinator.constant_of(nil)
|
|
620
|
+
end
|
|
621
|
+
|
|
622
|
+
def type_of_lambda(_node)
|
|
623
|
+
Type::Combinator.nominal_of(Proc)
|
|
624
|
+
end
|
|
625
|
+
|
|
626
|
+
def type_of_range(node)
|
|
627
|
+
left_static, left = static_range_endpoint(node.left)
|
|
628
|
+
right_static, right = static_range_endpoint(node.right)
|
|
629
|
+
return Type::Combinator.nominal_of(Range) unless left_static && right_static
|
|
630
|
+
|
|
631
|
+
Type::Combinator.constant_of(Range.new(left, right, node.exclude_end?))
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def type_of_regexp(_node)
|
|
635
|
+
Type::Combinator.nominal_of(Regexp)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def static_range_endpoint(node)
|
|
639
|
+
return [true, nil] if node.nil?
|
|
640
|
+
return [true, node.value] if node.is_a?(Prism::IntegerNode)
|
|
641
|
+
|
|
642
|
+
[false, nil]
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
# Helper for the many control-flow handlers that read a body
|
|
646
|
+
# `Prism::StatementsNode` or treat its absence as nil. Note that
|
|
647
|
+
# Prism uses nil (rather than an empty `StatementsNode`) for
|
|
648
|
+
# missing bodies in many node kinds.
|
|
649
|
+
def statements_or_nil(statements_node)
|
|
650
|
+
return Type::Combinator.constant_of(nil) if statements_node.nil?
|
|
651
|
+
|
|
652
|
+
statements_type_for(statements_node)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def type_of_virtual(node)
|
|
656
|
+
case node
|
|
657
|
+
when AST::TypeNode then node.type
|
|
658
|
+
else
|
|
659
|
+
fallback_for(node, family: :virtual)
|
|
660
|
+
end
|
|
661
|
+
end
|
|
662
|
+
|
|
663
|
+
def fallback_for(node, family:)
|
|
664
|
+
inner = dynamic_top
|
|
665
|
+
record_fallback(node, family: family, inner_type: inner)
|
|
666
|
+
inner
|
|
667
|
+
end
|
|
668
|
+
|
|
669
|
+
def record_fallback(node, family:, inner_type:)
|
|
670
|
+
return unless tracer
|
|
671
|
+
|
|
672
|
+
location = node.respond_to?(:location) ? node.location : nil
|
|
673
|
+
event = Fallback.new(
|
|
674
|
+
node_class: node.class,
|
|
675
|
+
location: location,
|
|
676
|
+
family: family,
|
|
677
|
+
inner_type: inner_type
|
|
678
|
+
)
|
|
679
|
+
tracer.record_fallback(event)
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def symbol_type_for(node)
|
|
683
|
+
raw = node.value
|
|
684
|
+
return Type::Combinator.nominal_of(Symbol) if raw.nil?
|
|
685
|
+
|
|
686
|
+
Type::Combinator.constant_of(raw.to_sym)
|
|
687
|
+
end
|
|
688
|
+
|
|
689
|
+
def string_type_for(node)
|
|
690
|
+
unescaped = node.unescaped
|
|
691
|
+
return Type::Combinator.nominal_of(String) if unescaped.nil?
|
|
692
|
+
|
|
693
|
+
Type::Combinator.constant_of(unescaped)
|
|
694
|
+
end
|
|
695
|
+
|
|
696
|
+
def local_read(node)
|
|
697
|
+
scope.local(node.name) || dynamic_top
|
|
698
|
+
end
|
|
699
|
+
|
|
700
|
+
# Slice 5 phase 1 upgrades array literals to `Tuple[T1..Tn]`
|
|
701
|
+
# when every element is a non-splat value. Splatted entries
|
|
702
|
+
# (`[*xs, 1]`) preserve the Slice 4 phase 2d behavior: we union
|
|
703
|
+
# the contributed element types and emit
|
|
704
|
+
# `Nominal[Array, [union]]`. An empty literal stays as the raw
|
|
705
|
+
# `Array` (no element evidence to lock either an arity or an
|
|
706
|
+
# element type).
|
|
707
|
+
def array_type_for(node)
|
|
708
|
+
elements = node.elements
|
|
709
|
+
return Type::Combinator.nominal_of(Array) if elements.empty?
|
|
710
|
+
|
|
711
|
+
if elements.any?(Prism::SplatNode)
|
|
712
|
+
element_types = elements.map { |e| type_of(e) }
|
|
713
|
+
element_union = Type::Combinator.union(*element_types)
|
|
714
|
+
return Type::Combinator.nominal_of(Array, type_args: [element_union])
|
|
715
|
+
end
|
|
716
|
+
|
|
717
|
+
Type::Combinator.tuple_of(*elements.map { |e| type_of(e) })
|
|
718
|
+
end
|
|
719
|
+
|
|
720
|
+
def parentheses_type_for(node)
|
|
721
|
+
body = node.body
|
|
722
|
+
return Type::Combinator.constant_of(nil) if body.nil?
|
|
723
|
+
|
|
724
|
+
type_of(body)
|
|
725
|
+
end
|
|
726
|
+
|
|
727
|
+
def statements_type_for(statements_node)
|
|
728
|
+
return Type::Combinator.constant_of(nil) if statements_node.nil?
|
|
729
|
+
|
|
730
|
+
body = statements_node.body
|
|
731
|
+
return Type::Combinator.constant_of(nil) if body.empty?
|
|
732
|
+
|
|
733
|
+
type_of(body.last)
|
|
734
|
+
end
|
|
735
|
+
|
|
736
|
+
# Slice 2 routes call expressions through `MethodDispatcher`. The
|
|
737
|
+
# receiver and every argument are typed first, then the dispatcher is
|
|
738
|
+
# asked for a result type. A nil result triggers the fail-soft fallback
|
|
739
|
+
# for the CallNode itself (the inner type_of calls already record
|
|
740
|
+
# their own fallbacks for unrecognised receivers/args, so the tracer
|
|
741
|
+
# captures both the immediate dispatch miss and the deeper cause).
|
|
742
|
+
def call_type_for(node)
|
|
743
|
+
receiver = call_receiver_type_for(node)
|
|
744
|
+
arg_types = call_arg_types(node)
|
|
745
|
+
block_type = block_return_type_for(node, receiver, arg_types)
|
|
746
|
+
|
|
747
|
+
result = MethodDispatcher.dispatch(
|
|
748
|
+
receiver_type: receiver,
|
|
749
|
+
method_name: node.name,
|
|
750
|
+
arg_types: arg_types,
|
|
751
|
+
block_type: block_type,
|
|
752
|
+
environment: scope.environment
|
|
753
|
+
)
|
|
754
|
+
return result if result
|
|
755
|
+
|
|
756
|
+
# Dynamic-origin propagation: when the receiver is Dynamic[T] and
|
|
757
|
+
# no positive rule resolves the call, the result inherits the
|
|
758
|
+
# dynamic origin. Per the value-lattice algebra, this is a
|
|
759
|
+
# recognised semantic outcome, not a fail-soft compromise, so it
|
|
760
|
+
# MUST NOT record a tracer event.
|
|
761
|
+
return dynamic_top if receiver.is_a?(Type::Dynamic)
|
|
762
|
+
|
|
763
|
+
fallback_for(node, family: :prism)
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
# Slice A-engine. Implicit-self calls (no `node.receiver`)
|
|
767
|
+
# adopt the surrounding scope's `self_type` as their receiver
|
|
768
|
+
# so calls like `attr_reader_method_name` or
|
|
769
|
+
# `private_helper(...)` inside an instance method dispatch
|
|
770
|
+
# against the enclosing class. Slice 7 phase 10 — when
|
|
771
|
+
# `self_type` is nil (top-level program), the receiver
|
|
772
|
+
# MUST default to `Nominal[Object]` so Kernel intrinsics
|
|
773
|
+
# like `require`, `require_relative`, `raise`, and `puts`
|
|
774
|
+
# dispatch through Object/Kernel rather than falling through
|
|
775
|
+
# to `Dynamic[Top]`.
|
|
776
|
+
def call_receiver_type_for(node)
|
|
777
|
+
return type_of(node.receiver) if node.receiver
|
|
778
|
+
|
|
779
|
+
scope.self_type || implicit_top_level_self
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def implicit_top_level_self
|
|
783
|
+
scope.environment.nominal_for_name("Object") || dynamic_top
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def call_arg_types(node)
|
|
787
|
+
arguments_node = node.arguments
|
|
788
|
+
return [] if arguments_node.nil?
|
|
789
|
+
|
|
790
|
+
arguments_node.arguments.map { |argument| type_of(argument) }
|
|
791
|
+
end
|
|
792
|
+
|
|
793
|
+
# When the call carries a `Prism::BlockNode`, build the block's
|
|
794
|
+
# entry scope (outer locals plus parameter bindings driven by
|
|
795
|
+
# the receiving method's RBS signature), type the block body
|
|
796
|
+
# under that scope, and return the body's value type. The
|
|
797
|
+
# result feeds `MethodDispatcher.dispatch`'s `block_type:` so
|
|
798
|
+
# generic methods like `Array#map[U] { (Elem) -> U } -> Array[U]`
|
|
799
|
+
# resolve `U` to the block's return type. Returns `nil` when
|
|
800
|
+
# the call has no block, when the receiver is unknown, or
|
|
801
|
+
# when typing the body raises (defensive against malformed
|
|
802
|
+
# subtrees); the dispatcher then runs in its no-block-aware
|
|
803
|
+
# path.
|
|
804
|
+
def block_return_type_for(call_node, receiver_type, arg_types)
|
|
805
|
+
block_node = call_node.block
|
|
806
|
+
return nil unless block_node.is_a?(Prism::BlockNode)
|
|
807
|
+
return nil if receiver_type.nil?
|
|
808
|
+
|
|
809
|
+
expected = MethodDispatcher.expected_block_param_types(
|
|
810
|
+
receiver_type: receiver_type,
|
|
811
|
+
method_name: call_node.name,
|
|
812
|
+
arg_types: arg_types,
|
|
813
|
+
environment: scope.environment
|
|
814
|
+
)
|
|
815
|
+
bindings = BlockParameterBinder.new(expected_param_types: expected).bind(block_node)
|
|
816
|
+
block_scope = bindings.reduce(scope) { |acc, (name, type)| acc.with_local(name, type) }
|
|
817
|
+
type_block_body(block_node, block_scope)
|
|
818
|
+
rescue StandardError
|
|
819
|
+
nil
|
|
820
|
+
end
|
|
821
|
+
|
|
822
|
+
def type_block_body(block_node, block_scope)
|
|
823
|
+
body = block_node.body
|
|
824
|
+
return Type::Combinator.constant_of(nil) if body.nil?
|
|
825
|
+
|
|
826
|
+
block_scope.type_of(body)
|
|
827
|
+
end
|
|
828
|
+
end
|
|
829
|
+
# rubocop:enable Metrics/ClassLength
|
|
830
|
+
end
|
|
831
|
+
end
|