0052 — bang's effect dispatch is LEXICAL; the CalcVM reference re-derives cap-keyed (route B)
- Status: Accepted
- Summary: The kernel (ADR-0045 static/cap dispatch) and the CalcVM reference
evalD(ADR-0023 dynamic/nearest-label) build-provenly DISAGREE on a well-typed same-label-shadowing program:handle (state 1 10) (handle (state 1 20) (perform cap=1 1 "get"))gives kernel10(cap=1 names the OUTER handler, lexical) butevalD20(nearest-label, dynamic) — bothrfl. The witness is well-TYPED (not just well-capped):handleState's subsumption premisee ≤ labelEff ℓ ⊔ φadmits a handler over a body that doesn't use ℓ, so same-label nesting types. DECISION: bang's effect dispatch is LEXICAL — the kernel (ADR-0045's cap dispatch) is canonical;evalDis the stale dynamic half left over from before the typed-static pivot. Route B: re-deriveevalD(+unwindFind/compile/exec) cap-keyed to match the kernel — full generality, no seam, return-only/vacuous handlers retained. Sequence B AFTER the absolute-caps migration (ADR-0053): cap-keyingevalDagainst de-Bruijn caps then re-doing it for absolute caps is wasted work. Rejected: (A) CalcVM-seam — ships a KNOWN machine≠kernel divergence (the diff-testAgreeasserts onevfrom both, UNSATISFIABLE on shadow programs → not diff-testable → strictly weaker than the LR seam-5, which hasexecas oracle); (C-new=relevance typing) — alabelEff ℓ ≤ epremise forbids the divergence by construction but also kills return-only handlers (handle (throws 0) (ret 7), a deliberate + tested feature; the effect row cannot distinguish "ℓ handled below" from "ℓ unused" — both⊥); (C-new=scope-tracking) — a true narrow rule needs an in-scope-label set threaded through 12 rules + ~134 Metatheory + the FROZEN Spec (large, re-freeze). - Amends: 0044, 0050
- Depends-on: 0023, 0044, 0045, 0050
Status
Accepted (2026-06-25, operator ruling "CalcVM B"). Decision recorded now; execution deferred to a deliberate multi-session unit after the absolute-caps migration. This ADR fixes the semantic question (dispatch is lexical) and the route (B); the implementation is future work.
Context
ADR-0045 pivoted the kernel to typed static (cap) dispatch: Source.step (K, perform cap ℓ op v) = staticDispatch K cap op v — the cap names which handler, resolved lexically at the use-site. The CalcVM reference evalD (ADR-0023/0024) predates this and resolves operations by nearest matching label (dynamic). While cap was inert (ADR-0045 step 1a) the two agreed; step 1b made the cap live, and the CalcVM was never re-run. The ◊5 "CalcVM re-run" investigation (2026-06-25) found this is not a mechanical re-thread but the dynamic-vs-lexical fork ADR-0044 anticipated ("changing dispatch re-runs the calculation"), and produced the build-proven divergence witness above.
The divergence is orthogonal to the cap representation (de-Bruijn vs absolute, ADR-0053): an absolute cap still names the OUTER handler by position; the gap is purely resolution-discipline (lexical vs dynamic). So absolute caps cleans the LR shift-wall but does NOT close this — confirmed from both the CalcVM side and the cap-representation spike.
Decision
bang's effect dispatch is lexical (the cap is a real, semantically-load-bearing selector, per ADR-0045). The CalcVM reference must faithfully calculate the kernel; the kernel is lexical; therefore evalD is re-derived cap-keyed (route B). This keeps the full language (return-only/vacuous handlers, same-label nesting with cap-selection) and makes the executable spec sound against the kernel everywhere — no seam, no feature loss.
The cost (accepted): a multi-session Bahr–Hutton re-derivation touching the reference + calculated machine, and it makes the VM cap-dependent (forfeiting the "VM is cap-agnostic / absolute-caps cost = zero" property the absolute-caps spike measured) — which is exactly why B is sequenced after the absolute-caps migration.
Rejected alternatives
- (A) CalcVM-seam (restrict
evalD_agrees_sourceto a no-shadow fragment). Rejected: the shadow region is a known machine≠kernel divergence, not a tested-but-unproven gap — the diff-testAgree fuel M v := exec (compile M) = some [ret v] ∧ Source.eval fuel M = .done v(CalcVM.lean:2365) needs the samevfrom both and is unsatisfiable on shadow programs, so that region cannot be diff-tested. Shipping it would mean the compiler's shadow behavior is unverified AND inconsistent with the kernel. Strictly weaker than the LR seam-5 (ADR-0050), whereexecis the oracle. Acceptable only as an honestly-documented out-of-scope hole if forced to ship immediately — not chosen. - (C-new = relevance typing) (
labelEff ℓ ≤ eonhandleState/handleThrows). Rejected: makes the divergence unrepresentable cheaply (headline stays frozen) BUT also rejects return-only/vacuous handlers — a deliberate, tested feature (handle (throws 0) (ret 7),CapEscapeWitness.h_M). Build-proven that the effect row cannot separate "ℓ handled below" (⊥) from "ℓ unused" (⊥), so any row-based rule that kills the witness also kills return-only handlers. - (C-new = scope-tracking typing) (add an in-scope-label set to
HasCTy). Rejected for v1: forbids exactly same-label nesting with no feature loss, but threads through 12 computation rules + ~134 Metatheory sites + 31 Syntax + 12 frozen Spec.lean statements (changes the judgment arity → frozen-statement violation + a re-freeze ADR). Large kernel-level refactor.
Sequencing
- Absolute-caps migration (ADR-0053) — LR 5→2, kernel rep change. First.
- Route B: re-derive
evalDcap-keyed against the absolute-cap kernel — the ◊5 CalcVM re-run, full generality. Multi-session, kernel/proof-engineer-paired.
Evidence
- The build-proven witness + the three rejected-route assessments (diff-testability, relevance cost, scope-tracking size) — investigation thread 2026-06-25, captured in this ADR. The bounded route-B bridge diagnosis (re-key
dispatch K ℓ op→ cap-keyed via the kernelstaticSplit_*family) is the starting point for the B implementation.
Amendment (2026-06-28, inc-6 prologue — scope corrected, decision unchanged)
The decision stands (dispatch is lexical; re-derive evalD cap-keyed). Two corrections from the build-arbitrated inc-6 scoping (ke-calcvm-scope, manager-gated; full scope + unit plan in paths/archive/PATH-inc6-calcvm-route-b.md):
-
Target kernel = IDENTITY, not absolute caps. The Sequencing's "absolute-caps migration (ADR-0053) first" is moot — ADR-0053 was superseded by ADR-0054/0055 (handler reference by generative identity + global-fresh). route-B now re-derives
evalDagainst the identity kernel: dispatch by the generative idn(mirroring the greenidDispatch/splitAtId, Operational.lean:284/374), not a level/cap integer. -
route-B is BIGGER than the Evidence's "re-key
dispatch K ℓ op" framing — it is a signature-level re-derivation, not a lookup swap.evalDmust thread the fresh-id counterg, MINT+substitutevcap g h.labelathandle, and re-key itsSStore/THeapfrom label ℓ → identity n — its type grows (fuel → g → … → Option (Outcome × g' × …)), rippling through every lemma, PLUS a ~1820-line bridge rebuild (CalcVM.lean ~2483–4303:ctxStates/CtxCorr/dispatchRun/run_evalD/evalD_agrees_source) that is latent past the line-2383 error halt. By line-count it is mostly mechanical transfer (~3:1, the dispatch-agnostic{RET,LAMI,SUBST,APP,CASE,SPLIT}Code falls out unchanged); by effort/risk it is mostly the dispatch re-derivation (the novel{MARK,UNMARK,THROW,OP}+unwindFindidentity-keyed Code + the bridge — invariant-#4's "evalD = denotation of the new kernel," the genuine Bahr–Hutton calculation, garby §6's open handler-compiler frontier). The wall-risk concentrates inevalD_agrees_sourceover the shadow witness — de-risked consumer-first by a shadow#guardbefore the multi-session grind.