Class: Rigor::Inference::VoidTailSummary

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

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.

Parameters:

  • def_node (Prism::DefNode)
  • owner (String)

    the qualified receiver class the def belongs to.

  • kind (Symbol)

    :instance or :singleton.

Returns:



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