Skip to content

Commit 9bb35ee

Browse files
feat(proofs): Lean array typing — mirror Coq's T_Array/T_Lit_Array (#90)
## What Closes the one **array-shaped Tier-1 gap** on the Lean side of the formal development. AUDIT.md flagged it as *"exactly the Tier-1 item still open on the Lean side"*; `WokeLang.lean` is now at parity with `WokeLang.v` on arrays. The `array` / `vArray` syntax already existed in Lean but had **no typing rules**, so `induction` in `progress`/`preservation` never saw an array case. This adds the rules and the operational semantics, with full proof coverage. ## Changes (`docs/proofs/verification/WokeLang.lean`, +117 / −0 — purely additive) **Typing** (mirror of Coq `T_Array` / `T_Lit_Array`): - `tArray` — `(∀ e ∈ es, HasType Γ e t) → HasType Γ (.array es) (.array t)` - `tArrayVal` — `(∀ v ∈ vs, HasType Γ (.lit v) t) → HasType Γ (.lit (.vArray vs)) (.array t)` **Evaluation** (mirror of `S_Array_step` / `S_Array_val` + panic propagation): - `sArrayStep` — reduce the left-most non-value element - `sArrayVal` — normalise `.array (vs.map .lit)` to the literal value `.lit (.vArray vs)` - `sArrayErr` — propagate a panic out of an array (as the binop rules do) **Proofs** — `progress` and `preservation` extended to cover all three step rules; helper lemmas `isLitB` + `array_split` (the analogue of Coq's `array_elements_progress`). ## Verification - `lean docs/proofs/verification/WokeLang.lean` → **exit 0, empty output** (no errors, no warnings). - **`sorry`-free and axiom-free** under Lean 4.30.0. - Diff is **+117 / −0** on the proof file: existing theorems are untouched; every change is a new constructor or a new case, so no existing `cases`/`induction` becomes non-exhaustive. - `WokeLang.v` (Coq) is **not modified**. ## Method note (vs. Coq) Lean's `∀ e ∈ es` premise yields a **per-element induction hypothesis directly** from `induction`, so — unlike the Coq proof — **no well-founded recursion on an `expr_size` measure** is needed (`array_split` does the pure-list decomposition; the IH answers value-or-steps per element). A fully-evaluated array is *not* an `IsValue` (it always steps via `sArrayVal` to the array literal), so array equality reuses the generic `.lit`-equality rules after normalisation rather than needing the extra array-equality step rules Coq carries. `AUDIT.md` is updated to record the new parity. > Branch note: `claude/sleepy-carson-bREoV` was based on current `main` (it had been 9 commits behind). Its two prior commits — a Coq repair + CI gate and a machine-readable state update — are fully superseded by `main` (which already carries both CI gates, the repaired `WokeLang.v`, and the binop-parity work). They are folded into history via a `-s ours` merge (nothing rewritten or lost); the branch tree is a clean superset of `main`. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_013wg3Mtq2QFhYi4XVw1Z6z7 --- _Generated by [Claude Code](https://claude.ai/code/session_013wg3Mtq2QFhYi4XVw1Z6z7)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 178786e commit 9bb35ee

2 files changed

Lines changed: 159 additions & 8 deletions

File tree

docs/proofs/verification/AUDIT.md

