Skip to content

Commit cbf2a43

Browse files
committed
feat(L10): add propositional state-machine theorems for linearity (A3)
Closes PROOF-NEEDS §P0.2. The existing `NoDoubleFree` is a nullary marker — documentation-as-data — that asserts QTT's structural guarantee without producing a theorem a caller can hand to another proof. This change ADDS a witness-manipulating propositional layer alongside (no existing API changed): data Usage = Fresh | Consumed data LinHandleU : Usage -> Nat -> Type consume : LinHandleU Fresh tok -> LinHandleU Consumed tok Theorems: distinctUsage : Fresh = Consumed -> Void — Usage constructors are distinct; without this the state machine would collapse. consumePreservesData : consume (MkLinFresh off sid) = MkLinConsumed off sid — behavioural lemma for `consume`: offset and schemaId carry through; only the state tag changes. noReuse : LinHandleU Consumed tok -> (Consumed = Fresh) -> Void — the main Level 10 theorem: the only way to transport a consumed handle back to fresh is through an absurd equality, so no total `unconsume` exists in the safe fragment. noReuseEcho : LinHandleU Consumed tok -> LinHandleU Fresh tok -> (Consumed = Fresh) -> Void — cast as an echo-type / displayed-fiber statement: the fiber of `consume` over any consumed handle is at-most-one point. Connects Linear to the Echo.idr machinery landed in A0. The existing (1 _ : LinHandle tok) QTT-based linearity enforcement and `NoDoubleFree` marker remain untouched; this is purely additive. idris2 --check clean. panic-attack assail: 0 weak points. Zero dangerous patterns. %default total throughout.
1 parent ff31380 commit cbf2a43

2 files changed

Lines changed: 99 additions & 2 deletions

File tree

