Module: Rigor::Inference::MethodDispatcher

Defined in:
lib/rigor/inference/method_dispatcher.rb,
lib/rigor/inference/method_dispatcher/file_folding.rb,
lib/rigor/inference/method_dispatcher/rbs_dispatch.rb,
lib/rigor/inference/method_dispatcher/block_folding.rb,
lib/rigor/inference/method_dispatcher/shape_dispatch.rb,
lib/rigor/inference/method_dispatcher/kernel_dispatch.rb,
lib/rigor/inference/method_dispatcher/constant_folding.rb,
lib/rigor/inference/method_dispatcher/iterator_dispatch.rb,
lib/rigor/inference/method_dispatcher/overload_selector.rb

Overview

Coordinates method dispatch for the inference engine.

Given ‘(receiver_type, method_name, arg_types, block_type, environment)`, the dispatcher returns the inferred result type or `nil` when no rule matches. `nil` is a deliberately blunt “I don’t know” signal: callers (today only ‘ExpressionTyper`) own the fail-soft fallback and decide whether to record a `FallbackTracer` event.

Tiers (in order):

  1. ConstantFolding: executes the Ruby operation directly when the receiver and argument are ‘Constant` carriers and the method is on the curated whitelist. Slice 2.

  2. ShapeDispatch: returns the precise element/value type for a curated catalogue of ‘Tuple`/`HashShape` element-access methods (`first`, `last`, `[]` with a static integer/key, `fetch`, `dig`, `size`/`length`/`count`). Slice 5 phase 2.

  3. RbsDispatch: looks up the receiver’s class in the RBS environment carried by the scope and translates the method’s return type into a Rigor::Type. Slice 4.

‘ShapeDispatch` deliberately runs above RbsDispatch so the precise per-position/per-key answer wins over the projected `Array#[]`/`Hash#fetch` answer; it falls through (`nil`) when the call cannot be proved against the static shape, in which case the projection answer from RbsDispatch applies.

The dispatcher’s public signature reserves space for ‘block_type:` and ADR-2 plugin extensions (later slices), so call sites added now do not have to be rewritten when those tiers arrive.

Defined Under Namespace

Modules: BlockFolding, ConstantFolding, FileFolding, IteratorDispatch, KernelDispatch, OverloadSelector, RbsDispatch, ShapeDispatch

Class Method Summary collapse

Class Method Details

.array_new_fill(type) ⇒ Object



234
235
236
237
238
# File 'lib/rigor/inference/method_dispatcher.rb', line 234

def array_new_fill(type)
  return Type::Combinator.constant_of(nil) if type.nil?

  type
end

.array_new_lift(class_name, arg_types) ⇒ Object



217
218
219
220
221
222
223
224
225
226
# File 'lib/rigor/inference/method_dispatcher.rb', line 217

def array_new_lift(class_name, arg_types)
  return nil unless class_name == "Array"
  return nil if arg_types.empty? || arg_types.size > 2

  size = array_new_size(arg_types.first)
  return nil if size.nil? || size.negative? || size > ARRAY_NEW_TUPLE_LIMIT

  fill = array_new_fill(arg_types[1])
  Type::Combinator.tuple_of(*Array.new(size, fill))
end

.array_new_size(type) ⇒ Object



228
229
230
231
232
# File 'lib/rigor/inference/method_dispatcher.rb', line 228

def array_new_size(type)
  return nil unless type.is_a?(Type::Constant) && type.value.is_a?(Integer)

  type.value
end

.constant_constructor_lift(class_name, arg_types) ⇒ Object



195
196
197
198
199
200
201
202
203
204
205
206
207
# File 'lib/rigor/inference/method_dispatcher.rb', line 195

def constant_constructor_lift(class_name, arg_types)
  builder = CONSTANT_CONSTRUCTORS[class_name]
  return nil if builder.nil?
  return nil unless arg_types.size == 1

  arg = arg_types.first
  return nil unless arg.is_a?(Type::Constant) && arg.value.is_a?(String)

  result = builder.call(arg.value)
  Type::Combinator.constant_of(result)
rescue StandardError
  nil
end

.constant_metaclass(value) ⇒ Object



248
249
250
251
252
253
# File 'lib/rigor/inference/method_dispatcher.rb', line 248

def constant_metaclass(value)
  CONSTANT_METACLASSES.each do |klass, name|
    return Type::Combinator.singleton_of(name) if value.is_a?(klass)
  end
  nil
end

.dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil) ⇒ Rigor::Type?

Returns inferred result type, or ‘nil` for “no rule”.

