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):
-
ConstantFolding: executes the Ruby operation directly when the receiver and argument are ‘Constant` carriers and the method is on the curated whitelist. Slice 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.
-
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
- .array_new_fill(type) ⇒ Object
- .array_new_lift(class_name, arg_types) ⇒ Object
- .array_new_size(type) ⇒ Object
- .collect_plugin_contributions(registry, call_node, scope) ⇒ Object
- .constant_constructor_lift(class_name, arg_types) ⇒ Object
- .constant_metaclass(value) ⇒ Object
-
.dispatch(receiver_type:, method_name:, arg_types:, block_type: nil, environment: nil, call_node: nil, scope: nil) ⇒ Rigor::Type?
Inferred result type, or ‘nil` for “no rule”.
-
.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.
-
.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`.
- .meta_class(receiver_type) ⇒ Object
- .meta_new(receiver_type, arg_types = []) ⇒ Object
-
.try_meta_introspection(receiver_type, method_name, arg_types = []) ⇒ Object
Slice 7 phase 8 — meta-introspection shortcuts.
-
.try_plugin_contribution(call_node, scope) ⇒ Object
ADR-2 § “Flow Contribution Bundle” / v0.1.1 Track 2 slice 7.
- .try_user_class_fallback(receiver_type, method_name, arg_types, environment, block_type) ⇒ Object
- .user_class_fallback_receiver(receiver_type, environment) ⇒ Object
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 (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”.
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) = (receiver_type, method_name, arg_types) return if 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.
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 (receiver_type) case receiver_type when Type::Nominal then Type::Combinator.singleton_of(receiver_type.class_name) when Type::Constant then (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 (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 (receiver_type, method_name, arg_types = []) case method_name when :class then (receiver_type) when :new then (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 |