0054 — Handler reference by generative IDENTITY (re-base as data), not an integer cap
- Status: Accepted
- Summary: Reverse ADR-0053's absolute caps (build-verified UNSOUND: a first-class thunk that locally handles its own effect, forced under an unrelated handler, mis-dispatches its own
performto the outer handler — a well-typed,LWConfig-valid program evaluating to a wrong-typed value). Root cause is structural: a SINGLE INTEGER cap cannot be both migration-stable and shift-free, andcloseC ≡ Comp.substcouples them (a substitution-time shift for migration soundness re-shiftscloseC_handle*= the ADR-0050 LR wall). Fix (deep-research-grounded, Lexa/Effekt/Koka): theperform's handler reference is a generative IDENTITY (a fresh label/capability travelling WITH the thunk), and any handler-crossing re-base is carried as explicit DATA on the value, not proof-internal index arithmetic. Identity-as-value needs NO 6th primitive (a label/capability is an ordinary value;handleralready primitive) — refining ADR-0044's hesitation. Keep the step-indexed LR (route B, Effekt System Ξ shows lexical capability-passing admits a closeable LR); Leroy forward-simulation (Lexa, no LR) is a recorded alternative. First-class-thunk escape is ruled out by the EXISTINGLWTnon-escape gate (preservation_returnEscape), not by second-class thunks. Representation redesign in SHAPE (Core/Syntax + VM dispatch re-derivation); replaces an unsound kernel, not an upgrade. - Supersedes: 0053
- Depends-on: 0044, 0045, 0046, 0050, 0052, 0053, 0016
- See-also: 0023, 0024, 0030
Status
Accepted (2026-06-25, operator ruling after a build-verified soundness counterexample + a fan-out literature deep-research, 22/25 claims confirmed 3-0 against primary peer-reviewed sources). The DECISION (representation = generative identity) is recorded; IMPLEMENTATION is pending — the kernel still carries ADR-0053 absolute caps (unsound) until the redesign lands. The LR-layer achievement ADR-0053 reported (seam 5→2) is to be re-established under the new representation, not discarded.
Context
ADR-0053 adopted absolute/level caps (the perform cap counts handlers from the program root) to
dissolve the ADR-0050 de-Bruijn-shift LR wall and land the LR seam 5→2. Its soundness rationale was
"a cap-carrying thunk cannot escape its handler; migration only pushes handlers ABOVE the target,
never below it." That rationale has a hole, build-verified this session:
-
The counterexample (
scratch/MigrationSoundnessProbe.lean,scratch/MigrationTypingProbe.lean).migrate vFragile = (λx. handle (throws 2) (force x)) { handle (state 1 7) (perform 0 1 "get" ()) }. A thunk that locally handles its OWNstate, forced under an unrelatedthrows. Build-verified:HasCTy [] [] (migrate vFragile) ⊥ (F 1 int)— well-typed, axiom-clean[propext, Classical.choice, Quot.sound](HasCTy.performleavescapUNCONSTRAINED, Syntax.lean:163 — typing is cap-irrelevant by design).LWConfig ([], migrate vFragile)— proven (the live invariant ACCEPTS it).Source.eval (migrate vFragile) = done(non-int)— a wrong-TYPED value (thegetmis-dispatches to thethrows), whileforce vFragile(no migration) =done 7and the migration-aware cap-1 variant =done 7. type_safety is violated. The missed case: the thunk targets its OWN internal handler (not an ambient one), so forcing it under another handler is an insert-BELOW-the-target migration. The regression suite (capMigrate1/2) only tested insert-ABOVE-target, so the build stayed green. ADR-0053's "duty #3 of the multi-duty shift" (WC below-insert compensation) was framed as a proof artifact; it is runtime soundness.
-
Why no integer cap works, and why the minimal patch is dead.
closeC(the LR's environment- closing) ISComp.substfolded (LR.lean:931; thecloseC_handle*proofs at Compat.lean:218-252 consumesubstFrom's no-shift handle case verbatim). So the cap shift is ONE shared knob:substFrom shifts on handle? migration sound? LR 5→2 closes? absolute caps NO ✗ (the bug) ✓ restore shift YES ✓ ✗ (closeC_handle* re-shift = ADR-0050 wall)Migration-soundness and the LR win are the same knob in opposite positions. The "minimal patch" (keep dispatch absolute, restore a substitution-time re-base) reintroduces exactly the wall.
Decision
The perform's handler reference is a generative IDENTITY, and any handler-crossing re-base is
carried as explicit DATA on the value — not a single integer counted from anywhere.
- Identity, not position. A
handlemints a fresh generative label/capability; aperformtargets that identity directly. The identity is captured WITH the thunk, so dispatch follows it regardless of the forcing context → migration-sound by construction; shadowing = ordinary lexical scoping. - Re-base as data (the literature's key refinement). The handler-crossing adjustment is NOT
eliminated — it is RELOCATED out of proof-internal index arithmetic into first-class data (Koka's
under/third-evidence-component; Effekt'slift; Koka'sopen). The concrete BANG shape: store the thunk's defining handler-context as an explicit field, re-based at CAPTURE, not recomputed at force. This is WHY the LR closes — the shift becomes a term/data transform the relation quantifies over, instead of an opaque re-index inside its closing/substitution lemmas. - No 6th primitive (invariant #5 preserved). A generative label / capability is an ordinary value;
handleris already a primitive. Identity-by-value is expressible over{thunk, force, effect-row, handler, STM}. (Deep-research, 3-0: this refutes ADR-0044's worry that direct dispatch "may be a 6th-primitive-class change.") - Proof route = keep the step-indexed LR (route B). Effekt's System Ξ shows lexical capability-
passing admits a CLOSEABLE step-indexed LR (mechanized in Coq) — the opposite of the relative-cap
shift wall. This preserves the banked
lr_soundinfrastructure (the 5→2 is re-established, not rebuilt from scratch). Route A (Leroy-style forward simulation, à la Lexa, which has NO step-indexed LR so the obstruction never arises) is a recorded alternative — clean-slate, discardslr_sound. - First-class-thunk escape = the EXISTING
LWTnon-escape gate. A thunk whose target identity has escaped its handler's extent → stuck unless ruled out by typing. We already have the gate: the two- contextLWT S Rdiscipline (a returned value's caps must resolve where it LANDS) — closingpreservation_returnEscapeIS this. We do NOT make thunks second-class (Effekt's 2020 move, incompatible with BANG's first-class thunks). Effekt's System C boxing (capture-set types, OOPSLA'22) is the recorded post-v1 expressiveness upgrade if escaped capabilities must be re-usable.
Amendment (2026-06-25, during implementation) — the invariant COLLAPSE
Implementing inc 2/3 surfaced a major simplification (operator-approved). ADR-0045 introduced the separate
WellCapped/LWConfig structural invariant because "typing is cap-irrelevant" — the positional cap was
a bare Nat, invisible to typing, so cap-resolution needed its own invariant. Under identity caps that
premise is false: the capability is a value c : Cap ℓ and HasCTy.perform requires it, so typing is
now cap-RELEVANT. Resolution becomes a TYPING + lexical-scoping property (a vvar : Cap ℓ in scope ⟹ its
binding handle lexically encloses the perform ⟹ that handler is on the stack when it fires; runtime
vcap n names the just-installed handleF n).
Decision: COLLAPSE the well-cappedness invariant into typing.
HasConfig = HasConfigTy ∧ LWConfig (positional resolution + WC keystone + absResolvesKind + shiftCap)
↓
HasConfig = HasConfigTy ∧ NonEscape — NonEscape = capabilities don't outlive their handler
The ENTIRE positional well-cappedness machinery dissolves (the WC keystone — the session-long hard piece —
absResolvesKind/CapResolves/staticSplit/absSplit and the shift theory are DELETED). The sole
remaining structural obligation is non-escape, which IS the existing preservation_returnEscape,
now promoted from one clause to the whole invariant. Mirrors the research (Effekt: capability-passing makes
resolution lexical, not a runtime-searched invariant). The exact NonEscape definition is the inc-4
metatheory crux (revealed by what preservation/progress need; the thunk-escape case is the subtle part,
gated as before). Frozen-statement-safe: preservation/progress/type_safety stay stated over HasConfig.
Confirmation
- Build-gated unsoundness witnesses (this session):
scratch/MigrationSoundnessProbe.lean(the mis-evaluation +LWConfigvalidity),scratch/MigrationTypingProbe.lean(migrate_vFragile_well_typed, axiom-clean),scratch/archive/WCKeystoneCounterProbe.lean(theabsSplitcap-0 mis-resolution under below-insert). These pin the ADR-0053 hole so a future "fixed" claim is gated against a run, not prose. - Implementation fitness (future): a generative-identity
performdispatches CORRECTLY on themigrate vFragilewitness; the re-establishedcloseC_handle*distribute without a positional shift;#print axioms lr_soundre-closes to the descent set;preservation_returnEscapedischarges via theLWTgate. ThecapMigratesuite gains an insert-below-target case (the gap that hid this).
Rejected alternatives
- Absolute / level caps (ADR-0053). Build-verified UNSOUND (above). Superseded.
- Relative de-Bruijn caps + handler-crossing shift (ADR-0046/0050). Migration-sound but the shift
crosses handler binders and walls the LR (
δ.map shiftCapmismatch in the 3 handler arms). - Minimal patch (absolute dispatch + restore the substitution-time shift). Structurally dead:
closeC ≡ subst, so shiftingsubstre-shiftscloseC_handle*= the ADR-0050 wall. The shared knob cannot be in two positions at once. - Leroy-style forward simulation, no step-indexed LR (Lexa). Sound + the index-shift obstruction
never arises — but discards the already-built
lr_sound(the 5→2). Recorded as a viable alternative route, not chosen now (keep the LR investment). - Second-class capabilities (Effekt 2020). Makes the bad migration unrepresentable by forbidding capabilities from being returned/stored — incompatible with BANG's FIRST-CLASS thunks.
- System C boxing now (Effekt OOPSLA'22). Recovers first-class escaped capabilities via capture-set
types — more machinery than v1 needs; deferred (the
LWTgate suffices to FORBID escape for v1).
More information
Deep-research synthesis (fan-out web search → 23 sources → 25 claims adversarially verified 3-0; full report in the session transcript). The convergent finding across four surface forms — generative labels as lexical capabilities (Lexa), capability-passing typed by an ordered stack shape (Effekt/λCap), evidence vectors with a data re-base (Koka), protocol reasoning (Hazel) — is: reference by identity, carry the re-base as data. Primary sources:
- Ma, Ge, Lee, Zhang, "Lexical Effect Handlers, Directly" (OOPSLA'24) — generative labels, Leroy forward-simulation compiler correctness (no step-indexed LR).
- Brachthäuser, Schuster, Ostermann, Effekt / "Effects as Capabilities" (POPL'20) + Schuster et al
λCap (ICFP'20) — capability-passing,
lift, ordered stack shape; System Ξ closeable LR (ICFP'22); System C boxing (OOPSLA'22) for first-class blocks. - Xie, Leijen, "Generalized Evidence Passing for Effect Handlers" (ICFP'21) — evidence triple
(marker, handler, defining-vector); theunderframe's third component is the re-base-as-data. - Biernacki, Piróg, Polesiuk, Sieczkowski, "Handle with Care" (POPL'18) — step-indexed biorthogonal
LR; label-select +
lift-skip (the label+skip-count hybrid our single-int cap collapsed). - de Vilhena, Pottier, "A Separation Logic for Effect Handlers" / Hazel (POPL'21) — protocol (not cap) dispatch; unary, single-unnamed-effect (partial data point; the relational/named extension is the frontier the compiler-correctness LR needs).
Open questions carried forward (deep-research + internal): (1) Does "spine cap vs thunk-internal cap" map exactly onto Koka's third-evidence-component (defining-context stored on the thunk, re-based at CAPTURE)? (2) The binary/relational LR for compiling MULTIPLE generative handlers (Hazel is unary; the 2026-POPL "blaze" relational separation logic is the closest closeable-binary-relation candidate). (3) Does System C boxing port to graded-CBPV if first-class escaped capabilities are later needed?
Inherited from ADR-0053 (carries forward as input, not reversed): the multi-duty-shift analysis;
that the LR layer (Compat) is independent of the WC/WellCapped machinery; that the CalcVM is the
orthogonal deferred route-B (ADR-0052). The keystone-2c (WCComp.shiftCap_insert) and the now-dead
WellCapped are subsumed — the live path is LWConfig/LWT, which the identity representation rebuilds.