Class: Rigor::Inference::ExpressionTyper
- Inherits:
-
Object
- Object
- Rigor::Inference::ExpressionTyper
- Defined in:
- lib/rigor/inference/expression_typer.rb,
sig/rigor/inference.rbs
Overview
Translates AST nodes into Rigor::Type values, consulting the surrounding Rigor::Scope for local-variable bindings and the environment registry for nominal-type resolution. Pure: never mutates the receiver scope.
Accepts both real Prism nodes and synthetic Rigor::AST::Node instances; the synthetic family lets callers and plugins ask "what would the analyzer infer if a value of type T appeared here?" without building a real Prism expression.
Slice 1 recognises literal expressions, local-variable reads/writes, shallow Array literals, and Rigor::AST::TypeNode. Slice 2 adds Prism::CallNode (routed through Rigor::Inference::MethodDispatcher), Prism::ArgumentsNode (a non-value position whose children are typed individually by the CallNode handler), constant references resolved through Rigor::Environment::ClassRegistry, hash and interpolated string/symbol literals, definition expressions (def/class/module), and explicit handlers for parameter, block, splat, instance/class/global-variable, and self positions. Many of those handlers return Dynamic silently because they are non-value or out-of-scope positions for Slice 2; later slices refine them in place.
Slice 4 phase 2b types bare-constant references (Foo, Foo::Bar) as Singleton[Foo] rather than
Nominal[Foo], so that method dispatch on the constant correctly looks up class methods. The
corresponding instance type is reachable through Foo.new and the value-lattice projections.
Every other node falls back to Dynamic per the fail-soft policy in docs/internal-spec/inference-engine.md. The optional tracer is a Rigor::Inference::FallbackTracer (or any object answering #record_fallback) that receives a Fallback event for each fallback; the tracer MUST NOT change the return value of type_of. rubocop:disable Metrics/ClassLength
Instance Attribute Summary collapse
-
#scope ⇒ Scope
readonly
Returns the value of attribute scope.
-
#tracer ⇒ FallbackTracer?
readonly
Returns the value of attribute tracer.
Class Method Summary collapse
-
.harvest_return_memo ⇒ Hash[untyped, Array[untyped]]
ADR-89 WD2 — the current run's return memo bucket as
{ def_node => [MemoEntry, …] }(only entries that carry a call descriptor, i.e. every stored entry).
Instance Method Summary collapse
-
#block_return_type_for(call_node, receiver_type, arg_types) ⇒ Type::t?
When the call carries a
Prism::BlockNode, build the block's entry scope (outer locals plus parameter bindings driven by the receiving method's RBS signature), type the block body under that scope, and return the body's value type. - #call_arg_types(node) ⇒ Array[Type::t]
-
#call_receiver_type_for(node) ⇒ Type::t?
Slice A-engine.
- #dynamic_top ⇒ Type::Dynamic
- #fallback_for(node, family:) ⇒ Type::t
-
#initialize(scope:, tracer: nil) ⇒ ExpressionTyper
constructor
A new instance of ExpressionTyper.
- #record_fallback(node, family:, inner_type:, origin: nil) ⇒ void
-
#return_type_for(def_node, receiver, arg_types) ⇒ Type::t?
ADR-89 WD2 — the return type of
def_nodecalled withreceiver/arg_types, computed exactly as a resolved in-body dispatch does (through the ADR-84 memo, so it is cheap and yields FINAL values only). -
#statements_or_nil(statements_node) ⇒ Type::t
Helper for the many control-flow handlers that read a body
Prism::StatementsNodeor treat its absence as nil. - #type_of(node) ⇒ Type::t
- #type_of_virtual(node) ⇒ Type::t
- #untraced_type_of(node) ⇒ Object
Constructor Details
#initialize(scope:, tracer: nil) ⇒ ExpressionTyper
Returns a new instance of ExpressionTyper.
219 220 221 222 223 |
# File 'lib/rigor/inference/expression_typer.rb', line 219 def initialize(scope:, tracer: nil) @scope = scope @tracer = tracer @typing_node = nil end |
Instance Attribute Details
#scope ⇒ Scope (readonly)
Returns the value of attribute scope.
279 280 281 |
# File 'lib/rigor/inference/expression_typer.rb', line 279 def scope @scope end |
#tracer ⇒ FallbackTracer? (readonly)
Returns the value of attribute tracer.
279 280 281 |
# File 'lib/rigor/inference/expression_typer.rb', line 279 def tracer @tracer end |
Class Method Details
.harvest_return_memo ⇒ Hash[untyped, Array[untyped]]
ADR-89 WD2 — the current run's return memo bucket as { def_node => [MemoEntry, …] } (only entries
that carry a call descriptor, i.e. every stored entry). Read by the incremental session right after a
recording run to harvest each analyzed callee's observed call keys → return descriptors. Returns an
empty hash when no bucket exists (a run that memoised nothing). Class method: the bucket lives on
Thread.current, independent of any one ExpressionTyper instance.
270 271 272 273 274 275 |
# File 'lib/rigor/inference/expression_typer.rb', line 270 def self.harvest_return_memo slot = Thread.current[RETURN_MEMO_KEY] return {} if slot.nil? slot[1].transform_values(&:values) end |
Instance Method Details
#block_return_type_for(call_node, receiver_type, arg_types) ⇒ Type::t?
When the call carries a Prism::BlockNode, build the block's entry scope (outer locals plus
parameter bindings driven by the receiving method's RBS signature), type the block body under that
scope, and return the body's value type. The result feeds MethodDispatcher.dispatch's
block_type: so generic methods like Array#map[U] { (Elem) -> U } -> Array[U] resolve U to the
block's return type. Returns nil when the call has no block, when the receiver is unknown, or when
typing the body raises (defensive against malformed subtrees); the dispatcher then runs in its
no-block-aware path.
ADR-14 gap-#3 (d): a Prism::BlockArgumentNode carrying &:symbol (the Symbol#to_proc shorthand) is
treated as a block. The block's return type is computed by dispatching :symbol on the expected
block param type (per Symbol#to_proc's { |x| x.symbol } semantics). A precise inner dispatch
produces the right return; any failure step falls back to Dynamic[Top] so the dispatcher still
SEES a block — selecting the block-bearing overload of e.g. Hash#transform_values over the
no-block overload that returns Enumerator.
2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 |
# File 'lib/rigor/inference/expression_typer.rb', line 2406 def block_return_type_for(call_node, receiver_type, arg_types) block_arg = call_node.block return nil if block_arg.nil? return nil if receiver_type.nil? expected = MethodDispatcher.expected_block_param_types( receiver_type: receiver_type, method_name: call_node.name, arg_types: arg_types, environment: scope.environment ) # ADR-16 Tier A: when a registered plugin's `block_as_methods` entry matches `(receiver_type, # call_node.name)`, narrow the block body's `self_type` to the receiver class's instance type. The # narrowing is `nil` for unmatched calls, leaving the existing scope contract unchanged. narrowed_self = MacroBlockSelfType.narrow_self_type_for( scope: scope, call_node: call_node, receiver_type: receiver_type ) block_return_for(block_arg, expected, narrowed_self_type: narrowed_self) rescue StandardError nil end |
#call_arg_types(node) ⇒ Array[Type::t]
2385 2386 2387 2388 2389 2390 |
# File 'lib/rigor/inference/expression_typer.rb', line 2385 def call_arg_types(node) arguments_node = node.arguments return [] if arguments_node.nil? arguments_node.arguments.map { |argument| type_of(argument) } end |
#call_receiver_type_for(node) ⇒ Type::t?
Slice A-engine. Implicit-self calls (no node.receiver) adopt the surrounding scope's self_type
as their receiver so calls like attr_reader_method_name or private_helper(...) inside an
instance method dispatch against the enclosing class. Slice 7 phase 10 — when self_type is nil
(top-level program), the receiver MUST default to Nominal[Object] so Kernel intrinsics like
require, require_relative, raise, and puts dispatch through Object/Kernel rather than
falling through to Dynamic[Top].
2375 2376 2377 2378 2379 |
# File 'lib/rigor/inference/expression_typer.rb', line 2375 def call_receiver_type_for(node) return type_of(node.receiver) if node.receiver scope.self_type || implicit_top_level_self end |
#dynamic_top ⇒ Type::Dynamic
281 282 283 |
# File 'lib/rigor/inference/expression_typer.rb', line 281 def dynamic_top Type::Combinator.untyped end |
#fallback_for(node, family:) ⇒ Type::t
934 935 936 937 938 939 |
# File 'lib/rigor/inference/expression_typer.rb', line 934 def fallback_for(node, family:) inner = dynamic_top record_fallback(node, family: family, inner_type: inner, origin: DynamicOrigin::UNSUPPORTED_SYNTAX) scope.record_dynamic_origin(node, DynamicOrigin::UNSUPPORTED_SYNTAX) inner end |
#record_fallback(node, family:, inner_type:, origin: nil) ⇒ void
This method returns an undefined value.
941 942 943 944 945 946 947 948 949 950 951 952 953 |
# File 'lib/rigor/inference/expression_typer.rb', line 941 def record_fallback(node, family:, inner_type:, origin: nil) return unless tracer location = node.respond_to?(:location) ? node.location : nil event = Fallback.new( node_class: node.class, location: location, family: family, inner_type: inner_type, origin: origin ) tracer.record_fallback(event) end |
#return_type_for(def_node, receiver, arg_types) ⇒ Type::t?
ADR-89 WD2 — the return type of def_node called with receiver / arg_types, computed exactly as a
resolved in-body dispatch does (through the ADR-84 memo, so it is cheap and yields FINAL values only).
Public entry point for the incremental session's return-summary re-evaluation: it re-drives a
declaration-stable changed callee at each previously-observed call key to prove its return is
unchanged before skipping the callee's symbol dependents. nil for an abstract / bodyless def, or when
the memo refuses a transient result — the caller treats either as "not provably stable" (keeps the
dependents), the conservative direction.
261 262 263 |
# File 'lib/rigor/inference/expression_typer.rb', line 261 def return_type_for(def_node, receiver, arg_types) infer_user_method_return(def_node, receiver, arg_types) end |
#statements_or_nil(statements_node) ⇒ Type::t
Helper for the many control-flow handlers that read a body Prism::StatementsNode or treat its
absence as nil. Note that Prism uses nil (rather than an empty StatementsNode) for missing bodies
in many node kinds.
920 921 922 923 924 |
# File 'lib/rigor/inference/expression_typer.rb', line 920 def statements_or_nil(statements_node) return Type::Combinator.constant_of(nil) if statements_node.nil? statements_type_for(statements_node) end |
#type_of(node) ⇒ Type::t
225 226 227 228 229 230 231 232 233 234 235 236 |
# File 'lib/rigor/inference/expression_typer.rb', line 225 def type_of(node) previous = @typing_node @typing_node = node result = if FlowTracer.active? FlowTracer.trace_node(node) { untraced_type_of(node) } else untraced_type_of(node) end result ensure @typing_node = previous end |
#type_of_virtual(node) ⇒ Type::t
926 927 928 929 930 931 932 |
# File 'lib/rigor/inference/expression_typer.rb', line 926 def type_of_virtual(node) case node when AST::TypeNode then node.type else fallback_for(node, family: :virtual) end end |
#untraced_type_of(node) ⇒ Object
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/rigor/inference/expression_typer.rb', line 238 def untraced_type_of(node) # Slice A-declarations. ScopeIndexer pre-fills `scope.declared_types` for declaration-position nodes # (`module Foo` / `class Bar` headers) with the qualified `Singleton` type so the header itself does # not fall through to `Dynamic[Top]`. The override is consulted before any other dispatch and # bypasses fail-soft tracing on a recognised match. declared = scope.declared_types[node] return declared if declared return type_of_virtual(node) if node.is_a?(AST::Node) handler = PRISM_DISPATCH[node.class] return send(handler, node) if handler fallback_for(node, family: :prism) end |