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,
lib/rigor/inference/method_dispatcher/literal_string_folding.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, LiteralStringFolding, OverloadSelector, RbsDispatch, ShapeDispatch

Class Method Summary collapse

Class Method Details

.array_new_fill(type) ⇒ Object



285
286
287
288
289
# File 'lib/rigor/inference/method_dispatcher.rb', line 285

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

  type
end

.array_new_lift(class_name, arg_types) ⇒ Object



268
269
270
271
272
273
274
275
276
277
# File 'lib/rigor/inference/method_dispatcher.rb', line 268

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



279
280
281
282
283
# File 'lib/rigor/inference/method_dispatcher.rb', line 279

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

  type.value
end

.collect_plugin_contributions(registry, call_node, scope) ⇒ Object



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

def collect_plugin_contributions(registry, call_node, scope)
  registry.plugins.filter_map do |plugin|
    contribution = plugin.flow_contribution_for(call_node: call_node, scope: scope)
    contribution.is_a?(FlowContribution) ? contribution : nil
  rescue StandardError
    nil
  end
end

.constant_constructor_lift(class_name, arg_types) ⇒ Object



246
247
248
249
250
251
252
253
254
255
256
257
258
# File 'lib/rigor/inference/method_dispatcher.rb', line 246

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



299
300
301
302
303
304
# File 'lib/rigor/inference/method_dispatcher.rb', line 299

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, call_node: nil, scope: 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”.



64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# File 'lib/rigor/inference/method_dispatcher.rb', line 64

def dispatch(receiver_type:, method_name:, arg_types:, # rubocop:disable Metrics/ParameterLists
             block_type: nil, environment: nil,
             call_node: nil, scope: nil)
  return nil if receiver_type.nil?

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

  # v0.1.1 Track 2 slice 7 — plugin return-type contribution
  # tier. Sits ahead of `RbsDispatch` so a plugin that
  # understands a domain-specific dispatch (e.g. an
  # `ActiveRecord::Base.find` returning `Nominal[<resolved
  # model>]`) wins over the RBS-projected envelope. Only
  # consults the registry when both `call_node` and `scope`
  # are supplied — the dispatcher's own internal callers
  # (per-element block fold, etc.) skip this tier.
  plugin_result = try_plugin_contribution(call_node, scope)
  return plugin_result if plugin_result

  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.



149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/rigor/inference/method_dispatcher.rb', line 149

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) ||
    LiteralStringFolding.try_dispatch(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:



324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
# File 'lib/rigor/inference/method_dispatcher.rb', line 324

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



210
211
212
213
214
215
# File 'lib/rigor/inference/method_dispatcher.rb', line 210

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.



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

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.



203
204
205
206
207
208
# File 'lib/rigor/inference/method_dispatcher.rb', line 203

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_plugin_contribution(call_node, scope) ⇒ Object

ADR-2 § “Flow Contribution Bundle” / v0.1.1 Track 2 slice 7. Walks every loaded plugin’s ‘#flow_contribution_for(call_node:, scope:)` hook, collects the non-nil `FlowContribution` bundles, merges them through `FlowContribution::Merger`, and returns the merged `return_type` slot (or nil when no plugin contributed a return type).

Plugins whose hook raises have their contribution silently dropped for this call so the dispatch chain keeps moving — the run-level diagnostic envelope (per ADR-2 § “Plugin Trust and I/O Policy”) is owned by ‘Analysis::Runner#plugin_emitted_diagnostics`.



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

def try_plugin_contribution(call_node, scope)
  return nil if call_node.nil? || scope.nil?

  registry = scope.environment&.plugin_registry
  return nil if registry.nil? || registry.empty?

  contributions = collect_plugin_contributions(registry, call_node, scope)
  return nil if contributions.empty?

  FlowContribution::Merger.merge(contributions).return_type
end

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



163
164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/rigor/inference/method_dispatcher.rb', line 163

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



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

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