Skip to content

Commit 59dd0f7

Browse files
feat(formal): real-lift R-mem (kernel) — linear memory + heap tuple round-trips (#666)
Fifth rung of the real lift (`formal/REAL-LIFT.adoc`): **linear memory** — the word-addressed heap and the deterministic cell layout (`field i at base+i`) the codegen's elaboration nodes assume, the precondition for arrays/records/strings/floats. Axiom-free. > Branch note: on `claude/lucid-cray-4a22dp-rmem` (per the per-rung branch convention, since the prior `…/-r2loops` and base branches are now protected post-merge). Diff is against current `main`, so it shows only the R-mem changes. ## What landed **Target IR (`RealWasm.v`)** - Adds the faithful `lib/wasm.ml` memory instructions `I32Load (off)` / `I32Store (off)` to `instr` (+ dead `step1` arms). **Zero blast radius** — `wexec`, `cexec`, `compile_correct`, and `compile_stmt_correct` all still compile unchanged. **R-mem kernel (new file `RealMem.v`)** - **Word-addressed memory** `mem := list Z` (each cell one i32), with `mem_get` / `mem_set` and the heap round-trip lemmas (get-after-set same/other, length) reused *directly* from RealWasm's `set_nth_eq` / `set_nth_neq` / `set_nth_length`. - A **bump allocator** `alloc n m = (length m, m ++ repeat 0 n)` (`base = current size`) with base/length lemmas. - The memory-aware executor **`mexec`** — because memory ops are *straight-line* (no nested control), `mexec` is a **structural `Fixpoint`** on the instruction list, **no fuel needed** (unlike R2's `wexec` / R2-loops' `cexec`). `I32Load off` pops addr `a`, pushes `mem[⌊a⌋+off]`; `I32Store off` pops value then addr, writes `mem[⌊a⌋+off]`; everything else defers to the memory-free `step1`. Plus the clean structural sequencing lemma `mexec_app`. - **`mexec_store_then_load`** — storing `v` at a fresh in-range cell then loading it back yields `v` (`set_nth`'s get-after-set, lifted through `mexec`). - **`mexec_pair_build_proj`** — build a 2-cell tuple at `base` (store `v0` at `base+0`, `v1` at `base+1`) then project **both** fields back: the `field i at base+i` heap layout, proved as a get-after-set round-trip (`Nat2Z.id` discharges the `Z.of_nat`/`Z.to_nat` address round-trip). ## Verification `coqc` 8.18, whole `formal/` track re-audited against current `main`: - **20 files compile**, **35 `Print Assumptions` reports**, every one *"Closed under the global context"* — **zero axioms, no `Admitted`**. ## Docs `formal/REAL-LIFT.adoc` (R-mem row ⏳ + §8 status), `formal/README.adoc` (RealMem row + "real lift R0 → R-mem"), `docs/PROOF-NEEDS.adoc` (counts 19→20 / 33→35). `_CoqProject` / `justfile` / `.hypatia-ignore` wire `RealMem.v` in. ## Remaining R-mem (next sub-rungs) Runtime `memory.grow`/`size` allocator instruction; byte-granular layout; control-bearing field expressions (fuse `mexec` with the R2-loops `cexec`); and the full source `tuple`/`array`/`record` + indexing/field-access compile-correctness. Then **R-float** / **R-str** / **R-call** per the ladder. Refs `REAL-LIFT.adoc`, `docs/PROOF-NEEDS.adoc` (K-1 row). 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KPG9mEQXFyA3k7NWAzMNMr --- _Generated by [Claude Code](https://claude.ai/code/session_01KPG9mEQXFyA3k7NWAzMNMr)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent ea496bd commit 59dd0f7

8 files changed

Lines changed: 223 additions & 15 deletions

File tree

.hypatia-ignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,3 +87,7 @@ cicd_rules/banned_language_file:formal/RealCompile.v
8787
# script, wired into formal/_CoqProject; exempt from the V-lang ban.
8888
cicd_rules/vlang_detected:formal/RealLoop.v
8989
cicd_rules/banned_language_file:formal/RealLoop.v
90+
# RealMem.v — REAL-LIFT rung R-mem (linear memory: I32Load/I32Store + heap
91+
# tuples). Coq proof script, wired into formal/_CoqProject; exempt from V-lang.
92+
cicd_rules/vlang_detected:formal/RealMem.v
93+
cicd_rules/banned_language_file:formal/RealMem.v

docs/PROOF-NEEDS.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ This is the single document that enumerates *what AffineScript must prove*, at
1212
gap between the soundness *arguments* the project relies on (prose in
1313
`docs/academic/proofs/`, the borrow-checker comments in `lib/borrow.ml`,
1414
`docs/CAPABILITY-MATRIX.adoc`) and the soundness *proofs* that are actually
15-
mechanized today: a growing axiom-free Coq core (19 files) that pins each
15+
mechanized today: a growing axiom-free Coq core (20 files) that pins each
1616
obligation on a deliberately *small model* — no full-language theorem is
1717
discharged yet (see <<status>>).
1818

@@ -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 — **19 files, 33 `Print
73+
**exists** (Coq/Rocq 8.18), axiom-free throughout — **20 files, 35 `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).
@@ -90,7 +90,11 @@ than the prose corpus suggests:
9090
branch-aware executor `cexec` over backward jumps, a source `while`/`seq`/`set`
9191
statement language, and `compile_stmt_correct` — the lowered terminating
9292
`while` simulates the reference run, settling the #601 value-returning-tail
93-
question concretely: the statement tail leaves an empty value stack).
93+
question concretely: the statement tail leaves an empty value stack) +
94+
`RealMem.v` (R-mem: word-addressed linear memory with `I32Load`/`I32Store`, a
95+
structural memory executor `mexec`, a bump allocator, and the store→load /
96+
2-cell tuple-layout round-trips — `field i at base+i` proved via `set_nth`'s
97+
get-after-set; the heap precondition for arrays/records/strings/floats).
9498
Record rows: `Rows.v` (P-11 — progress + preservation for extensible records).
9599
See <<outstanding>>, <<faces>>.
96100
* **Research tracks** (not core soundness): `docs/academic/tropical-session-types/`

