Skip to content

Commit 9f5192e

Browse files
authored
Integrate Iris small-step Wasm semantics (#161)
* Integrate Iris small-step Wasm semantics * update justfile * Verify GCD and merge sort examples * Clean merge sort proof warnings * Keep CodeLib examples out of downstream imports * Split Lean CI proof builds * Run long Lean builds on macOS runners * Update GitHub actions to Node 24
1 parent 61d9491 commit 9f5192e

116 files changed

Lines changed: 51773 additions & 6541 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/lean_action_ci.yml

Lines changed: 26 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,29 @@ on:
77
workflow_dispatch:
88

99
jobs:
10-
build:
11-
name: Build & verify proofs
12-
runs-on: ubuntu-latest
10+
codelib:
11+
name: Build CodeLib proofs
12+
runs-on: macos-latest
13+
steps:
14+
- uses: actions/checkout@v5
15+
- uses: actions/cache@v5
16+
with:
17+
path: .lake/packages
18+
key: lake-pkgs-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('interpreter/lake-manifest.json') }}
19+
restore-keys: |
20+
lake-pkgs-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-
21+
- uses: leanprover/lean-action@v1
22+
with:
23+
lake-package-directory: codelib
24+
build-args: "--wfail"
25+
use-mathlib-cache: true
26+
27+
programs:
28+
name: Build program proofs
29+
runs-on: macos-latest
1330
steps:
14-
- uses: actions/checkout@v4
15-
- uses: actions/cache@v4
31+
- uses: actions/checkout@v5
32+
- uses: actions/cache@v5
1633
with:
1734
path: .lake/packages
1835
key: lake-pkgs-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('interpreter/lake-manifest.json') }}
@@ -21,27 +38,16 @@ jobs:
2138
- uses: leanprover/lean-action@v1
2239
with:
2340
lake-package-directory: programs/lean
41+
build-args: "Project --wfail"
2442
use-mathlib-cache: true
25-
- name: Fail on build warnings
26-
working-directory: programs/lean
27-
run: |
28-
set -o pipefail
29-
# Rebuild (cached) and capture output. Warnings (e.g. `sorry`,
30-
# unused-variable lints) are re-emitted on every build but do not
31-
# change lake's exit code, so scan the log and fail if any appear.
32-
lake build 2>&1 | tee build.log
33-
if grep -E '(^|[[:space:]])warning:' build.log; then
34-
echo "::error::Build produced warnings; warnings are not allowed."
35-
exit 1
36-
fi
3743

3844
test:
3945
name: Smoke test
4046
runs-on: ubuntu-latest
41-
needs: build
47+
needs: [codelib, programs]
4248
steps:
43-
- uses: actions/checkout@v4
44-
- uses: actions/cache@v4
49+
- uses: actions/checkout@v5
50+
- uses: actions/cache@v5
4551
with:
4652
path: .lake/packages
4753
key: lake-pkgs-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('interpreter/lake-manifest.json') }}

.github/workflows/testsuite-report.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,11 @@ jobs:
1313
name: Check testsuite_report.txt is up to date
1414
runs-on: ubuntu-latest
1515
steps:
16-
- uses: actions/checkout@v4
16+
- uses: actions/checkout@v5
1717
with:
1818
submodules: true
1919

20-
- uses: actions/cache@v4
20+
- uses: actions/cache@v5
2121
with:
2222
path: .lake/packages
2323
key: lake-pkgs-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('lean-toolchain') }}-${{ hashFiles('interpreter/lake-manifest.json') }}

.github/workflows/verifier-freshness.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ jobs:
1111
name: Programs up to date
1212
runs-on: ubuntu-latest
1313
steps:
14-
- uses: actions/checkout@v4
14+
- uses: actions/checkout@v5
1515

1616
# The verifier executable doesn't need Mathlib (see PR #12), so we
1717
# skip the mathlib cache entirely.

.github/workflows/verifier-report-preview.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ jobs:
2727
VERCEL_ORG_ID: ${{ secrets.VERCEL_ORG_ID }}
2828
VERCEL_PROJECT_ID: ${{ secrets.VERCEL_PROJECT_ID }}
2929
steps:
30-
- uses: actions/checkout@v4
30+
- uses: actions/checkout@v5
3131
with:
3232
# `verifier extract` records the repo commit; full history isn't
3333
# needed but a real .git is.

IirisMigration.md

Lines changed: 633 additions & 6 deletions
Large diffs are not rendered by default.

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ See [`interpreter/samples/factorial.wat`](interpreter/samples/factorial.wat) for
4545

4646
**Prove something about it:**
4747

48-
[`interpreter/Interpreter/Wasm/Examples/Factorial.lean`](interpreter/Interpreter/Wasm/Examples/Factorial.lean) shows a complete correctness proof using the WP tactic layer.
48+
[`interpreter/Interpreter/Wasm/Examples/Factorial.lean`](interpreter/Interpreter/Wasm/Examples/Factorial.lean) shows a complete correctness proof by composing instruction-granular small-step traces.
4949

