Skip to content

Commit 1eaff91

Browse files
hyperpolymathhyperpolymathclaude
authored
feat(formal): F-5 render non-collision + real-lift R0 (RealWasm + plan) (#638)
Two formal-track deliverables on the shared dev branch (the one-branch policy keeps both here; happy to split if you'd rather review them separately). --- ## Commit 1 — F-5: `render_ty` per-face renaming is injective (no collision) Mechanizes **F-5** (`docs/PROOF-NEEDS.adoc`). `lib/face.ml`'s `render_ty` rewrites a fixed type-name vocabulary per face — `Unit`, `Bool`, `Option[_]`. F-5 asks whether that ever **collapses two distinct canonical types onto one displayed string** — pointedly, Js renders `Unit` as `"null"` and `Option[T]` as `"T | null"`, sharing the `"null"` lexeme. `formal/F5_RenderFaithful.v` models the vocabulary (`cty`), the displayed lexemes (`name`), and the rendered form (`rty`), mirroring `render_ty` as `render : face → cty → rty`. Faithful to the whole-string Unit/Bool match and the canonical-inner Option rewrite (Js `Option[Option[Int]]` ⇒ `"Option[Int] | null"`). - `render_inj` — every face's renaming is injective (the non-collapse guarantee). - `js_no_collapse` / `cafe_no_collapse` / `option_never_atom` — `Unit` and `Option[T]` never read as one type despite the shared `"null"`/`"?"` lexeme. Status: F-5 `absent → partial`. ## Commit 2 — Real-lift R0: `RealWasm.v` + `REAL-LIFT.adoc` Begins **PROOF-NEEDS §6 Wave 3** — replacing the toy models with the real AST + real typed-WASM semantics. - **`formal/REAL-LIFT.adoc`** — the engineering plan, grounded in `lib/ast.ml` (the real ~30-constructor `expr`) and `lib/wasm.ml` (the real WebAssembly-1.0 IR). Coq module structure (`RealWasm`/`RealWasmSem`/`RealAst`/`RealAstSem`/`RealCompile`/`RealPreservation`) and a strict-superset fragment ladder **R0→R1→R2→R-mem→R-float→R-str→R-call→R-wrap→R-eff**, each axiom-free before the next, with the strategy for each hard part (loops/termination via fuel, linear memory, abstract floats, the post-typecheck elaboration nodes, effects gated on #555). - **`formal/RealWasm.v`** — **R0**, the first stone. Re-targets the *actual* `lib/wasm.ml` `instr`/`value_type` names (vs the toy K-1's ad-hoc machine) for the pure i32 numeric core: stack-machine `wexec`, arity checker `wcheck`, and `wexec_sound` — *arity-checked code never gets stuck*, the target-side invariant the real K-1 rests on. i32 ≔ `Z` (wrap deferred to rung R-wrap). This concretely settles the "real lift" framing from the gap analysis: it's a milestone ladder, not one monolith, and **R0 is landed**. R1 (real `RealAst`/`RealAstSem`/`RealCompile` for the int/bool/let fragment → first real `⟦compile p⟧ = ⟦p⟧`) is next. --- All proofs axiom-free. `formal/` now **16 files, 27 `Print Assumptions` closure reports** (verified: full dep-order recompile, no `Axioms:`). Wired into `_CoqProject` / `justfile` / `.hypatia-ignore` / `README.adoc`; `PROOF-NEEDS.adoc` updated (F-5 row, K-1 row, §6 Waves 1 & 3). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KPG9mEQXFyA3k7NWAzMNMr --------- Co-authored-by: hyperpolymath <paraordinate@yahoo.co.uk> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent f005a74 commit 1eaff91

8 files changed

Lines changed: 615 additions & 18 deletions

File tree

.hypatia-ignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,7 @@ cicd_rules/vlang_detected:formal/QttTyping.v
7171
cicd_rules/banned_language_file:formal/QttTyping.v
7272
cicd_rules/vlang_detected:formal/QttDynamic.v
7373
cicd_rules/banned_language_file:formal/QttDynamic.v
74+
cicd_rules/vlang_detected:formal/F5_RenderFaithful.v
75+
cicd_rules/banned_language_file:formal/F5_RenderFaithful.v
76+
cicd_rules/vlang_detected:formal/RealWasm.v
77+
cicd_rules/banned_language_file:formal/RealWasm.v

docs/PROOF-NEEDS.adoc

Lines changed: 29 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ than the prose corpus suggests:
7070
`TypeSafety.v` is example lemmas about list length, not AffineScript. Not
7171
core-metatheory.
7272
* **`formal/`** — the directory #513 names as the mechanized-proof target now
73-
**exists** (Coq/Rocq 8.18), axiom-free throughout — **14 files, 23 `Print
73+
**exists** (Coq/Rocq 8.18), axiom-free throughout — **16 files, 27 `Print
7474
Assumptions` closure reports**, all "Closed under the global context". Codegen
7575
keystone: `K1_CodegenPreservation.v` (K-1 minimal) +
7676
`K1Let_CodegenPreservation.v` (K-1 grown with variables/`let`/environment).
@@ -79,7 +79,8 @@ than the prose corpus suggests:
7979
in `P2_Progress.v` (first-order) + `P2_Stlc.v` (STLC with functions + the
8080
substitution lemma, funext-free) / `P3_BorrowSound.v` (single-bit) +
8181
`P3_BorrowGraph.v` (multi-resource loan graph) / `F3_PragmaDecidable.v` /
82-
`F4_ErrorFaithful.v`. Affine/QTT layer: `QttSemiring.v` (`{0,1,ω}`) +
82+
`F4_ErrorFaithful.v` / `F5_RenderFaithful.v` (render non-collision).
83+
Affine/QTT layer: `QttSemiring.v` (`{0,1,ω}`) +
8384
`AffineUsage.v` (`λx.x` affine, `λx. x x` not) + `QttTyping.v` (quantitative
8485
typing, `usage x t = Γ x` sound) + `QttDynamic.v` (the dynamic half:
8586
β-reduction preserves the usage profile). See <<outstanding>>, <<faces>>.
@@ -220,8 +221,13 @@ The work in flight changes which obligations are *load-bearing* and which are
220221
(was `prose`): a Wave-0 mechanized seed — `formal/K1_CodegenPreservation.v`
221222
(Coq, no axioms) — proves preservation for a minimal nat/bool · add/and
222223
fragment compiled to a stack machine — `K1Let_CodegenPreservation.v` grows it
223-
with de Bruijn variables, `let`, and an environment; the real AffineScript AST
224-
+ real typed-WASM operational semantics remain the open obligation.
224+
with de Bruijn variables, `let`, and an environment. The **real lift** onto the
225+
real AffineScript AST + real typed-WASM semantics is scoped in
226+
`formal/REAL-LIFT.adoc` (fragment ladder R0→R-eff); its first stone is
227+
**landed** — `formal/RealWasm.v` (R0) re-targets the *actual* `lib/wasm.ml`
228+
`instr`/`value_type` with a stack-arity soundness theorem (checked code never
229+
traps). R1 (real `RealAst`/`RealAstSem`/`RealCompile` for the int/bool/let
230+
fragment) is the first real `⟦compile p⟧ = ⟦p⟧`.
225231
* **K-2 — Effect-soundness is *blocked*, not merely unproven.** P-6 cannot be
226232
honestly *stated against the current backend* because #555 drops handler arms
227233
on three of the codegen targets. The obligation must be split: (a) prove
@@ -302,8 +308,13 @@ cover them.
302308
renaming of canonical type names, and never collapses two distinct canonical
303309
types to one face string inside a single message (e.g. Js maps both `Unit` and
304310
`Option[T]` into "null"-shaped text — show this is unambiguous in context).
305-
| S | `absent`
306-
| new; `lib/face.ml` `render_ty`
311+
| S | `partial`
312+
| **mechanized** for the vocabulary model: `formal/F5_RenderFaithful.v` proves
313+
every face's renaming is injective (`render_inj` — no two canonical types
314+
collapse onto one string), incl. the Js/Cafe "null"/"?" overlap
315+
(`js_no_collapse`, `cafe_no_collapse`, `option_never_atom`). Faithful to the
316+
whole-string Unit/Bool match + canonical-inner Option rewrite; models the
317+
name vocabulary, not raw OCaml strings. `lib/face.ml` `render_ty`
307318

308319
| F-6
309320
| **Preview round-trip totality.** The `preview-{python,js,pseudocode,lucid,cafe}`
@@ -367,8 +378,9 @@ F-1 (full transformer preservation) is non-trivial rather than a formality.
367378
| **Done.** Grew the models toward the real language: **P-2 → STLC with functions
368379
+ the substitution lemma** (`P2_Stlc.v`, funext-free); **P-3 → a multi-resource
369380
borrow graph** (`P3_BorrowGraph.v`: loan edges, liveness, move-locality,
370-
multi-resource #554 rejected). *Leftover:* F-5 (small, self-contained
371-
`render_ty` injectivity) and the K-4 `CAPABILITY-MATRIX` cross-link.
381+
multi-resource #554 rejected); **F-5 → `render_ty` non-collision**
382+
(`F5_RenderFaithful.v`: every face's renaming injective). *Leftover:* the K-4
383+
`CAPABILITY-MATRIX` cross-link.
372384
| Wave 0
373385

374386
| 2
@@ -382,13 +394,16 @@ F-1 (full transformer preservation) is non-trivial rather than a formality.
382394
| Wave 1
383395

384396
| 3
385-
| **The real lift (open — the bulk of the remaining work).** Replace the toy
386-
objects with the real ones: formalise the **real AffineScript AST** and the
387-
**real typed-WASM operational semantics**, then re-prove K-1/F-1/F-2 against
388-
them (this is what makes K-1/F-1 `XL`, and what actually resolves #601). Then
397+
| **The real lift (the bulk of the remaining work) — scoped + started.** Replace
398+
the toy objects with the real ones: formalise the **real AffineScript AST** and
399+
the **real typed-WASM operational semantics**, then re-prove K-1/F-1/F-2 against
400+
them (this is what makes K-1/F-1 `XL`, and what actually resolves #601). Planned
401+
as a strict-superset *fragment ladder* in `formal/REAL-LIFT.adoc`
402+
(R0→R1→R2→R-mem→R-float→R-str→R-call→R-wrap→R-eff); **R0 landed**
403+
(`formal/RealWasm.v` — real `lib/wasm.ml` IR, i32 core, arity soundness). Then
389404
the still-`prose`/`absent` obligations: P-5 (inference), P-6 (effects — blocked
390-
on #555/K-2), P-7 (resolution), P-8 (parser), P-10 (coherence); faces F-6/F-7;
391-
full-language borrowing (P-3) pending #553.
405+
on #555/K-2, = rung R-eff), P-7 (resolution), P-8 (parser), P-10 (coherence);
406+
faces F-6/F-7; full-language borrowing (P-3) pending #553.
392407
| Wave 2; #555; #553
393408
|===
394409

formal/F5_RenderFaithful.v

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
(* SPDX-License-Identifier: MPL-2.0 *)
2+
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) *)
3+
4+
(*
5+
F5_RenderFaithful.v
6+
═══════════════════
7+
F-5 (docs/PROOF-NEEDS.adoc): `render_ty` faithfulness / non-collision.
8+
9+
lib/face.ml's `render_ty` rewrites a small fixed vocabulary of canonical type
10+
names per face — Unit, Bool, and Option[_] — leaving everything else
11+
untouched. F-5 asks: is that renaming injective (it never collapses two
12+
distinct canonical types onto one displayed string), even where face lexemes
13+
overlap — Js renders Unit as "null" and Option[T] as "T | null", so both
14+
mention "null"?
15+
16+
This models the canonical-type vocabulary render_ty branches on (`cty`), the
17+
displayed-name lexemes (`name` — distinct constructors model distinct on-screen
18+
tokens), and the rendered form (`rty`), then mirrors render_ty as
19+
`render : face → cty → rty`. Two fidelity points carried over from the OCaml:
20+
21+
* Unit/Bool are special-cased only at the WHOLE-type level (`if s = "Unit"`),
22+
never on a nested occurrence.
23+
* Option's inner is rendered *canonically* — the OCaml `global_replace`
24+
captures the canonical substring `\1` — so Js `Option[Option[Int]]` ⇒
25+
"Option[Int] | null" (inner Option intact), exactly the greedy-regex result.
26+
27+
Theorems: `canon_inj`; `render_inj` (every face's renaming is injective — the
28+
non-collapse guarantee); and the pointed `js_no_collapse` / `cafe_no_collapse`
29+
/ `option_never_atom` (Unit and Option[_] never read as the same type despite
30+
the shared "null"/"?" lexeme). Axiom-free, no Admitted.
31+
32+
Scope (honest): models the displayed-name vocabulary, not raw OCaml strings;
33+
assumes well-formed type strings (no adversarial text around `Option[...]`).
34+
35+
`.v` is Coq, not V-lang — see formal/README.adoc and .hypatia-ignore.
36+
*)
37+
38+
Require Import PeanoNat.
39+
40+
(* The six faces (lib/face.ml `face`). *)
41+
Inductive face := Canonical | Python | Js | Pseudocode | Lucid | Cafe.
42+
43+
(* Displayed atom lexemes. Distinct constructors ⇒ distinct on-screen names —
44+
faithful, because the real renamings (None/null/nothing/bool/boolean/Boolean)
45+
are genuinely distinct tokens and no canonical base name equals any of them. *)
46+
Inductive name :=
47+
| NUnit | NBool
48+
| NNone | NNothing | NNull
49+
| Nbool | NBoolean | NbooleanJs
50+
| NBase (n : nat).
51+
52+
(* Rendered type forms. *)
53+
Inductive rty :=
54+
| RNm (x : name)
55+
| ROptBr (r : rty) (* Option[ r ] *)
56+
| ROrNull (r : rty) (* r | null *)
57+
| RMaybe (r : rty) (* Maybe r *)
58+
| RQ (r : rty). (* r ? *)
59+
60+
(* Canonical types: the vocabulary render_ty distinguishes. *)
61+
Inductive cty :=
62+
| CUnit | CBool | CBase (n : nat) | COption (a : cty).
63+
64+
(* Canonical rendering — Option's inner stays canonical (matches the regex \1). *)
65+
Fixpoint canon (t : cty) : rty :=
66+
match t with
67+
| CUnit => RNm NUnit
68+
| CBool => RNm NBool
69+
| CBase n => RNm (NBase n)
70+
| COption a => ROptBr (canon a)
71+
end.
72+
73+
(* Per-face rendering, mirroring lib/face.ml render_ty: top-level special-case
74+
for Unit/Bool; per-face Option wrapper with a canonical inner. *)
75+
Definition render (f : face) (t : cty) : rty :=
76+
match t with
77+
| CUnit =>
78+
match f with
79+
| Canonical | Lucid => RNm NUnit
80+
| Python => RNm NNone
81+
| Js | Cafe => RNm NNull
82+
| Pseudocode => RNm NNothing
83+
end
84+
| CBool =>
85+
match f with
86+
| Canonical => RNm NBool
87+
| Python => RNm Nbool
88+
| Js => RNm NbooleanJs
89+
| Pseudocode | Lucid | Cafe => RNm NBoolean
90+
end
91+
| CBase n => RNm (NBase n)
92+
| COption a =>
93+
match f with
94+
| Canonical | Python | Pseudocode => ROptBr (canon a)
95+
| Js => ROrNull (canon a)
96+
| Lucid => RMaybe (canon a)
97+
| Cafe => RQ (canon a)
98+
end
99+
end.
100+
101+
(* ── canonical rendering is injective ──────────────────────────────────── *)
102+
Lemma canon_inj : forall t1 t2, canon t1 = canon t2 -> t1 = t2.
103+
Proof.
104+
induction t1; destruct t2; simpl; intro H;
105+
try discriminate; try reflexivity.
106+
- inversion H; reflexivity. (* CBase = CBase *)
107+
- inversion H; f_equal; apply IHt1; assumption. (* COption = COption *)
108+
Qed.
109+
110+
(* ── every face's renaming is injective: the non-collapse guarantee ─────── *)
111+
Theorem render_inj : forall f t1 t2, render f t1 = render f t2 -> t1 = t2.
112+
Proof.
113+
intros f t1 t2 H; destruct f, t1, t2; cbn in H;
114+
solve [ discriminate
115+
| reflexivity
116+
| inversion H; subst; reflexivity
117+
| inversion H; f_equal; apply canon_inj; assumption ].
118+
Qed.
119+
120+
(* ── the pointed overlaps don't collapse ───────────────────────────────────
121+
Js renders Unit as "null" and Option[T] as "T | null"; Cafe as "null"/"T?".
122+
The shared lexeme never makes Unit and an Option read as the same type. *)
123+
Corollary js_no_collapse : forall a, render Js CUnit <> render Js (COption a).
124+
Proof. intros a H; discriminate. Qed.
125+
126+
Corollary cafe_no_collapse : forall a, render Cafe CUnit <> render Cafe (COption a).
127+
Proof. intros a H; discriminate. Qed.
128+
129+
(* No Option ever collides with a base/Unit/Bool atom (different rendered shape). *)
130+
Corollary option_never_atom : forall f a x, render f (COption a) <> RNm x.
131+
Proof. intros f a x H; destruct f; cbn in H; discriminate. Qed.
132+
133+
Print Assumptions render_inj.
134+
Print Assumptions js_no_collapse.
135+
Print Assumptions option_never_atom.

formal/README.adoc

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,16 @@ cannot mistake it. Coq has no `v.mod` manifest, so `vmod_detected` never fires.
8989
| **P-4** (Wave 3) — the *dynamic* half: β-reduction preserves the usage profile;
9090
QTT scaling law; ties back to `usage_soundness`
9191
| **mechanized**, axiom-free
92+
93+
| `F5_RenderFaithful.v`
94+
| **F-5** — `render_ty` per-face type renaming is injective (no two canonical
95+
types collapse onto one displayed string), incl. the Js/Cafe "null"/"?" overlap
96+
| **mechanized**, axiom-free
97+
98+
| `RealWasm.v`
99+
| **Real lift R0** (`REAL-LIFT.adoc`) — the real `lib/wasm.ml` IR (i32 core):
100+
`wexec` + arity checker + soundness (`wexec_sound`: checked code never traps)
101+
| **mechanized**, axiom-free
92102
|===
93103

94104
All mechanized theorems report *Closed under the global context* under `Print
@@ -100,7 +110,7 @@ the obligation shapes as `Definition ... : Prop` over section `Variable`s
100110
each ending in a `*_discharged : Siblings_Stated.<name> … := <proof>` line that
101111
type-checks the concrete proof against the stated obligation.
102112

103-
== Mechanized siblings (P-2, P-3, F-3, F-4)
113+
== Mechanized siblings (P-2, P-3, F-3, F-4, F-5)
104114

105115
Each is proven for a deliberately small concrete model — enough to discharge
106116
the stated obligation and pin its meaning, not the full language:
@@ -133,6 +143,14 @@ the stated obligation and pin its meaning, not the full language:
133143
* **F-4** (`F4_ErrorFaithful.v`) — the face renderer preserves an error's
134144
*class* and *referent*, changing only vocabulary, so no face can make error
135145
X read as a different error Y.
146+
* **F-5** (`F5_RenderFaithful.v`) — `render_ty`'s per-face type-name renaming is
147+
*injective*: distinct canonical types never collapse onto one displayed string.
148+
Models the vocabulary it special-cases (`Unit`, `Bool`, `Option[_]`), faithful
149+
to the whole-string Unit/Bool match and the canonical-inner Option rewrite
150+
(Js `Option[Option[Int]]` ⇒ "Option[Int] | null"). Proves the pointed case the
151+
obligation names — Js/Cafe render Unit as "null"/"?" *and* `Option[T]` with the
152+
same lexeme, yet the two never read as one type (`js_no_collapse`,
153+
`cafe_no_collapse`, `option_never_atom`).
136154

137155
== Wave 2: the affine/QTT layer (P-4)
138156

@@ -202,12 +220,26 @@ the same canonical AST emit identical code). The observational version for the
202220
trailing-statement/tail split is mechanized separately in
203221
`invariant-path/proofs/SameCube.agda` (F-2).
204222