formal/README.adoc

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,12 @@ cannot mistake it. Coq has no `v.mod` manifest, so `vmod_detected` never fires.
114114
empty-stack unit tail — settles #601)
115115
| **mechanized**, axiom-free
116116

117+
| `RealMem.v`
118+
| **Real lift R-mem (kernel)** — `I32Load`/`I32Store`; word-addressed memory +
119+
structural executor `mexec`; bump allocator; store→load and 2-cell
120+
tuple build+project round-trips (`field i at base+i`)
121+
| **mechanized**, axiom-free
122+
117123
| `Rows.v`
118124
| **P-11** — record-row soundness: progress + preservation for STLC + extensible
119125
records (empty/extend/select/restrict over a `lacks`-checked row), funext-free
@@ -239,11 +245,11 @@ the same canonical AST emit identical code). The observational version for the
239245
trailing-statement/tail split is mechanized separately in
240246
`invariant-path/proofs/SameCube.agda` (F-2).
241247

242-
== The real lift (R0 → R2-loops)
248+
== The real lift (R0 → R-mem)
243249

244250
The *real lift* replaces the toy stack machine with the actual `lib/wasm.ml` IR
245251
and the toy source with the real (resolved) AST, climbing a strict-superset
246-
fragment ladder. Four rungs are down:
252+
fragment ladder. Five rungs are down (R-mem in kernel form):
247253