5050
## Repository layout
5151

codelib/CodeLib.lean

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,18 @@ import CodeLib.RustStd.U64.Shr
2626
import CodeLib.RustStd.Array.Basic
2727
import CodeLib.RustStd.Array.Len
2828
import CodeLib.RustStd.Array.IsEmpty
29+
import CodeLib.RustStd.Array.SmallStep
2930
import CodeLib.RustStd.Option
3031
import CodeLib.Near.State
3132
import CodeLib.Near.Env
3233
import CodeLib.Near.Proof
3334
import CodeLib.IEEE32.Exec
3435
import CodeLib.SepLogic.WasmHeap
3536
import CodeLib.SepLogic.WasmRules
36-
import CodeLib.SepLogic.WasmWP
37-
import CodeLib.SepLogic.Adequacy
37+
import CodeLib.SepLogic.SmallStepLanguage
38+
import CodeLib.SepLogic.SmallStepState
39+
import CodeLib.SepLogic.SmallStepLifting
40+
import CodeLib.SepLogic.SmallStepAdequacy
3841

3942
/-!
4043
# CodeLib — umbrella import for downstream code

codelib/CodeLib/Equivalence.lean

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,4 +189,157 @@ theorem ObservationallyEquiv.trans {env : HostEnv α}
189189
ObservationallyEquiv env m₁ id₁ m₃ id₃ initial args :=
190190
ObservationallyEquivOn.trans h₁₂ h₂₃
191191

