Skip to content

Commit 8282857

Browse files
fix(coq): preservation 910 -> 29 open goals via remember-cfg pattern (Refs standards#134) (#102)
## Summary **97% reduction in `preservation`'s open-goal count** (910 → 29) by applying the standard preservation pattern. The proof remains `Admitted.` — this PR is the cross-case discharge mechanism + the per-case work list, not the full close. This supersedes ephapax#98 (which proposed the 910-goal handoff but did not include the fix); #98 can be closed in favour of this PR. ## The root cause `induction Hstep` did not substitute the outer expression slot `e` to the constructor's form (`e := EStringNew r s` etc.). So `inversion Htype` produced **all 26 typing arms per step rule** — the full 35 × 26 = 910 cross-case combinatorial. Cross-cases had no discriminating equation in scope, so `try solve [exfalso; discriminate | exfalso; congruence]` couldn't fire. Verified empirically with `Show. Show Existentials.` after `inversion Htype; subst;` — the existing `try solve [...]` chain at L3228–L3340 closes **ZERO** of the 910. ## The fix ```coq intros mu R e mu' R' e' Hstep. remember (mu, R, e) as cfg eqn:Hcfg. remember (mu', R', e') as cfg' eqn:Hcfg'. induction Hstep; intros G0 T0 G0' Htype; inversion Hcfg; subst; inversion Hcfg'; subst; inversion Htype; subst; (* existing try-solve chain (unchanged) *) ``` `remember` turns `Hstep : step (mu, R, e) (mu', R', e')` into `Hstep : step cfg cfg'` with two side equations. After `induction Hstep`, `Hcfg` becomes (for `S_StringNew`) `(mu0, R0, EStringNew r s) = (mu, R, e)`, which `inversion Hcfg; subst` decomposes — substituting `e := EStringNew r s` everywhere including in `Htype`. Then `inversion Htype; subst` only generates the diagonal `T_StringNew` arm; the 25 cross-arms are eliminated by inversion's constructor-mismatch check. The earlier `remember e_typed as e_orig` was a misdiagnosis — it remembered the *typing's* `e` (already abstract) instead of the *config's* expression slot (which is what `induction Hstep` substitutes for). ## What's left (29 real diagonal goals) | Category | Step rules | Tactic | |----------|-----------|--------| | Axiom + reconstruct | `S_StringNew`, `S_StringConcat`, `S_StringLen` | `eexists; eapply T_Loc/T_StringConcat/...` | | β-reduction | `S_Let_Val`, `S_LetLin_Val`, `S_App_Fun`, `S_If_*`, `S_Fst`, `S_Snd`, `S_Case_*` | `eexists; eapply subst_preserves_typing; eauto` | | Congruence | `S_*_Step` variants (~15) | `destruct (IHHstep ...); eexists; econstructor; eauto` | | Region | `S_Region_Enter`, `S_Region_Step`, `S_Region_Exit` | existing `T_Region_Active` / `region_shrink_preserves_typing`; `S_Region_Step + T_Region_Active` needs a NEW region-env weakening lemma | | Linear | `S_Drop`, `S_Copy` | `eexists; constructor` | Supporting lemmas already Qed: `subst_preserves_typing`, `region_env_perm_typing`, `region_add_typing`, `region_shrink_preserves_typing`, `values_dont_step`. The region-env weakening lemma for non-values is the only genuine theory question; the other 28 are 1–2 days of standard tactic glue. ## Files - `formal/Semantics.v` — apply the remember-cfg pattern. Replace the old `remember e_typed as e_orig` (the misdiagnosis) with the new `remember (mu, R, e) as cfg` / `remember (mu', R', e') as cfg'` before `induction`, plus `inversion Hcfg; subst; inversion Hcfg'; subst;` after `intros ... Htype` and before the existing `inversion Htype; subst;`. Update the in-file proof-status comment to reflect 910 → 29. - `PROOF-NEEDS.md` — `preservation` "what needs proving" entry updated to reflect the 910 → 29 reduction + pointer to the handoff doc. - `formal/PRESERVATION-HANDOFF.md` — full per-case checklist for the 29 remaining goals; supersedes the 910-goal version on PR#98. ## Verification (local, Coq 8.18.0) ``` cd formal && coq_makefile -f _CoqProject -o Makefile.coq && make -f Makefile.coq # → exit 0; Syntax.vo + Typing.vo + Semantics.vo all produced. ``` ## Refs Refs standards#134 (**NOT Closes** — joint-close on agreement). Supersedes ephapax#98. ## Test plan - [x] `cd formal && coq_makefile -f _CoqProject -o Makefile.coq && make -f Makefile.coq` → exit 0 locally - [x] `Show. Show Existentials.` immediately before `Admitted.` shows 29 goals (down from 910 on the prior state) - [ ] CI green on `rust-ci.yml` "Coq proofs" job - [ ] Close #98 in favour of this PR 🤖 Generated with [Claude Code](https://claude.com/claude-code)
1 parent 95868b9 commit 8282857

3 files changed

Lines changed: 206 additions & 57 deletions

File tree

PROOF-NEEDS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
- Coq admitted proofs remaining in `formal/Semantics.v`: 1 (`preservation`)
1111

1212
## What needs proving
13-
- **`preservation`**: Close the remaining **~40 open goals** in the proof script at `formal/Semantics.v` L3215–L3326 so the `Qed` lands and the file builds without `Admitted.` The supporting lemmas (`region_env_perm_typing`, `region_add_typing`, `region_shrink_preserves_typing`) are Qed; the residual gap is in the top-level case analysis on `step`. Diagnostic build (2026-05-20, `Admitted → Qed` + `idtac` at the remaining-goals point) shows the open goals span ten distinct type-shape variants: `TBase TUnit/TBool/TI32`, `T0`, `TString`, `TFun`, `TProd`, `TSum`, `TBorrow`. Most are congruence-case failures — the proof script's IH-application pattern picks ANY `has_type` in scope and frequently picks the wrong one when inversion introduces multiple typing premises. Closing them is multi-day proof engineering: each step-constructor's congruence case needs its IH applied to the specific inner-expression typing (not just any `has_type`), then the outer typing reconstructed by the matching typing constructor. An earlier in-file comment in `formal/Semantics.v` claimed "Only ONE case remains open: S_Region_Step + T_Region_Active" — that's the *language-design* bottleneck (region-env weakening for non-values), real, but the ~40 figure is the congruence-case overcount on top of it.
13+
- **`preservation`**: Close the remaining ~29 open goals in the proof script at `formal/Semantics.v` L3215–L3339 so the `Qed` lands and the file builds without `Admitted.` Down from 910 open goals (full 35×26 cross-case combinatorial) after introducing the standard preservation pattern (`remember (mu, R, e) as cfg eqn:Hcfg` + symmetric for cfg', then `inversion Hcfg; subst; inversion Hcfg'; subst;` inside each case). The remaining 29 are real diagonal cases — see `formal/PRESERVATION-HANDOFF.md` for the per-case checklist. Supporting lemmas already Qed (`subst_preserves_typing`, `region_env_perm_typing`, `region_add_typing`, `region_shrink_preserves_typing`, `values_dont_step`). The S_Region_Step + T_Region_Active case still blocks on a region-env *weakening* lemma for non-values, which does not yet exist. An earlier ~40-goal diagnostic (PR #104) measured the PRE-`remember-cfg`-pattern state and is superseded.
1414
- **Linear type consumption**: Prove resources with linear types are consumed exactly once across all execution paths (region boundaries, exception handlers)
1515
- **Effect system soundness**: Prove the effect type system correctly tracks side effects and that effect-free terms are truly pure
1616
- **Region safety**: Prove that region-based memory management prevents use-after-free and dangling references across region boundaries

formal/PRESERVATION-HANDOFF.md

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
<!-- SPDX-License-Identifier: PMPL-1.0-or-later -->
2+
3+
# Hand-off: closing `preservation` in `formal/Semantics.v`
4+
5+
Diagnostic + remediation log. The proof is still `Admitted.`, but as of
6+
2026-05-20 it's **down from 910 open goals to ~29 real ones** via the
7+
standard preservation pattern. This file tells whoever picks it up
8+
next exactly what's open and why.
9+
10+
## State at a glance
11+
12+
| Date | Open goals | Notes |
13+
|------|-----------:|-------|
14+
| 2026-04-27 | "fully closed" | In-file comment — but `coqc` rejected the `Qed.`. The claim was unsubstantiated; the proof never closed. |
15+
| 2026-05-20 (am) | 910 | Discovered via `Show. Show Existentials.` before the `Admitted.`. Exactly 35 (step rules) × 26 (typing rules). The existing `try solve [...]` chain closes ZERO. |
16+
| 2026-05-20 (pm) | **29** | After the standard preservation pattern (`remember (mu, R, e) as cfg` + symmetric for cfg', then `inversion Hcfg; subst; inversion Hcfg'; subst;` inside each case). 97% reduction. |
17+
18+
## What the 910 → 29 fix did
19+
20+
The prior `induction Hstep` did not substitute the outer expression
21+
slot `e` to the constructor's form — so `inversion Htype` produced
22+
all 26 typing arms per step rule instead of just the diagonal. Cross-
23+
cases (e.g. `S_StringNew` step + `T_Unit` typing) had no
24+
discriminating equation in scope, so `try solve [exfalso;
25+
discriminate | exfalso; congruence]` couldn't fire.
26+
27+
The fix:
28+
29+
```coq
30+
intros mu R e mu' R' e' Hstep.
31+
remember (mu, R, e) as cfg eqn:Hcfg.
32+
remember (mu', R', e') as cfg' eqn:Hcfg'.
33+
induction Hstep; intros G0 T0 G0' Htype;
34+
inversion Hcfg; subst;
35+
inversion Hcfg'; subst;
36+
inversion Htype; subst;
37+
(* … existing try-solve chain … *)
38+
```
39+
40+
`remember` turns `Hstep : step (mu, R, e) (mu', R', e')` into
41+
`Hstep : step cfg cfg'` with two side equations `Hcfg : ... = (mu, R,
42+
e)`, `Hcfg' : ... = (mu', R', e')`. Now `induction Hstep` substitutes
43+
the constructor's "from" config into `cfg` and the equation `Hcfg`
44+
becomes (for `S_StringNew`) `(mu0, R0, EStringNew r s) = (mu, R, e)`.
45+
`inversion Hcfg; subst` decomposes this and substitutes
46+
`e := EStringNew r s` everywhere — including in `Htype`. Then
47+
`inversion Htype; subst` only generates the diagonal `T_StringNew`
48+
arm; the 25 cross-arms are eliminated by inversion's constructor-
49+
mismatch check.
50+
51+
The earlier `remember e_typed as e_orig eqn:He_orig` (the original
52+
"preserve discriminating equation" attempt) was a misdiagnosis — it
53+
remembered the *typing's* `e`, which was already abstract; the
54+
problem was that the *config's* expression slot was abstract.
55+
56+
## How to reproduce the diagnostic
57+
58+
```coq
59+
end).
60+
Show. (* prints current goal *)
61+
Show Existentials. (* prints all 29 unresolved metavariables *)
62+
Admitted.
63+
```
64+
65+
then:
66+
67+
```sh
68+
cd formal
69+
coq_makefile -f _CoqProject -o Makefile.coq && make -f Makefile.coq
70+
```
71+
72+
`coqc` prints goal count + every open existential. Restore the
73+
`Admitted.` afterwards. Same recipe (now yielding 29 instead of 910)
74+
is the per-case work list.
75+
76+
## The 29 remaining goals (per-case checklist)
77+
78+
### Axiom cases needing explicit reconstruction (~3 goals)
79+
80+
| Step rule | Goal needs | Tactic sketch |
81+
|-----------|-----------|---------------|
82+
| `S_StringNew` | type `ELoc l r` at `TString r` | `eexists; eapply T_Loc; eauto using mem_alloc_lookup` |
83+
| `S_StringConcat` | type `ELoc l' r` at `TString r0` | same as above, with the new alloc |
84+
| `S_StringLen` | type `EI32 (String.length s)` at `TBase TI32` | `eexists; constructor` |
85+
86+
### β-reduction cases needing `subst_preserves_typing` (~6 goals)
87+
88+
| Step rule | Tactic |
89+
|-----------|--------|
90+
| `S_Let_Val`, `S_LetLin_Val`, `S_App_Fun`, `S_Fst`, `S_Snd`, `S_Case_Inl`, `S_Case_Inr` | `eexists; eapply subst_preserves_typing; eauto` (most should fall through the existing chain once the expression slots are concrete) |
91+
| `S_If_True`, `S_If_False` | `eexists; eassumption` (the branch typing is already in context) |
92+
93+
### Congruence cases needing IH + reconstruction (~15 goals)
94+
95+
For each `S_*_Step`: `destruct (IHHstep ...) as [G_out Hout]; eexists;
96+
econstructor; eauto`. The IH's form is awkward after `remember` —
97+
contains the `Hcfg`/`Hcfg'` equations as extra premises. May need
98+
`specialize IHHstep with (1 := Heqcfg_inner) (2 := Heqcfg'_inner)`
99+
first, or restructure to revert + induct on typing instead of step.
100+
101+
### Region cases (~3 goals)
102+
103+
| Step rule | Status |
104+
|-----------|--------|
105+
| `S_Region_Enter` | typing reconstruction (`T_Region_Active` + `In r (r::R)`) — the existing `try solve [eexists; eapply T_Region_Active; ...]` should fire |
106+
| `S_Region_Exit` | needs `region_shrink_preserves_typing` (existing — Qed) + the `expr_free_of_region` step premise |
107+
| `S_Region_Step` + `T_Region_Active` | **the bottleneck** — needs a **region-env *weakening* lemma for non-values** that does not yet exist. Roughly: `forall R e G G' T r, R; G \|- e : T -\| G' -> ~ In r R -> e is not a value -> (r :: R); G \|- e : T -\| G'`. The "not a value" precondition is delicate — naive weakening is unsound for `EBorrow` and friends. Probably 50–150 LOC by itself. |
108+
109+
### Linear cases (~2 goals)
110+
111+
| Step rule | Tactic |
112+
|-----------|--------|
113+
| `S_Drop` | `eexists; constructor` (drop yields `EUnit : TBase TUnit`) |
114+
| `S_Copy` | `eexists; constructor; assumption` (copy yields `EPair v v : TProd T T`) |
115+
116+
## What it would take to finish
117+
118+
Realistic estimate: **days, not weeks** — the 29 remaining goals are
119+
all standard preservation-proof glue except for the region-env
120+
weakening lemma. The lemma's design (which non-value carriers admit
121+
weakening, which don't) is the only genuine theory question. The
122+
mechanical 28 are 1–2 days of tactic work.
123+
124+
## What is NOT a fix
125+
126+
- Adding more `try solve [...]` lines to the existing chain at random.
127+
Run `Show. Show Existentials.` first to see what's actually open.
128+
- Replacing `induction Hstep` with `inversion Hstep` — that loses the
129+
IHs needed for congruence cases.
130+
- Mass-`Admitted.` per case — defeats the point and conflicts with
131+
estate's "build is the only oracle" policy. The honest mark is one
132+
`Admitted.` on `preservation`, not 29.
133+
134+
## Unwind checklist (when finally closed)
135+
136+
1. Replace `Admitted.` with `Qed.`
137+
2. Flip `ROADMAP.adoc`'s admitted-proofs counter `1 → 0`
138+
3. Flip `PROOF-NEEDS.md`'s status row + delete the "what needs
139+
proving" item for `preservation`
140+
4. Delete this file
141+
5. Update `RUST-SPARK-STANCE.adoc`'s E1 row from OWED to DISCHARGED
142+
(and remove the "honest gap" entry about preservation)
143+
6. Delete the proof-status comment block at `Semantics.v` immediately
144+
below the (now-`Qed.`) preservation

formal/Semantics.v

Lines changed: 61 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -3212,18 +3212,31 @@ Theorem preservation :
32123212
exists G_out, R'; G |- e' : T -| G_out.
32133213
Proof.
32143214
intros mu R e mu' R' e' Hstep.
3215+
(* Remember the configs so `induction Hstep` generates explicit equations on
3216+
the outer expression slots. Without these remembers, `induction Hstep`
3217+
leaves the outer `e`/`e'` abstract — for an axiom step rule like
3218+
S_StringNew the constructor pre-conditions land in the context but the
3219+
equation `e = EStringNew r s` does not, so cross-cases (e.g. S_StringNew
3220+
step + T_Unit typing) have no discriminating equation in scope and the
3221+
`try solve [exfalso; discriminate | exfalso; congruence]` chain below
3222+
cannot close them.
3223+
Empirically (Coq 8.18.0, 2026-05-20): without these remembers,
3224+
`inversion Htype; subst` produces the FULL 35 × 26 = 910 cross-case
3225+
combinatorial and the `try solve [...]` chain closes ZERO of them.
3226+
With these remembers + `inversion Hcfg; subst; inversion Hcfg'; subst;`,
3227+
`inversion Htype` only generates the diagonal arm per step rule (since
3228+
the expression slot is concrete after substitution), and the chain
3229+
closes the trivially-dischargeable ones, leaving ~29 real proof
3230+
obligations. Standard preservation pattern; cf. Software Foundations
3231+
`Stlc.preservation`. The prior `remember e_typed as e_orig` was a
3232+
misdiagnosis of the same problem — it remembered the WRONG expression
3233+
(the typing's `e`, which was already abstract) instead of the config's
3234+
expression slot (which is what `induction Hstep` substitutes for). *)
3235+
remember (mu, R, e) as cfg eqn:Hcfg.
3236+
remember (mu', R', e') as cfg' eqn:Hcfg'.
32153237
induction Hstep; intros G0 T0 G0' Htype;
3216-
(* Before inversion: remember the typed expression so that cross-case unification
3217-
(inversion Htype on a free variable `e`) does not lose the step-case equation.
3218-
After `induction Hstep; intros ... Htype`, the expression in Htype may still be
3219-
a free variable `e` with a separate hypothesis `He : e = EStringNew r s` etc.
3220-
If `inversion Htype; subst` fires for a cross-case typing rule (e.g. T_Unit
3221-
typing `e := EUnit`), subst can eliminate `e` and consume `He` in a direction
3222-
that loses the discriminating information. `remember` pins the form as `He_orig`
3223-
so congruence / discriminate can close all expression-mismatch goals. *)
3224-
try (match type of Htype with
3225-
| has_type _ _ ?e_typed _ _ => remember e_typed as e_orig eqn:He_orig
3226-
end);
3238+
inversion Hcfg; subst;
3239+
inversion Hcfg'; subst;
32273240
inversion Htype; subst;
32283241
try solve [eexists; econstructor; eassumption];
32293242
try solve [eexists; eassumption];
@@ -3325,48 +3338,40 @@ Proof.
33253338
end
33263339
end).
33273340
Admitted.
3328-
(* PROOF STATUS [preservation] — ADMITTED.
3329-
3330-
Earlier in-file note (above, around line 3269) claimed "Only ONE
3331-
case remains open: S_Region_Step + T_Region_Active." Diagnostic
3332-
build (Admitted -> Qed + `all: match goal with |- ?G => idtac G end`
3333-
inserted before the Qed) on 2026-05-20 disproves that count:
3334-
~40 goals remain open across the following type-shape variants —
3335-
3336-
exists G_out, R'; G |- e' : T0 -| G_out (many)
3337-
exists G_out, R'; G |- e' : TBase TUnit -| G_out
3338-
exists G_out, R'; G |- e' : TBase TBool -| G_out
3339-
exists G_out, R'; G |- e' : TBase TI32 -| G_out
3340-
exists G_out, R'; G0 |- e' : TString r0 -| G_out
3341-
exists G_out, R'; G0 |- e' : TFun T1 T2 -| G_out
3342-
exists G_out, R'; G0 |- e' : TProd T1 T2 -| G_out
3343-
exists G_out, R'; G0 |- e' : TSum T1 T2 -| G_out
3344-
exists G_out, R'; G0 |- e' : TBorrow T -| G_out
3345-
exists G_out, R'; G0 |- e' : TProd T T -| G_out
3346-
3347-
The "only ONE case" framing — accurate for the *language-design*
3348-
bottleneck (S_Region_Step + T_Region_Active needs region-env
3349-
weakening for non-values) — has obscured that the surrounding
3350-
try-solve scaffolding ALSO fails to close many simpler congruence
3351-
cases. The proof script's IH-application pattern
3352-
match goal with
3353-
| [ IH : forall _ _ _, _ -> exists _, _ |- _ ] =>
3354-
match goal with
3355-
| [ H : has_type _ _ _ _ _ |- _ ] =>
3356-
destruct (IH _ _ _ H) ...
3357-
end end
3358-
picks ANY has_type in scope; with multiple inversion-introduced
3359-
has_types it often picks the wrong one and the subsequent
3360-
`eassumption` fails silently inside `try solve [...]`.
3361-
3362-
Closing the additional ~40 goals is multi-day proof engineering:
3363-
each step-constructor's congruence case needs its IH applied to
3364-
the specific inner-expression typing (not just any has_type), then
3365-
the outer typing reconstructed by the matching typing constructor.
3366-
Tracked separately from the S_Region_Step language-design item.
3367-
3368-
Supporting lemmas remain Qed:
3369-
region_env_perm_typing: Qed (transfers typing across region-env permutations).
3370-
region_add_typing: Qed (adding a region to R preserves typing).
3371-
region_shrink_preserves_typing: Qed (T_Region_Active shadowing case closed via in_dec).
3372-
preservation itself: Admitted, pending discharge of the residual ~40 open goals. *)
3341+
(* PROOF STATUS [preservation] — ADMITTED, but down from 910 → ~29 open goals.
3342+
3343+
PRIOR STATE (before the `remember (mu, R, e) as cfg` introduction at L3232):
3344+
`induction Hstep; intros G0 T0 G0' Htype; inversion Htype; subst; ...`
3345+
left **910 goals open** = 35 (step rules) × 26 (typing rules) — the FULL
3346+
cross-case combinatorial, because `induction Hstep` did not substitute
3347+
the outer expression slot `e` to the constructor's form, so `inversion
3348+
Htype` produced all 26 arms instead of just the diagonal. Cross-cases
3349+
(e.g. S_StringNew step + T_Unit type) had no discriminating equation in
3350+
scope, so `try solve [exfalso; discriminate | exfalso; congruence]` closed
3351+
none of them.
3352+
3353+
CURRENT STATE: ~29 real diagonal goals remain — one per step rule, modulo
3354+
the 6 that the existing `try solve [...]` chain closes once the expression
3355+
slots are concrete. Remediation per case:
3356+
- Axiom cases needing explicit reconstruction: S_StringNew, S_StringConcat,
3357+
S_StringLen (typing the result location/literal via T_Loc/T_I32).
3358+
- β-reduction cases needing `subst_preserves_typing`: S_Let_Val,
3359+
S_LetLin_Val, S_App_Fun, S_If_True/False, S_Fst, S_Snd,
3360+
S_Case_Inl/Inr.
3361+
- Congruence cases needing IH + reconstruction: S_*_Step variants.
3362+
- Region: S_Region_Enter, S_Region_Step, S_Region_Exit (the prior named
3363+
"remaining case" + 2 the in-file comment did not name).
3364+
- Linear: S_Drop, S_Copy.
3365+
3366+
Supporting lemmas already Qed: subst_preserves_typing,
3367+
region_env_perm_typing, region_add_typing, region_shrink_preserves_typing,
3368+
values_dont_step. Per the handoff doc, the S_Region_Step + T_Region_Active
3369+
case still blocks on a region-env *weakening* lemma for non-values, which
3370+
does not yet exist. The other 28 are per-case tactic glue.
3371+
3372+
97% reduction (910 → 29) via the standard preservation pattern (remember
3373+
the configs so `induction Hstep`'s substitution is recovered through
3374+
`inversion Hcfg; subst`). See formal/PRESERVATION-HANDOFF.md for the
3375+
per-case checklist (the handoff doc's 910-goal diagnostic captures the
3376+
PRIOR state; the same Show. recipe applied here yields the 29-goal
3377+
diagnostic for ongoing work). *)

0 commit comments

Comments
 (0)