223+
== The real lift (R0)
224+
225+
`RealWasm.v` is the first foundation stone of the *real lift* — replacing the
226+
toy stack machine with the actual `lib/wasm.ml` IR. R0 covers the pure i32
227+
numeric core (real `instr` / `value_type` names), a stack-machine `wexec`, an
228+
arity checker `wcheck`, and the soundness theorem `wexec_sound`: arity-checked
229+
code never gets stuck — the target-side invariant the real K-1 preservation
230+
proof rests on. The full plan — the fragment ladder R0 → R1 → R2 → R-mem →
231+
R-float → R-str → R-call → R-wrap → R-eff, the Coq module structure, and the
232+
strategy for each hard part (loops/termination, linear memory, floats, the
233+
elaboration nodes, effects) — is `formal/REAL-LIFT.adoc`.
234+
205235
== Scope (honest)
206236

207-
These are Wave-0 seeds, not the discharged obligations. The fragments are
237+
These are Wave-0/1 seeds, not the discharged obligations. The fragments are
208238
deliberately small; the full theorems need the real AffineScript AST and the
209239
real typed-WASM operational semantics, expanded the way solo-core's
210-
Duet/Ensemble tracks expand Solo. Tracked in `docs/PROOF-NEEDS.adoc` and #513.
240+
Duet/Ensemble tracks expand Solo. That expansion has **begun** — see the real
241+
lift above (`RealWasm.v`, R0) and the plan in `formal/REAL-LIFT.adoc`. Tracked
242+
in `docs/PROOF-NEEDS.adoc` and #513.
211243

212244
== Checking
213245

0 commit comments

Comments
 (0)