192+
/-! ## Authoritative small-step equivalence
193+
194+
This is the cutover form of the relation above. It observes finite executions
195+
of `SmallStep.Step` directly and therefore has no fuel or dependency on the
196+
legacy `run` function. -/
197+
198+
namespace SmallStep
199+
200+
/-- Terminal outcomes observed by equivalence. Traps remain structural and
201+
are not collapsed into divergence. -/
202+
inductive ObservableOutcome where
203+
| done (values : List Value)
204+
| trapped (reason : TrapReason)
205+
deriving BEq, Repr
206+
207+
def ObservableOutcome.toExpr : ObservableOutcome → Expr α
208+
| .done values => .done values
209+
| .trapped reason => .trapped reason
210+
211+
/-- A finite authoritative trace reaches a particular terminal outcome. -/
212+
def Reaches (config : Config α)
213+
(outcome : ObservableOutcome) (store : MachineStore α) : Prop :=
214+
∃ trace, Steps config trace ⟨outcome.toExpr, store⟩
215+
216+
/-- Two initialized configurations have exactly the same successful or
217+
trapping observable outcomes. -/
218+
def ObservationallyEquivOn
219+
(config₁ config₂ : Config α) (obs : MachineStore α → β) : Prop :=
220+
∀ (outcome : ObservableOutcome) (o : β),
221+
(∃ store, Reaches config₁ outcome store ∧ obs store = o) ↔
222+
(∃ store, Reaches config₂ outcome store ∧ obs store = o)
223+
224+
private theorem ObservableOutcome.toExpr_injective :
225+
Function.Injective (ObservableOutcome.toExpr : ObservableOutcome → Expr α) := by
226+
intro first second heq
227+
cases first <;> cases second <;>
228+
simp_all [ObservableOutcome.toExpr]
229+
230+
/-- Determinism and terminal irreducibility make a reached outcome unique. -/
231+
theorem Reaches.outcome_unique_on
232+
{config : Config α} {obs : MachineStore α → β}
233+
{outcome outcome' : ObservableOutcome}
234+
{store store' : MachineStore α} {o o' : β}
235+
(first : Reaches config outcome store) (hfirst : obs store = o)
236+
(second : Reaches config outcome' store') (hsecond : obs store' = o') :
237+
outcome = outcome' ∧ o = o' := by
238+
obtain ⟨trace, execution⟩ := first
239+
obtain ⟨trace', execution'⟩ := second
240+
have terminal (terminalOutcome : ObservableOutcome)
241+
(kind : StepKind) (next : Config α) :
242+
¬Step ⟨terminalOutcome.toExpr, store⟩ kind next := by
243+
cases terminalOutcome with
244+
| done => exact done_terminal
245+
| trapped => exact trapped_terminal
246+
have terminal' (terminalOutcome : ObservableOutcome)
247+
(kind : StepKind) (next : Config α) :
248+
¬Step ⟨terminalOutcome.toExpr, store'⟩ kind next := by
249+
cases terminalOutcome with
250+
| done => exact done_terminal
251+
| trapped => exact trapped_terminal
252+
have hconfig := steps_irreducible_deterministic execution execution'
253+
(terminal outcome) (terminal' outcome')
254+
have hparts := Config.mk.inj hconfig
255+
have houtcome := ObservableOutcome.toExpr_injective hparts.1
256+
exact
257+
⟨houtcome,
258+
hfirst.symm.trans ((congrArg obs hparts.2).trans hsecond)⟩
259+
260+
/-- A common reached terminal outcome discharges equivalence. -/
261+
theorem ObservationallyEquivOn.of_common_reached
262+
{config₁ config₂ : Config α} {obs : MachineStore α → β}
263+
{outcome : ObservableOutcome} {store₁ store₂ : MachineStore α} {o : β}
264+
(first : Reaches config₁ outcome store₁) (hfirst : obs store₁ = o)
265+
(second : Reaches config₂ outcome store₂) (hsecond : obs store₂ = o) :
266+
ObservationallyEquivOn config₁ config₂ obs := by
267+
intro candidate observed
268+
constructor
269+
· rintro ⟨store, reached, hobs⟩
270+
obtain ⟨rfl, rfl⟩ :=
271+
Reaches.outcome_unique_on reached hobs first hfirst
272+
exact ⟨store₂, second, hsecond⟩
273+
· rintro ⟨store, reached, hobs⟩
274+
obtain ⟨rfl, rfl⟩ :=
275+
Reaches.outcome_unique_on reached hobs second hsecond
276+
exact ⟨store₁, first, hfirst⟩
277+
278+
/-- A common normally terminating result is the main success-specialized
279+
discharge rule. -/
280+
theorem ObservationallyEquivOn.of_common_outcome
281+
{config₁ config₂ : Config α} {obs : MachineStore α → β}
282+
{r : List Value} {o : β}
283+
(first : TerminatesWith config₁
284+
(fun values store => values = r ∧ obs store = o))
285+
(second : TerminatesWith config₂
286+
(fun values store => values = r ∧ obs store = o)) :
287+
ObservationallyEquivOn config₁ config₂ obs := by
288+
obtain ⟨trace₁, values₁, store₁, execution₁, hvalues₁, hobs₁⟩ := first
289+
obtain ⟨trace₂, values₂, store₂, execution₂, hvalues₂, hobs₂⟩ := second
290+
subst values₁
291+
subst values₂
292+
apply ObservationallyEquivOn.of_common_reached
293+
(outcome := .done r) (store₁ := store₁) (store₂ := store₂)
294+
· exact ⟨trace₁, execution₁⟩
295+
· exact hobs₁
296+
· exact ⟨trace₂, execution₂⟩
297+
· exact hobs₂
298+
299+
/-- A common structural trap is an observable common outcome. -/
300+
theorem ObservationallyEquivOn.of_common_trap
301+
{config₁ config₂ : Config α} {obs : MachineStore α → β}
302+
{reason : TrapReason} {store₁ store₂ : MachineStore α} {o : β}
303+
(first : Reaches config₁ (.trapped reason) store₁)
304+
(hfirst : obs store₁ = o)
305+
(second : Reaches config₂ (.trapped reason) store₂)
306+
(hsecond : obs store₂ = o) :
307+
ObservationallyEquivOn config₁ config₂ obs :=
308+
ObservationallyEquivOn.of_common_reached first hfirst second hsecond
309+
310+
theorem ObservationallyEquivOn.refl
311+
(config : Config α) (obs : MachineStore α → β) :
312+
ObservationallyEquivOn config config obs :=
313+
fun _ _ => Iff.rfl
314+
315+
theorem ObservationallyEquivOn.symm
316+
{config₁ config₂ : Config α} {obs : MachineStore α → β}
317+
(equivalent : ObservationallyEquivOn config₁ config₂ obs) :
318+
ObservationallyEquivOn config₂ config₁ obs :=
319+
fun outcome observed => (equivalent outcome observed).symm
320+
321+
theorem ObservationallyEquivOn.trans
322+
{config₁ config₂ config₃ : Config α} {obs : MachineStore α → β}
323+
(first : ObservationallyEquivOn config₁ config₂ obs)
324+
(second : ObservationallyEquivOn config₂ config₃ obs) :
325+
ObservationallyEquivOn config₁ config₃ obs :=
326+
fun outcome observed =>
327+
(first outcome observed).trans (second outcome observed)
328+
329+
/-- Host-state observation, deliberately ignoring module-private scratch
330+
memory as in the legacy relation. -/
331+
def ObservationallyEquiv (config₁ config₂ : Config α) : Prop :=
332+
ObservationallyEquivOn config₁ config₂ (fun store => store.wasm.host)
333+
334+
theorem ObservationallyEquiv.of_common_outcome
335+
{config₁ config₂ : Config α} {r : List Value} {h : α}
336+
(first : TerminatesWith config₁
337+
(fun values store => values = r ∧ store.wasm.host = h))
338+
(second : TerminatesWith config₂
339+
(fun values store => values = r ∧ store.wasm.host = h)) :
340+
ObservationallyEquiv config₁ config₂ :=
341+
ObservationallyEquivOn.of_common_outcome first second
342+
343+
end SmallStep
344+
192345
end Wasm

0 commit comments

Comments
 (0)