Class: Rigor::Inference::VoidTailSummary
- Inherits:
-
Object
- Object
- Rigor::Inference::VoidTailSummary
- Defined in:
- lib/rigor/inference/void_tail_summary.rb
Overview
ADR-100 WD4 — the lazy per-def "void tail" summary that carries an author-declared -> void
provenance ACROSS an intermediate method whose own signature declares nothing. The direct case
(x = logger.log(...), an RBS -> void resolved on the receiver's own class) is recorded at the
call site by MethodDispatcher::RbsDispatch; this summary answers the transitive case
(def bar; foo; end; b = bar) where the void reaches the use through bar's body, so no rule
reading bar's signature can see it.
VoidTail(def_node) -> VoidOrigin | nil: computed on demand at the first consult, memoised per
def_node (identity-keyed), cycle-guarded (a def already on the current chain rejects), and
pure — AST shape + RBS reflection (Reflection.instance_method_definition /
Reflection.singleton_method_definition) + discovery-index lookups (Scope#user_def_for /
Scope#singleton_def_for) only, never expression evaluation. Because it never evaluates, it can
neither re-enter the dispatcher nor depend on evaluation order or the fork-pool's file
partitioning — the eager record-at-body-evaluation table WD4 rejected failed exactly there.
A def (resolved on owner / kind) is admitted iff all four hold:
1. its OWN resolved RBS signature for `(owner, name, kind)` is absent, or every overload
returns `untyped` (`RBS::Types::Bases::Any`). An author-declared concrete return — including
`-> void` itself, which the direct rule already serves at the call site — keeps it out, so
the direct rule and the summary are disjoint by construction (no double record).
2. its body is a plain statements body (no `rescue` / `else` / `ensure` on the `def` — those
shapes make `def_node.body` a `Prism::BeginNode`) containing NO `Prism::ReturnNode` anywhere:
a bare `return` yields `nil`, a non-void path, so the void tail must be the SOLE return path.
The walk is deliberately over-conservative — a `return` inside a nested block also
disqualifies.
3. its tail (last) expression is a `Prism::CallNode` with an implicit-`self` receiver (`nil`
receiver or a literal `self`).
4. that tail resolves on the SAME owner (exact class, no ancestor walk on the discovery side) to
either (a) an RBS definition whose EVERY overload returns `void` — the leaf, whose origin is
`VoidOrigin(owner, tail_name, kind)` — or (b) another discovered `def` of the same kind, which
recurses. Composition (`def baz; bar; end`) is the recursion, and the origin stays the leaf,
so the message still names the author's `-> void` method. The RBS-void-leaf branch is checked
FIRST because under ADR-93 auto-wire the leaf `def` is BOTH RBS-`void` and a discovered `def`;
recursing into its body would miss the leaf.
Everything else — explicit-receiver tails, conditional / boolean / begin tails, unresolvable
names, non-void concrete RBS — rejects.
Each tail chain is a functional graph (every def has exactly one tail, hence at most one
successor), so a def's result is fully determined by its own forward chain and is independent of
the entry point: a chain either reaches a void leaf (VoidOrigin), a non-admitted node (nil), or a
cycle (nil). Memoising is therefore always sound, and the cycle guard only ever rejects a node that
is still mid-computation on the current chain (never yet memoised).
Instance Method Summary collapse
-
#initialize(scope) ⇒ VoidTailSummary
constructor
A new instance of VoidTailSummary.
-
#origin_for(def_node, owner, kind) ⇒ Inference::VoidOrigin?
The recovered
-> voidorigin fordef_node(whose owner / kind the caller resolved through the discovery index), or nil when thedefis not admitted.
Constructor Details
#initialize(scope) ⇒ VoidTailSummary
Returns a new instance of VoidTailSummary.
68 69 70 71 |
# File 'lib/rigor/inference/void_tail_summary.rb', line 68 def initialize(scope) @scope = scope @environment = scope.environment end |
Instance Method Details
#origin_for(def_node, owner, kind) ⇒ Inference::VoidOrigin?
The recovered -> void origin for def_node (whose owner / kind the caller resolved through
the discovery index), or nil when the def is not admitted. A fresh visited set per top-level
consult; the memo persists across consults for the run. The memo is probed before the visited
set is allocated so the common post-warmup hit path allocates nothing.
82 83 84 85 86 87 88 89 |
# File 'lib/rigor/inference/void_tail_summary.rb', line 82 def origin_for(def_node, owner, kind) return nil if def_node.nil? memo = memo_bucket return memo[def_node] if memo.key?(def_node) walk(def_node, owner, kind, {}.compare_by_identity) end |