Skip to content

Commit ee5000d

Browse files
hyperpolymathclaude
andcommitted
feat(formal): F-5 — render_ty per-face type 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 renaming 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. F5_RenderFaithful.v models the canonical-type vocabulary (cty), the displayed lexemes (name, distinct constructors), and the rendered form (rty), then mirrors render_ty as render : face -> cty -> rty. Faithful to two OCaml details: Unit/Bool are special-cased only at the whole-type level, and Option's inner is rendered canonically (the global_replace captures the canonical substring), so e.g. Js Option[Option[Int]] => "Option[Int] | null". Theorems (axiom-free): canon_inj; render_inj (every face's renaming injective — the non-collapse guarantee); js_no_collapse / cafe_no_collapse / option_never_atom (Unit and Option never read as one type despite the shared null/? lexeme). Wired into _CoqProject / justfile / .hypatia-ignore / README.adoc. PROOF-NEEDS.adoc: F-5 absent -> partial; section 6 Wave 1 leftover cleared; formal/ now 15 files, 26 axiom-free closure reports. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KPG9mEQXFyA3k7NWAzMNMr
1 parent f005a74 commit ee5000d

6 files changed

Lines changed: 167 additions & 8 deletions

File tree

.hypatia-ignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,5 @@ 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

docs/PROOF-NEEDS.adoc

Lines changed: 13 additions & 6 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 — **15 files, 26 `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>>.
@@ -302,8 +303,13 @@ cover them.
302303
renaming of canonical type names, and never collapses two distinct canonical
303304
types to one face string inside a single message (e.g. Js maps both `Unit` and
304305
`Option[T]` into "null"-shaped text — show this is unambiguous in context).
305-
| S | `absent`
306-
| new; `lib/face.ml` `render_ty`
306+
| S | `partial`
307+
| **mechanized** for the vocabulary model: `formal/F5_RenderFaithful.v` proves
308+
every face's renaming is injective (`render_inj` — no two canonical types
309+
collapse onto one string), incl. the Js/Cafe "null"/"?" overlap
310+
(`js_no_collapse`, `cafe_no_collapse`, `option_never_atom`). Faithful to the
311+
whole-string Unit/Bool match + canonical-inner Option rewrite; models the
312+
name vocabulary, not raw OCaml strings. `lib/face.ml` `render_ty`
307313

308314
| F-6
309315
| **Preview round-trip totality.** The `preview-{python,js,pseudocode,lucid,cafe}`
@@ -367,8 +373,9 @@ F-1 (full transformer preservation) is non-trivial rather than a formality.
367373
| **Done.** Grew the models toward the real language: **P-2 → STLC with functions
368374
+ the substitution lemma** (`P2_Stlc.v`, funext-free); **P-3 → a multi-resource
369375
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.
376+
multi-resource #554 rejected); **F-5 → `render_ty` non-collision**
377+
(`F5_RenderFaithful.v`: every face's renaming injective). *Leftover:* the K-4
378+
`CAPABILITY-MATRIX` cross-link.
372379
| Wave 0
373380

374381
| 2

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: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,11 @@ 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
9297
|===
9398

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

103-
== Mechanized siblings (P-2, P-3, F-3, F-4)
108+
== Mechanized siblings (P-2, P-3, F-3, F-4, F-5)
104109

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

137150
== Wave 2: the affine/QTT layer (P-4)
138151

formal/_CoqProject

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,4 @@ QttSemiring.v
1313
AffineUsage.v
1414
QttTyping.v
1515
QttDynamic.v
16+
F5_RenderFaithful.v

formal/justfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ check:
1212
for f in K1_CodegenPreservation K1Let_CodegenPreservation Siblings_Stated \
1313
F1_TransformerPreservation F3_PragmaDecidable F4_ErrorFaithful \
1414
P3_BorrowSound P3_BorrowGraph P2_Progress P2_Stlc \
15-
QttSemiring AffineUsage QttTyping QttDynamic; do
15+
QttSemiring AffineUsage QttTyping QttDynamic \
16+
F5_RenderFaithful; do
1617
echo "== coqc $f.v =="
1718
o="$(coqc -Q . ASFormal "$f.v")"
1819
printf '%s\n' "$o"

0 commit comments

Comments
 (0)