248254
* **R0** (`RealWasm.v`) — the pure i32 numeric core (real `instr` / `value_type`
249255
names) and a stack-machine `wexec`.
@@ -271,8 +277,17 @@ fragment ladder. Four rungs are down:
271277
proves a terminating `while` simulates the reference `run_stmt`, ending with an
272278
**empty value stack** — settling the #601 value-returning-tail question
273279
(statement tail = unit) concretely.
274-
275-
The full plan — the rest of the ladder (R-mem → R-float → R-str → R-call →
280+
* **R-mem** (kernel) (`RealWasm.v` grown with `I32Load`/`I32Store`; `RealMem.v`)
281+
— linear memory. A **word-addressed** heap (`mem := list Z`, cell = i32) with a
282+
*structural* executor `mexec` (memory ops are straight-line, so no fuel) +
283+
`mexec_app`, a bump allocator (`base = current size`), and the heap round-trips:
284+
store→load (`mexec_store_then_load`) and a 2-cell tuple build+project
285+
(`mexec_pair_build_proj` — `field i at base+i`, both projections), all reusing
286+
`set_nth`'s get-after-set. Remaining R-mem: runtime `memory.grow`, byte layout,
287+
control-bearing fields (fuse `mexec` with `cexec`), and the full source
288+
tuple/array/record compile-correctness.
289+
290+
The full plan — the rest of the ladder (finish R-mem → R-float → R-str → R-call →
276291
R-wrap → R-eff), the Coq module structure, and the strategy for each hard part
277292
(linear memory, floats, the elaboration nodes, effects) — is
278293
`formal/REAL-LIFT.adoc`.

formal/REAL-LIFT.adoc

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -188,9 +188,17 @@ next. The rung names are stable references the `PROOF-NEEDS.adoc` rows will cite
188188
and its lowering's `cexec` ends with an **empty value stack** (the unit tail) —
189189
a theorem — versus the expression tail, which keeps its value on the stack.
190190

191-
| **R-mem**
192-
| Linear memory: load/store family, `MemorySize/Grow`, a byte-addressed memory
193-
model; `ExprTuple`/`ExprArray`/`ExprRecord` + indexing/field access.
191+
| **R-mem** ⏳ (kernel landed)
192+
| Linear memory. **Done (`RealMem.v`):** `I32Load`/`I32Store` added to the real
193+
`instr` (zero blast radius); a **word-addressed** memory (`mem := list Z`, cell
194+
= i32) with a structural memory executor `mexec` (straight-line ⇒ no fuel) +
195+
`mexec_app`; a bump allocator (`base = current size`); and the heap round-trips
196+
— store→load (`mexec_store_then_load`) and a 2-cell **tuple build+project**
197+
(`mexec_pair_build_proj`: `field i at base+i`, both projections) — reusing
198+
`set_nth`'s get-after-set. **Remaining:** `MemorySize`/`Grow` as a runtime
199+
allocator instruction, byte-granular layout, control-bearing field
200+
expressions (combine `mexec` with the R2-loops `cexec`), and the full source
201+
`ExprTuple`/`ExprArray`/`ExprRecord` + indexing/field-access compile-correctness.
194202
| Heap layout — the precondition for strings and the "cell" elaboration nodes.
195203

196204
| **R-float**
@@ -324,8 +332,16 @@ already has the front-end scaffolding (`F1_TransformerPreservation.v`,
324332
terminating `while` lowered to `Block[Loop[…;BrIf 1;…;Br 0]]` simulates the
325333
reference run, ending with an **empty value stack** — the unit/statement tail,
326334
settling **#601** concretely. Axiom-free; 19 files / 33 closure reports.
327-
* Next: **R-mem** — linear memory (load/store, byte model) for tuples/arrays/
328-
records; then R-float / R-str / R-call per the ladder. (`for`/`break`/
329-
`continue` and a general source-divergence model reuse the R2-loops `cexec`.)
335+
* **R-mem kernel landed** — `RealWasm.v` grown with `I32Load`/`I32Store` (zero
336+
blast radius). `RealMem.v` gives a word-addressed memory (`mem := list Z`), a
337+
*structural* memory executor `mexec` (straight-line ⇒ no fuel) + `mexec_app`, a
338+
bump allocator, and the heap round-trips: store→load (`mexec_store_then_load`)
339+
and the 2-cell tuple build+project (`mexec_pair_build_proj`, `field i at
340+
base+i`), reusing `set_nth`'s get-after-set. Axiom-free; 20 files / 35 closure
341+
reports. **Remaining R-mem:** runtime `MemorySize`/`Grow`, byte layout,
342+
control-bearing fields (fuse `mexec` with `cexec`), full source
343+
tuple/array/record compile-correctness.
344+
* Next: finish R-mem (above), then **R-float** / **R-str** / **R-call** per the
345+
ladder.
330346

331347
Tracked against `docs/PROOF-NEEDS.adoc` §6 Wave 3 and #513.

formal/RealMem.v

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
(* SPDX-License-Identifier: MPL-2.0 *)
2+
(* SPDX-FileCopyrightText: 2026 Jonathan D.A. Jewell (hyperpolymath) *)
3+
4+
(*
5+
RealMem.v
6+
═════════
7+
REAL-LIFT rung **R-mem** (see formal/REAL-LIFT.adoc): linear memory.
8+
Adds the load/store execution for `lib/wasm.ml`'s `I32Load`/`I32Store` (the
9+
instructions were added to `instr` in RealWasm.v) and a source heap layer —
10+
`tuple`/`record`-style cells laid out at consecutive addresses — with the
11+
build-then-project round-trip proved.
12+
13+
Memory is **word-addressed** (`mem := list Z`, address = cell index): the
14+
deterministic layout the codegen's elaboration nodes assume, where tuples /
15+
records / arrays are runs of i32 cells. (Byte-granular `[len][utf8]` layout
16+
and IEEE lanes are the later R-str / R-float rungs; bit-exact wrap is R-wrap.)
17+
18+
Memory ops are *straight-line* (no control), so unlike R2's `wexec` / the
19+
R2-loops `cexec`, the memory executor `mexec` is a **structural** `Fixpoint`
20+
on the instruction list — no fuel needed. The heap round-trip is then exactly
21+
`set_nth`'s get-after-set, reused from RealWasm.
22+
23+
Axiom-free, no Admitted.
24+
`.v` is Coq, not V-lang — see formal/README.adoc and .hypatia-ignore.
25+
*)
26+
27+
Require Import List.
28+
Require Import ZArith.
29+
Require Import PeanoNat.
30+
Require Import Lia.
31+
Require Import ASFormal.RealWasm.
32+
Require Import ASFormal.RealCompile.
33+
Import ListNotations.
34+
35+
(* ── word-addressed linear memory ─────────────────────────────────────────── *)
36+
Definition mem := list Z.
37+
Definition mem_get (m : mem) (a : nat) : option Z := nth_error m a.
38+
Definition mem_set (a : nat) (v : Z) (m : mem) : mem := set_nth a v m.
39+
40+
Lemma mem_set_length : forall a v m, length (mem_set a v m) = length m.
41+
Proof. intros; apply set_nth_length. Qed.
42+
43+
Lemma mem_get_set_eq : forall a v m, a < length m -> mem_get (mem_set a v m) a = Some v.
44+
Proof. intros; apply set_nth_eq; assumption. Qed.
45+
46+
Lemma mem_get_set_neq : forall a a' v m,
47+
a <> a' -> mem_get (mem_set a' v m) a = mem_get m a.
48+
Proof. intros; apply set_nth_neq; assumption. Qed.
49+
50+
(* bump allocator: base = current size; reserve n zeroed cells. *)
51+
Definition alloc (n : nat) (m : mem) : nat * mem := (length m, m ++ repeat 0%Z n).
52+
53+
Lemma alloc_base : forall n m, fst (alloc n m) = length m.
54+
Proof. reflexivity. Qed.
55+
56+
Lemma alloc_length : forall n m, length (snd (alloc n m)) = length m + n.
57+
Proof. intros; cbn. rewrite app_length, repeat_length; reflexivity. Qed.
58+
59+
(* ── memory-aware executor (structural on the instruction list) ───────────────
60+
I32Load off : pop addr a, push mem[⌊a⌋+off].
61+
I32Store off : pop value v then addr a, set mem[⌊a⌋+off] := v.
62+
everything else: the memory-free `step1` (memory unchanged). Control
63+
instructions (Block/Loop/Br/BrIf/IfElse) trap via step1 = None — combining
64+
memory with control is the next sub-rung. *)
65+
Fixpoint mexec (is : list instr) (m : mem) (lo : locals) (st : stack)
66+
: option (mem * locals * stack) :=
67+
match is with
68+
| [] => Some (m, lo, st)
69+
| a :: r =>
70+
match a with
71+
| I32Load off =>
72+
match st with
73+
| addr :: t =>
74+
match mem_get m (Z.to_nat addr + off) with
75+
| Some v => mexec r m lo (v :: t)
76+
| None => None
77+
end
78+
| [] => None
79+
end
80+
| I32Store off =>
81+
match st with
82+
| v :: addr :: t => mexec r (mem_set (Z.to_nat addr + off) v m) lo t
83+
| _ => None
84+
end
85+
| _ =>
86+
match step1 a lo st with
87+
| Some (lo', st') => mexec r m lo' st'
88+
| None => None
89+
end
90+
end
91+
end.
92+
93+
(* sequencing: straight-line, so a plain structural split (no fuel/monotonicity). *)
94+
Lemma mexec_app : forall is1 m lo st m1 lo1 st1,
95+
mexec is1 m lo st = Some (m1, lo1, st1) ->
96+
forall is2,
97+
mexec (is1 ++ is2) m lo st = mexec is2 m1 lo1 st1.
98+
Proof.
99+
induction is1 as [| a r IH]; intros m lo st m1 lo1 st1 H is2.
100+
- cbn in H. injection H as -> -> ->. reflexivity.
101+
- cbn [app]. destruct a;
102+
try (cbn [mexec] in H |- *; destruct (step1 _ lo st) as [[lo' st']|];
103+
[ apply IH; exact H | discriminate H ]).
104+
+ (* I32Load *)
105+
cbn [mexec] in H |- *. destruct st as [| addr t]; [discriminate H|].
106+
destruct (mem_get m (Z.to_nat addr + off)) as [v|]; [| discriminate H].
107+
apply IH; exact H.
108+
+ (* I32Store *)
109+
cbn [mexec] in H |- *. destruct st as [| v [| addr t]]; try discriminate H.
110+
apply IH; exact H.
111+
Qed.
112+
113+
(* ── concrete sanity: store then load the same cell round-trips ─────────────── *)
114+
Example mexec_store_load_demo :
115+
(* mem = [0;0;0]; store 42 at addr 1, then load addr 1 ⇒ 42 *)
116+
mexec [I32Const 1; I32Const 42; I32Store 0; I32Const 1; I32Load 0]
117+
[0;0;0]%Z [] []
118+
= Some (mem_set 1 42 [0;0;0]%Z, [], [42%Z]).
119+
Proof. reflexivity. Qed.
120+
121+
(* the executor-level store→load round-trip: storing v at a fresh in-range cell
122+
then loading it back yields v (this is `set_nth`'s get-after-set, lifted
123+
through `mexec`). *)
124+
Lemma mexec_store_then_load : forall m a v,
125+
a < length m ->
126+
mexec [I32Const (Z.of_nat a); I32Const v; I32Store 0;
127+
I32Const (Z.of_nat a); I32Load 0] m [] []
128+
= Some (mem_set a v m, [], [v]).
129+
Proof.
130+
intros m a v Ha.
131+
cbn [mexec step1].
132+
rewrite Nat.add_0_r, Nat2Z.id.
133+
rewrite mem_get_set_eq by exact Ha.
134+
reflexivity.
135+
Qed.
136+
137+
(* ── heap tuples: build a 2-cell tuple at `base`, then project both fields ────
138+
This is the codegen's deterministic cell layout (field i at base+i) proved as
139+
a get-after-set round-trip — the precondition for arrays / records / strings.
140+
(`base` is the address the bump allocator hands out; a runtime `memory.grow`
141+
that produces it, and field expressions with control, are the next sub-rung.) *)
142+
Theorem mexec_pair_build_proj : forall m base v0 v1,
143+
base + 1 < length m ->
144+
exists m',
145+
(* build: store v0 at base+0, v1 at base+1 *)
146+
mexec [I32Const (Z.of_nat base); I32Const v0; I32Store 0;
147+
I32Const (Z.of_nat base); I32Const v1; I32Store 1] m [] []
148+
= Some (m', [], [])
149+
(* project field 0 ⇒ v0 *)
150+
/\ mexec [I32Const (Z.of_nat base); I32Load 0] m' [] [] = Some (m', [], [v0])
151+
(* project field 1 ⇒ v1 *)
152+
/\ mexec [I32Const (Z.of_nat base); I32Load 1] m' [] [] = Some (m', [], [v1]).
153+
Proof.
154+
intros m base v0 v1 Hb.
155+
exists (mem_set (base + 1) v1 (mem_set base v0 m)). split; [| split].
156+
- cbn [mexec step1]. rewrite !Nat2Z.id, Nat.add_0_r. reflexivity.
157+
- cbn [mexec step1]. rewrite Nat2Z.id, Nat.add_0_r.
158+
rewrite mem_get_set_neq by lia.
159+
rewrite mem_get_set_eq by lia. reflexivity.
160+
- cbn [mexec step1]. rewrite Nat2Z.id.
161+
rewrite mem_get_set_eq by (rewrite mem_set_length; lia). reflexivity.
162+
Qed.
163+
164+
Print Assumptions mexec_store_then_load.
165+
Print Assumptions mexec_pair_build_proj.

