Class: Rigor::Inference::ExpressionTyper

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(scope:, tracer: nil) ⇒ ExpressionTyper

Returns a new instance of ExpressionTyper.

Parameters:



221
222
223
224
225
# File 'lib/rigor/inference/expression_typer.rb', line 221

def initialize(scope:, tracer: nil)
  @scope = scope
  @tracer = tracer
  @typing_node = nil
end

Instance Attribute Details

#scopeScope (readonly)

Returns the value of attribute scope.

Returns:



281
282
283
# File 'lib/rigor/inference/expression_typer.rb', line 281

def scope
  @scope
end

#tracerFallbackTracer? (readonly)

Returns the value of attribute tracer.

Returns:



281
282
283
# File 'lib/rigor/inference/expression_typer.rb', line 281

def tracer
  @tracer
end

Class Method Details

.harvest_return_memoHash[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.

Returns:

  • (Hash[untyped, Array[untyped]])


272
273
274
275
276
277
# File 'lib/rigor/inference/expression_typer.rb', line 272

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.

Parameters:

  • call_node (Object)
  • receiver_type (Type::t, nil)
  • arg_types (Array[Type::t])

Returns:

  • (Type::t, nil)


2460
2461
2462
2463
2464
2465
2466
2467
2468
2469
2470
2471
2472
2473
2474
2475
2476
2477
2478
2479
2480
# File 'lib/rigor/inference/expression_typer.rb', line 2460

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]

Parameters:

  • node (Object)

Returns:

  • (Array[Type::t])


2439
2440
2441
2442
2443
2444
# File 'lib/rigor/inference/expression_typer.rb', line 2439

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].

Parameters:

  • node (Object)

Returns:

  • (Type::t, nil)


2429
2430
2431
2432
2433
# File 'lib/rigor/inference/expression_typer.rb', line 2429

def call_receiver_type_for(node)
  return type_of(node.receiver) if node.receiver

  scope.self_type || implicit_top_level_self
end

#dynamic_topType::Dynamic

Returns:



283
284
285
# File 'lib/rigor/inference/expression_typer.rb', line 283

def dynamic_top
  Type::Combinator.untyped
end

#fallback_for(node, family:) ⇒ Type::t

Parameters:

  • node (Object)
  • family: (Symbol)

Returns:

  • (Type::t)


959
960
961
962
963
964
# File 'lib/rigor/inference/expression_typer.rb', line 959

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.

Parameters:

  • node (Object)
  • family: (Symbol)
  • inner_type: (Type::t)


966
967
968
969
970
971
972
973
974
975
976
977
978
# File 'lib/rigor/inference/expression_typer.rb', line 966

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.

Parameters:

  • def_node (Object)
  • receiver (Type::t)
  • arg_types (Array[Type::t])

Returns:

  • (Type::t, nil)


263
264
265
# File 'lib/rigor/inference/expression_typer.rb', line 263

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.

Parameters:

  • statements_node (Object)

Returns:

  • (Type::t)


945
946
947
948
949
# File 'lib/rigor/inference/expression_typer.rb', line 945

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

Parameters:

  • node (Object)

Returns:

  • (Type::t)


227
228
229
230
231
232
233
234
235
236
237
238
# File 'lib/rigor/inference/expression_typer.rb', line 227

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

Parameters:

  • node (Object)

Returns:

  • (Type::t)


951
952
953
954
955
956
957
# File 'lib/rigor/inference/expression_typer.rb', line 951

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



240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
# File 'lib/rigor/inference/expression_typer.rb', line 240

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