Parameters:

  • receiver_type (Rigor::Type, nil)

    type of the receiver expression, or ‘nil` for an implicit-self call.

  • method_name (Symbol)
  • arg_types (Array<Rigor::Type>)

    positional argument types.

  • block_type (Rigor::Type, nil) (defaults to: nil)

    inferred return type of the accompanying ‘do … end` / `{ … }` block (Slice 6 phase C sub-phase 2). When non-nil, the dispatcher prefers an overload that declares a block, and binds the method’s block-return type variable to ‘block_type` so a return type like `Array` resolves to `Array`.

  • environment (Rigor::Environment, nil) (defaults to: nil)

    required for RBS-backed dispatch; when nil only constant folding can fire.

Returns:

  • (Rigor::Type, nil)

    inferred result type, or ‘nil` for “no rule”.



61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
# File 'lib/rigor/inference/method_dispatcher.rb', line 61

def dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil)
  return nil if receiver_type.nil?

  precise = dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type)
  return precise if precise

  rbs_result = RbsDispatch.try_dispatch(
    receiver: receiver_type, method_name: method_name, args: arg_types,
    environment: environment, block_type: block_type
  )
  return rbs_result if rbs_result

  # Slice 7 phase 10 — user-class ancestor fallback. When
  # the receiver is `Nominal[T]` or `Singleton[T]` for a
  # class not in the RBS environment (typically a
  # user-defined class), retry the dispatch against the
  # implicit ancestor: `Nominal[Object]` for instance
  # receivers and `Singleton[Object]` for singleton
  # receivers. This resolves Kernel intrinsics
  # (`require`, `raise`, `puts`, ...) and Module/Class
  # introspection (`attr_reader`, `private`, ...) on
  # user classes without requiring the user to author
  # their own RBS.
  try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
end

.dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type = nil) ⇒ Object

Runs the precision tiers (constant fold, shape dispatch, file-path fold, block fold) in order and returns the first non-nil answer. Each tier owns its own receiver/argument shape checks; a tier that does not recognise the receiver returns nil so the next tier can try. The RBS tier sits below this chain and is invoked by the outer ‘dispatch` method.

‘BlockFolding` runs last among the precision tiers because its rules apply only to block-taking calls, so the cheaper arity-based fold tiers above it filter out the common cases first. When `block_type` is nil the tier is a no-op.



99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/rigor/inference/method_dispatcher.rb', line 99

def dispatch_precise_tiers(receiver_type, method_name, arg_types, block_type = nil)
  meta_result = try_meta_introspection(receiver_type, method_name, arg_types)
  return meta_result if meta_result

  ConstantFolding.try_fold(receiver: receiver_type, method_name: method_name, args: arg_types) ||
    ShapeDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
    FileFolding.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
    KernelDispatch.try_dispatch(receiver: receiver_type, method_name: method_name, args: arg_types) ||
    BlockFolding.try_fold(
      receiver: receiver_type, method_name: method_name, args: arg_types, block_type: block_type
    )
end

.expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil) ⇒ Array<Rigor::Type>

Returns the positional block parameter types declared by the receiving method’s selected RBS overload, translated into ‘Rigor::Type`. Used by the StatementEvaluator’s CallNode handler to bind block parameter names before evaluating the block body.

The probe is best-effort: it returns an empty array whenever the receiver, environment, method definition, or selected overload does not provide statically declared block parameter types. Callers MUST treat the empty array as “no information”; the binder falls back to ‘Dynamic` for every parameter slot in that case.

Parameters:

Returns:



273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
# File 'lib/rigor/inference/method_dispatcher.rb', line 273

def expected_block_param_types(receiver_type:, method_name:, arg_types:, environment: nil)
  return [] if receiver_type.nil?

  iterator_result = IteratorDispatch.block_param_types(
    receiver: receiver_type,
    method_name: method_name,
    args: arg_types
  )
  return iterator_result if iterator_result

  RbsDispatch.block_param_types(
    receiver: receiver_type,
    method_name: method_name,
    args: arg_types,
    environment: environment
  )
end

.meta_class(receiver_type) ⇒ Object



159
160
161
162
163
164
# File 'lib/rigor/inference/method_dispatcher.rb', line 159

def meta_class(receiver_type)
  case receiver_type
  when Type::Nominal then Type::Combinator.singleton_of(receiver_type.class_name)
  when Type::Constant then constant_metaclass(receiver_type.value)
  end
end

.meta_new(receiver_type, arg_types = []) ⇒ Object

Singleton.new` returns `Nominal` (a fresh instance), regardless of whether Foo is in RBS. This short-circuits the Class.new generic-`instance` plumbing for user classes, so a discovered-class `ScanAccumulator.new` types as `Nominal` rather than `Class`.

v0.0.7 — for the curated set of immutable scalar-shaped classes that ‘Type::Constant::SCALAR_CLASSES` accepts (today: `Pathname`), `.new(Constant<…>)` lifts to a `Constant<…>` carrier so downstream method calls fold through the standard catalog tier.



178
179
180
181
182
183
184
185
186
187
188
# File 'lib/rigor/inference/method_dispatcher.rb', line 178

def meta_new(receiver_type, arg_types = [])
  return nil unless receiver_type.is_a?(Type::Singleton)

  constant_lift = constant_constructor_lift(receiver_type.class_name, arg_types)
  return constant_lift if constant_lift

  array_lift = array_new_lift(receiver_type.class_name, arg_types)
  return array_lift if array_lift

  Type::Combinator.nominal_of(receiver_type.class_name)
end

.try_meta_introspection(receiver_type, method_name, arg_types = []) ⇒ Object

Slice 7 phase 8 — meta-introspection shortcuts. The default ‘Object#class` RBS return type is `Class`, but for a receiver of known nominal identity we can do better: `instance_of(Foo).class` is `Singleton` (the class object itself), which downstream dispatch uses to resolve `self.class.some_class_method`. The same logic answers `Foo.class` as `Singleton` (deliberate; calling `.class` on a class object yields `Class`, the metaclass). We also special-case `is_a?`- adjacent calls and the trivial `instance_of?(self)` later as the rule catalogue grows; for now only `class` is handled.



152
153
154
155
156
157
# File 'lib/rigor/inference/method_dispatcher.rb', line 152

def try_meta_introspection(receiver_type, method_name, arg_types = [])
  case method_name
  when :class then meta_class(receiver_type)
  when :new then meta_new(receiver_type, arg_types)
  end
end

.try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type) ⇒ Object



112
113
114
115
116
117
118
119
120
121
122
123
124
125
# File 'lib/rigor/inference/method_dispatcher.rb', line 112

def try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type)
  return nil if environment.nil?

  fallback_receiver = user_class_fallback_receiver(receiver_type, environment)
  return nil if fallback_receiver.nil?

  RbsDispatch.try_dispatch(
    receiver: fallback_receiver,
    method_name: method_name,
    args: arg_types,
    environment: environment,
    block_type: block_type
  )
end

.user_class_fallback_receiver(receiver_type, environment) ⇒ Object



127
128
129
130
131
132
133
134
135
136
137
138
# File 'lib/rigor/inference/method_dispatcher.rb', line 127

def user_class_fallback_receiver(receiver_type, environment)
  case receiver_type
  when Type::Nominal
    return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)

    environment.nominal_for_name("Object")
  when Type::Singleton
    return nil if Rigor::Reflection.rbs_class_known?(receiver_type.class_name, environment: environment)

    environment.singleton_for_name("Class")
  end
end