LEVEL-STATUS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@ the remaining v1.1 work.**
5151
| 7 | Aliasing safety | Pointer.idr (Unique) | Erased (QTT) | ECHIDNA 10^4 | **Proven [sfap], erased** |
5252
| 8 | Effect-tracking | Effects.idr | Erased (QTT) | ECHIDNA 10^4 | **Proven [sfap], erased** |
5353
| 9 | Lifetime safety | Lifetime.idr | Erased (QTT) | ECHIDNA 10^4 | **Proven [sfap], erased** |
54-
| 10 | Linearity | Linear.idr (QTT q=1) | Erased (QTT) | ECHIDNA 10^4 | **Proven [sfap], erased** |
54+
| 10 | Linearity | Linear.idr (QTT q=1) | Erased (QTT) | ECHIDNA 10^4 | **Proven [sfap], erased. Propositional state-machine theorems added A3 (2026-04-18): distinctUsage, consumePreservesData, noReuse, noReuseEcho — usage-indexed handle `LinHandleU Fresh/Consumed tok` with `consume` state transition, alongside the QTT structural layer.** |
5555
| 11 | Tropical cost-tracking | Tropical.idr | Not yet | None | **In package (A1, 2026-04-18). Commutative-semiring closure PROVEN (A2, 2026-04-18): all 12 axioms — tropAddLeftId/RightId/Comm/Assoc, tropMulLeftId/RightId/Comm/Assoc, tropMulLeftAnn/RightAnn, tropMulDistrib/DistribR. Uses structural `tropMin` (007-lang template). Zero dangerous patterns.** |
5656
| 12 | Epistemic safety | Epistemic.idr | Not yet | None | **In package (A1, 2026-04-18); `writerKnowsFresh`, `freshOrStale`, `syncRestoresFresh` theorems live. Full freshness propagation under concurrent writes deferred.** |
5757
| 13 | Module isolation | ModuleIsolation.idr | (per-module handles, future) | 12 parser/Checker tests | **v1.2 — Idris2 proof + surface checker live; 007 lowering DONE (task #5)** |

src/abi/TypedWasm/ABI/Linear.idr

Lines changed: 98 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ data CompletedProtocol : (token : Nat) -> Type where
157157
MkCompleted : FreeResult -> CompletedProtocol token
158158

159159
-- ============================================================================
160-
-- No-Double-Free Proof
160+
-- No-Double-Free Proof (QTT structural layer)
161161
-- ============================================================================
162162

163163
||| Double-free is impossible by construction.
@@ -171,12 +171,109 @@ data CompletedProtocol : (token : Nat) -> Type where
171171
|||
172172
||| This is not a separate proof to maintain — it falls out automatically
173173
||| from QTT's linear quantity tracking. We document it here for clarity.
174+
|||
175+
||| The section below (Propositional State-Machine Theorems) adds a
176+
||| second, witness-manipulating layer for consumers that want an
177+
||| explicit proof term instead of just the QTT structural guarantee.
174178
public export
175179
data NoDoubleFree : Type where
176180
||| Structural guarantee from QTT: consuming a linear value removes
177181
||| it from scope, making re-consumption impossible.
178182
MkNoDoubleFree : NoDoubleFree
179183

184+
-- ============================================================================
185+
-- Propositional State-Machine Theorems (PROOF-NEEDS §P0.2)
186+
-- ============================================================================
187+
--
188+
-- The `NoDoubleFree` marker above captures the QTT side of the story:
189+
-- the (1 _ : LinHandle token) quantity means consume-exactly-once is
190+
-- enforced by Idris2's quantity tracker at the binding site. That is
191+
-- real but structural — it is not a proposition you can hand to another
192+
-- proof to consume.
193+
--
194+
-- This section adds an explicit witness-manipulating layer: a usage-
195+
-- indexed handle `LinHandleU u tok`, a state transition `consume`, and
196+
-- propositional theorems over the state machine. `LinHandleU` is a
197+
-- displayed type (echo type) over `Usage` — the A0 machinery applies
198+
-- directly.
199+
--
200+
-- Zero dangerous patterns, %default total.
201+
202+
||| Usage state of a linear resource handle.
203+
||| Fresh — not yet consumed; `consume` is callable.
204+
||| Consumed — `consume` has been called; no further transition valid.
205+
public export
206+
data Usage : Type where
207+
Fresh : Usage
208+
Consumed : Usage
209+
210+
||| Linear handle indexed by its usage state.
211+
|||
212+
||| The constructor choice tracks the state in the type, so applying
213+
||| `consume` to a `LinHandleU Consumed tok` is a type error at the
214+
||| binding site (not a runtime check). This is a state machine whose
215+
||| transitions are witnessed by the constructors of `consume`.
216+
public export
217+
data LinHandleU : (u : Usage) -> (token : Nat) -> Type where
218+
||| A fresh, unconsumed linear handle.
219+
MkLinFresh : (offset : Nat) -> (schemaId : Nat) -> LinHandleU Fresh tok
220+
||| A linear handle that has been consumed exactly once.
221+
MkLinConsumed : (offset : Nat) -> (schemaId : Nat) -> LinHandleU Consumed tok
222+
223+
||| State transition: a fresh handle becomes consumed. The underlying
224+
||| offset and schema identifier are preserved so the runtime can still
225+
||| emit the free call after the type-level state has flipped.
226+
public export
227+
consume : LinHandleU Fresh tok -> LinHandleU Consumed tok
228+
consume (MkLinFresh off sid) = MkLinConsumed off sid
229+
230+
-- ---- Theorems ----
231+
232+
||| Fresh and Consumed are distinct usage states.
233+
|||
234+
||| Without this, any attestation built from the state index would be
235+
||| vacuous because the index could collapse.
236+
public export
237+
distinctUsage : Fresh = Consumed -> Void
238+
distinctUsage Refl impossible
239+
240+
||| `consume` preserves the underlying offset and schemaId; only the
241+
||| state tag changes. This is the behavioural lemma for `consume`:
242+
||| the function does what its type promises and no more.
243+
public export
244+
consumePreservesData :
245+
{0 tok : Nat}
246+
-> (off, sid : Nat)
247+
-> consume {tok} (MkLinFresh off sid) = MkLinConsumed off sid
248+
consumePreservesData _ _ = Refl
249+
250+
||| The central Level 10 theorem: once consumed, no path back to Fresh.
251+
|||
252+
||| Any hypothetical "unconsume" function would have to apply this
253+
||| transport. Since its precondition `Consumed = Fresh` is uninhabited
254+
||| (by `distinctUsage`), no total function of type
255+
||| `LinHandleU Consumed tok -> LinHandleU Fresh tok` can be written in
256+
||| the safe fragment.
257+
|||
258+
||| Read: "the only way to move Consumed back to Fresh is to first prove
259+
||| the two states are equal, which produces Void."
260+
public export
261+
noReuse : (h : LinHandleU Consumed tok) ->
262+
(absurd : Consumed = Fresh) ->
263+
Void
264+
noReuse _ Refl impossible
265+
266+
||| Corollary: given a consumed handle, the set of fresh handles that
267+
||| would `consume` to it is empty when the states are distinct. This
268+
||| phrases the noReuse theorem as an echo type / displayed fiber: the
269+
||| fiber of `consume` over any consumed handle has cardinality exactly
270+
||| one (the fresh predecessor), not more.
271+
public export
272+
noReuseEcho : (h : LinHandleU Consumed tok) ->
273+
(f : LinHandleU Fresh tok) ->
274+
(Consumed = Fresh) -> Void
275+
noReuseEcho _ _ prf = distinctUsage (sym prf)
276+
180277
-- ============================================================================
181278
-- Resource Counting (Bounded Linearity)
182279
-- ============================================================================

0 commit comments

Comments
 (0)