Lines changed: 42 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ the Rust type checker (`src/typechecker/mod.rs`).
6464
| Logical | `and` (both `bool`) | `and or` with **`to_bool` coercion** of any value |
6565
| Result type | `okay`/`oops`/`unwrap`/`error`, `tOkay…tUnwrap` | present in **Rust** typechecker (`Result(_, _)`); **absent** from `core/eval.ml` |
6666
| Unary | `neg` (int/float), `not` | + measured propagation |
67-
| Calls / arrays | `call`, `array` exprs (no typing rules) | builtins + user functions; arrays typed |
67+
| Calls / arrays | `call` (no typing rule); **arrays typed + evaluated** (`tArray`/`tArrayVal`, `sArrayStep`/`sArrayVal`/`sArrayErr`) | builtins + user functions; arrays typed |
6868
| Statements | declared (`Stmt`), **no typing/exec** | full eval in `core/eval.ml` |
6969
| Units of measure | **not modelled** | `EMeasured` / `VMeasured`, unit-match checking |
7070
| Pattern matching, workers, custom types | **not modelled** | present |
@@ -109,8 +109,10 @@ the implementations toward it, or (b) build a *second* Lean model faithful to
109109
proven);
110110
- ordering comparisons `lt/gt/le/ge``bool`;
111111
- `or` (mirror of `and`).
112-
2. **Array typing** (`tArray`, an `array` congruence/▸ value rule) to retire
113-
one of the `array`-shaped TODO gaps.
112+
2. ~~**Array typing** (`tArray`, an `array` congruence/▸ value rule)~~
113+
**DONE (2026-06-18).** `tArray`/`tArrayVal` + `sArrayStep`/`sArrayVal`/
114+
`sArrayErr`, with full `progress`/`preservation` coverage (see
115+
"Extensions landed" below). This brings Lean to parity with Coq on arrays.
114116
3. **Decide the eval-correspondence question** (a) vs (b) above with the
115117
maintainer before attempting PROOF-NEEDS #2.
116118
4. **Compiler/VM track** (the file's §8 TODO stubs: bytecode, compiler,
@@ -151,7 +153,33 @@ theorems (`weaken_collapses_distinction`, `affine_canonical`,
151153
`decide`).
152154
- **Tier 3 (capability):** `capSubsumes` is a preorder (`_refl`, `_trans`).
153155
- Still open in Tier 1: **float** arithmetic variants (`sub`/`mul`/`div` on
154-
`float`, mirroring `add` on float) and **`array`** typing/evaluation.
156+
`float`, mirroring `add` on float).
157+
158+
## Arrays landed (2026-06-18) — Lean now at parity with Coq
159+
160+
The one array-shaped Tier-1 gap is closed on the Lean side, mirroring Coq's
161+
`T_Array`/`T_Lit_Array`:
162+
163+
- **Typing:** `tArray` (array expression, elementwise) and `tArrayVal`
164+
(fully-evaluated `.vArray` literal value), both with the `∀ e ∈ es` premise.
165+
- **Evaluation:** `sArrayStep` (reduce the left-most non-value element),
166+
`sArrayVal` (normalise `.array (vs.map .lit)` to `.lit (.vArray vs)`), and
167+
`sArrayErr` (propagate a panic out of an array, as the binop rules do). A
168+
fully-evaluated array is *not* an `IsValue` — it always steps via `sArrayVal`
169+
to the array literal, which is the value. (This is cleaner than Coq, where a
170+
`map ELit`-array is simultaneously a value *and* steppable, forcing extra
171+
array-equality step rules; in Lean, array equality reuses the generic
172+
`.lit`-equality rules after normalisation.)
173+
- **Proofs:** `progress` and `preservation` cover all three step rules,
174+
`sorry`-free and axiom-free under Lean 4.30.0. The helper `array_split`
175+
does the pure-list "all-literals vs. prefix + first non-literal" split
176+
(the analogue of Coq's `array_elements_progress`).
177+
- **Method note:** Lean's `∀ e ∈ es` premise yields a per-element induction
178+
hypothesis directly from `induction`, so the Lean `progress` needs **no
179+
well-founded recursion on an `expr_size` measure** — the device the Coq
180+
proof requires because Coq's `Forall`-based scheme does not recurse into the
181+
nested `has_type`s. The two developments now prove the same array results by
182+
different means.
155183

156184
## Coq proofs (`WokeLang.v`) — audited 2026-06-14
157185

@@ -178,10 +206,16 @@ treatment as the Lean file.
178206
axioms beyond those implicit in `Coq.Reals`"). Consistent, standard, honest.
179207

180208
- **Coverage vs. the Lean file.** Complementary, not identical:
181-
- Coq is **ahead on arrays** — full `T_Array`/`T_Lit_Array`, array stepping
182-
(`S_Array_step`/`S_Array_val`), and a `progress` that uses **well-founded
183-
induction on `expr_size`** to get an IH for array elements. (This is exactly
184-
the Tier-1 item still open on the Lean side.)
209+
- Arrays are **now at parity** (Lean caught up 2026-06-18): both have
210+
`T_Array`/`T_Lit_Array` (Coq) ⟷ `tArray`/`tArrayVal` (Lean) and array
211+
stepping. The proofs differ in method: Coq's `progress` uses **well-founded
212+
induction on `expr_size`** to get an IH for array elements (its
213+
`Forall`-based induction scheme does not recurse into the nested
214+
`has_type`s); Lean phrases the premise as `∀ e ∈ es`, which yields the
215+
per-element IH directly, so no size measure is needed. Coq additionally
216+
treats a `map ELit`-array as a value (needing extra array-equality step
217+
rules); Lean normalises arrays to a `.vArray` literal first and reuses
218+
generic `.lit`-equality.
185219
- Coq models floats as ```value_eq_dec` is fully decidable (`Req_EM_T`),
186220
at the cost of the classical-reals axioms above. (Lean models `Float` as
187221
opaque IEEE and decides equality classically via `by_cases`.)

docs/proofs/verification/WokeLang.lean

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535
- preservation: stepping preserves types
3636
- type_safety: multi-step evaluation preserves types (by induction + preservation)
3737
- consent_monotonicity / consent_preservation: consent system properties
38+
39+
### Arrays (parity with Coq's WokeLang.v):
40+
`tArray` / `tArrayVal` type array expressions and array literal values
41+
(the mirror of Coq's `T_Array` / `T_Lit_Array`); `Step` gains `sArrayStep`
42+
(left-most-element congruence), `sArrayVal` (normalise a fully-evaluated
43+
array to a `.vArray` literal value), and `sArrayErr` (panic propagation).
44+
`progress` and `preservation` cover all three. The `∀ e ∈ es` premise shape
45+
yields a per-element induction hypothesis directly, so — unlike the Coq
46+
development — no well-founded recursion on an expression-size measure is
47+
needed: `array_split` does the pure-list decomposition and the IH answers
48+
value-or-steps per element.
3849
-/
3950

4051
namespace WokeLang
@@ -221,6 +232,18 @@ inductive HasType : TypeEnv → Expr → WokeType → Prop where
221232
HasType Γ (.unwrap e) tOk
222233
| tError : ∀ Γ msg t,
223234
HasType Γ (.error msg) t
235+
-- Arrays (mirror of Coq's `T_Array` / `T_Lit_Array` in WokeLang.v).
236+
-- `tArray` types an array *expression* elementwise; `tArrayVal` types a
237+
-- fully-evaluated array *literal value*. Both premises use the `∀ e ∈ es`
238+
-- shape, which (unlike Coq's `Forall`) yields a per-element induction
239+
-- hypothesis directly from `induction`, so the `progress`/`preservation`
240+
-- array cases need no well-founded recursion on an `expr_size` measure.
241+
| tArray : ∀ Γ es t,
242+
(∀ e, e ∈ es → HasType Γ e t) →
243+
HasType Γ (.array es) (.array t)
244+
| tArrayVal : ∀ Γ vs t,
245+
(∀ v, v ∈ vs → HasType Γ (.lit v) t) →
246+
HasType Γ (.lit (.vArray vs)) (.array t)
224247

225248
-- =========================================================================
226249
-- 4. Operational Semantics
@@ -332,6 +355,20 @@ inductive Step : Expr → Env → Expr → Env → Prop where
332355
Step (.oops (.error msg)) ρ (.error msg) ρ
333356
| sUnwrapErr : ∀ msg ρ,
334357
Step (.unwrap (.error msg)) ρ (.error msg) ρ
358+
-- Array evaluation (mirror of Coq's `S_Array_step` / `S_Array_val`, plus
359+
-- panic propagation). `sArrayStep` reduces the left-most non-value element;
360+
-- `sArrayVal` normalises a fully-evaluated array to an array literal value;
361+
-- `sArrayErr` propagates a panic out of an array, as the binop rules do.
362+
-- A fully-evaluated array is *not* an `IsValue` (it always steps via
363+
-- `sArrayVal`); the `IsValue` is the resulting `.lit (.vArray vs)`.
364+
| sArrayStep : ∀ (vs : List Value) (e e' : Expr) (es : List Expr) (ρ ρ' : Env),
365+
Step e ρ e' ρ' →
366+
Step (.array (vs.map Expr.lit ++ e :: es)) ρ
367+
(.array (vs.map Expr.lit ++ e' :: es)) ρ'
368+
| sArrayVal : ∀ (vs : List Value) (ρ : Env),
369+
Step (.array (vs.map Expr.lit)) ρ (.lit (.vArray vs)) ρ
370+
| sArrayErr : ∀ (vs : List Value) (msg : String) (es : List Expr) (ρ : Env),
371+
Step (.array (vs.map Expr.lit ++ .error msg :: es)) ρ (.error msg) ρ
335372

336373
/-- Multi-step reduction (reflexive transitive closure) -/
337374
inductive MultiStep : Expr → Env → Expr → Env → Prop where
@@ -386,6 +423,40 @@ theorem canonical_forms_result : ∀ v tOk tErr,
386423
| tOkayVal _ _ h₁ => left; exact ⟨_, rfl⟩
387424
| tOopsVal _ _ => right; exact ⟨_, rfl⟩
388425

426+
/-- `isLitB e` is `true` exactly when `e` is a literal-value expression.
427+
Used by `array_split` to locate the left-most non-literal array element. -/
428+
def isLitB : Expr → Bool
429+
| .lit _ => true
430+
| _ => false
431+
432+
theorem isLitB_true {e : Expr} : isLitB e = true → ∃ v, e = .lit v := by
433+
cases e <;> simp [isLitB]
434+
435+
theorem isLitB_false {e : Expr} : isLitB e = false → ∀ v, e ≠ .lit v := by
436+
cases e <;> simp [isLitB]
437+
438+
/-- A list of expressions is either all literal values, or splits as a prefix
439+
of literal values, a left-most non-literal element, and a remainder. This
440+
is the Lean analogue of Coq's `array_elements_progress` decomposition;
441+
here it is pure list reasoning, with the "is it a value or does it step?"
442+
question deferred to the per-element IH in the `tArray` progress case. -/
443+
theorem array_split (es : List Expr) :
444+
(∃ vs : List Value, es = vs.map Expr.lit) ∨
445+
(∃ (vs : List Value) (e : Expr) (rest : List Expr),
446+
es = vs.map Expr.lit ++ e :: rest ∧ ∀ v, e ≠ .lit v) := by
447+
induction es with
448+
| nil => left; exact ⟨[], rfl⟩
449+
| cons hd tl ih =>
450+
cases hb : isLitB hd with
451+
| true =>
452+
obtain ⟨v, rfl⟩ := isLitB_true hb
453+
cases ih with
454+
| inl h => obtain ⟨vs, rfl⟩ := h; left; exact ⟨v :: vs, rfl⟩
455+
| inr h => obtain ⟨vs, e, rest, rfl, hne⟩ := h
456+
right; exact ⟨v :: vs, e, rest, rfl, hne⟩
457+
| false =>
458+
right; exact ⟨[], hd, tl, rfl, isLitB_false hb⟩
459+
389460
/-- Progress theorem: a well-typed closed expression is either a value or can step.
390461
unwrap of an error value steps to an error expression via sUnwrapError,
391462
modelling WokeLang's panic semantics for unwrapping failures. -/
@@ -838,6 +909,32 @@ theorem progress : ∀ e t,
838909
| tError msg _ =>
839910
-- Error expressions are terminal values (panics).
840911
left; exact .error msg
912+
| tArray es t hall ih =>
913+
-- An array expression is never a value: it either normalises (all
914+
-- elements are literals ⇒ sArrayVal), propagates a panic (left-most
915+
-- non-literal is an error ⇒ sArrayErr), or steps its left-most
916+
-- non-literal element (⇒ sArrayStep). The per-element progress IH `ih`
917+
-- discharges the last two via `array_split`.
918+
right
919+
cases array_split es with
920+
| inl hsplit =>
921+
obtain ⟨vs, rfl⟩ := hsplit
922+
exact ⟨.lit (.vArray vs), emptyEnv, .sArrayVal vs emptyEnv⟩
923+
| inr hsplit =>
924+
obtain ⟨vs, e, rest, rfl, hne⟩ := hsplit
925+
have hin : e ∈ vs.map Expr.lit ++ e :: rest := by simp
926+
cases ih e hin with
927+
| inl hv =>
928+
cases hv with
929+
| lit v => exact absurd rfl (hne v)
930+
| error msg => exact ⟨.error msg, emptyEnv, .sArrayErr vs msg rest emptyEnv⟩
931+
| inr hstp =>
932+
obtain ⟨e', ρ', hs⟩ := hstp
933+
exact ⟨.array (vs.map Expr.lit ++ e' :: rest), ρ',
934+
.sArrayStep vs e e' rest emptyEnv ρ' hs⟩
935+
| tArrayVal vs t hall ih =>
936+
-- A fully-evaluated array literal `.lit (.vArray vs)` is a value.
937+
left; constructor
841938

842939
/-- Preservation theorem: if a well-typed expression steps, the result is well-typed.
843940
Proof by induction on the Step derivation with inversion on the typing derivation.
@@ -1036,6 +1133,26 @@ theorem preservation : ∀ e e' t ρ ρ',
10361133
| sUnwrapErr msg _ =>
10371134
cases ht with
10381135
| tUnwrap _ tOk tErr h₁ => exact .tError _ msg _
1136+
| sArrayStep vs e e' rest ρ ρ' hs ih =>
1137+
-- Congruence: the stepped element keeps its type (by the IH); every other
1138+
-- element keeps the witness it had from the original `tArray` derivation.
1139+
cases ht with
1140+
| tArray _ t' hall =>
1141+
refine .tArray _ _ t' (fun x hx => ?_)
1142+
rcases List.mem_append.1 hx with hpre | hcons
1143+
· exact hall x (List.mem_append.2 (Or.inl hpre))
1144+
· rcases List.mem_cons.1 hcons with rfl | htail
1145+
· exact ih t' (hall e (List.mem_append.2 (Or.inr List.mem_cons_self)))
1146+
· exact hall x (List.mem_append.2 (Or.inr (List.mem_cons_of_mem _ htail)))
1147+
| sArrayVal vs ρ =>
1148+
-- Normalisation: `.array (vs.map lit) : array t'` becomes the literal value
1149+
-- `.lit (.vArray vs) : array t'`, re-typed via `tArrayVal`.
1150+
cases ht with
1151+
| tArray _ t' hall =>
1152+
exact .tArrayVal _ vs t' (fun v hv => hall (Expr.lit v) (List.mem_map.2 ⟨v, hv, rfl⟩))
1153+
| sArrayErr vs msg rest ρ =>
1154+
-- Panic propagation: the result `.error msg` is well-typed at any type.
1155+
exact .tError _ msg _
10391156

10401157
/-- Type safety theorem -/
10411158
theorem type_safety : ∀ e t v ρ,

0 commit comments

Comments
 (0)