formal/RealWasm.v

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ Inductive instr :=
4545
| Block (body : list instr) (* R2-loops: forward label — Br exits past it *)
4646
| Loop (body : list instr) (* R2-loops: backward label — Br re-enters it *)
4747
| Br (k : nat) (* R2-loops: branch to the k-th enclosing label *)
48-
| BrIf (k : nat). (* R2-loops: pop; if nonzero, branch to label k *)
48+
| BrIf (k : nat) (* R2-loops: pop; if nonzero, branch to label k *)
49+
| I32Load (off : nat) (* R-mem: pop addr, push mem[addr+off] (word) *)
50+
| I32Store (off : nat). (* R-mem: pop val then addr, mem[addr+off]:=val *)
4951

5052
Definition stack := list Z.
5153
Definition locals := list Z.
@@ -97,6 +99,7 @@ Definition step1 (i : instr) (lo : locals) (st : stack) : option (locals * stack
9799
end
98100
| IfElse _ _ => None
99101
| Block _ | Loop _ | Br _ | BrIf _ => None (* structured control: cexec (RealLoop), not step1 *)
102+
| I32Load _ | I32Store _ => None (* memory ops: mexec (RealMem), not step1 *)
100103
end.
101104

102105
(* fuel-indexed executor. None = trap OR out of fuel. *)

formal/_CoqProject

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,4 +17,5 @@ F5_RenderFaithful.v
1717
RealWasm.v
1818
RealCompile.v
1919
RealLoop.v
20+
RealMem.v
2021
Rows.v

formal/justfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ check:
1313
F1_TransformerPreservation F3_PragmaDecidable F4_ErrorFaithful \
1414
P3_BorrowSound P3_BorrowGraph P2_Progress P2_Stlc \
1515
QttSemiring AffineUsage QttTyping QttDynamic \
16-
F5_RenderFaithful RealWasm RealCompile RealLoop Rows; do
16+
F5_RenderFaithful RealWasm RealCompile RealLoop RealMem Rows; do
1717
echo "== coqc $f.v =="
1818
o="$(coqc -Q . ASFormal "$f.v")"
1919
printf '%s\n' "$o"

0 commit comments

Comments
 (0)