Perennial's goose framework and program proofs, in Lean 4 on top of
iris-lean. This branch
(lean) is the primary branch. It covers Go programs translated by goose,
without crash/recovery reasoning.
Guides: docs/PERENNIAL_PROOF_TUTORIAL.md,
docs/PERENNIAL_PROOF_REFERENCE.md,
docs/IRIS_PROOF_MODE.md.
-
One library,
Perennial. Framework directories are UpperCamelCase (Perennial/Golang/Theory/Slice.lean); generated and proof directories follow Go package paths (Perennial/Proof/github_com/tchajed/marshal.lean). -
No crash logic. There is no crash program logic (
wpc, staged invariants, recovery adequacy,crash_borrow): proofs reason about executions without crashes. GooseLang is an instance of iris-lean'sLanguage, and proofs use iris-lean'swp, which has later credits andnumLatersPerStep. The local and global state (state × GlobalState) form the single iris-leanState,CfgState. -
Bounded layer, time receipts and thread tokens. The trusted semantics
BaseStep(and its iris-lean languagegooseRealEctxiLang,GooseLang/Lang.lean) is the model of Go, but the language instance used by the program logic is a separate bounded layer (GooseLang/BoundedLang.lean): its state adds a fuel⟨steps, threads⟩. Once the step fuel is exhausted, Go instructions stutter instead of stepping; this supports time receipts (Mével, Jourdan, Pottier, ESOP 2019;GooseLang/Receipts.lean):⧗ n/⧖ n, with⧗ N ⊢ Falsefor the boundN = receiptBound GF. Once the thread fuel is exhausted,Forkstutters; a thread's exit returns thread fuel. This supports thread tokens (GooseLang/Threads.lean):threadToks n, one per live thread, withthreadToks T ⊢ Falsefor the boundT = threadBound GF, so that a counter backed by one token per pending goroutine (async.WaitGroup's) is belowT. For this the trusted semantics counts the live threads (GlobalState.threads):Fork espawnse ;; ThreadExitand increments it, and the no-opThreadExitdecrements it. The bounds are unspecified parameters, not constants: they are fields of the ghost stateReceiptGS GF/ThreadGS GF(part ofGooseGlobalGS, hence ofHeapGS), so downstream files, whose sections already assumeHeapGS, need no new argument, and the language instance and itsPureExec/Atomicinstances do not depend on them. A proof that needs a bound to be small takes a premise (wp_clock_incrinProgramLogic/TimeReceiptsTest.leantakesreceiptBound GF ≤ 2 ^ 64;wp_counter_registerinProgramLogic/ThreadTokensTest.leantakesthreadBound GF ≤ 2 ^ 31). The adequacy theorems (goose_adequacy N T,goose_invariance N T, and the grove/disk ones) hold for everyNandT: they allocate the ghost state withreceiptBound GF = NandthreadBound GF = T(hypotheses of the WP premiseHwp, which also receives the main thread's token, and from which the client discharges the proof's premises about the bounds) and are about real executions of fewer thanNsteps along which fewer thanTthreads are live, explicit hypotheses. Seedocs/PERENNIAL_PROOF_REFERENCE.md, "Time receipts" and "Thread tokens". -
Iris substrate. iris-lean provides the BI, proof mode, invariants, ghost maps, later credits and the WP. General-purpose libraries that iris-lean lacks (finite maps and sets, machine words, list lemmas) live in
Perennial/Std.- Finite maps are
Perennial.gmap K V(Perennial/Std/GMap.lean): finite partial functions, extensional so=is equality of bindings, needing onlyDecidableEq K.gset K = gmap K Unit. It is an iris-leanLawfulFiniteMap, so iris-lean'sghost_map/gen_heapapply. - Machine words are
BitVec n(w64 = BitVec 64, ...).uint.Z xis(x.toNat : Int)andsint.Z xisx.toInt. Arithmetic side conditions are discharged withword,omegaandbv_omega. Do not usebv_decide/native_decide: they trust native code (Lean.ofReduceBool); prove bitwise facts viatoNat. GoStringisList w8.
- Finite maps are
-
64-bit platform. The Go semantics assumes a 64-bit platform: the word-sized types
int,uintanduintptrare 64-bit (valuesw64).uintptr(go.UintptrSemantics,Perennial/Golang/Defn/Predeclared.lean) is an integer type likeuint64. Pointer/unsafe.Pointerto/fromuintptrconversions are not modelled: they are stuck. -
Model assumptions (overflow). Where Go panics on an overflow that a real program cannot reach (it would run out of memory first), the model assumes the overflow does not happen, with
assume(Golang/Defn/Assume.lean: anassumethat fails loops forever, so the proofs cover the executions where it holds):append's new length (sumAssumeNoOverflowSigned,Golang/Defn/Slice.lean);strings.Join's total length (TrustedCode/strings.lean).
Overflows that a program can reach are not assumed away.
sync.WaitGroup'sint32counter is one (twoAdd(1 << 30)calls panic in Go):WaitGroup.Addis Goose's translation, andWaitGroup.wp_Addasks its caller to bound the new counter by2^31. A caller whose counter is backed by one goroutine per unit discharges the bound with thread tokens (above):ntokens given < threadBound GF, and the client assumesthreadBound GF ≤ 2^31at adequacy time (ProgramLogic/ThreadTokensTest.lean,docs/PERENNIAL_PROOF_REFERENCE.md"Thread tokens"). -
Panics. A panic outcome is a value,
PanicV p, that unwinds every evaluation frame but that ofCatch e h k(GooseLang/Lang.lean);panic(p)steps to it, a function withdefers runs its body under aCatch(Golang/Defn/Defer.lean), andrecover()(Goose translates it only directly in a deferred function literal) reads and clears the panic. A postcondition receives panics too, a spec{{ P }} e {{ RET v; Q }}excludes them, and a goroutine must not panic (wp_fork,goose_adequacy_nopanic). Run-time panics of builtins (Panic "msg") are stuck. Seedocs/PERENNIAL_PROOF_REFERENCE.md, "Panics". -
Generated code comes from goose. The translator in
goose/emitsPerennial/Code/**andPerennial/GeneratedProof/**; regenerate withetc/update-goose-new.pyrather than editing them by hand.
| Directory | Contents |
|---|---|
Perennial/Std |
general libraries (gmap, words, lists, bytes) |
Perennial/Algebra |
cameras and ghost-state algebra |
Perennial/GooseLang |
GooseLang: semantics, lifting, bounded-step layer, receipts |
Perennial/GooseLang/Ffi |
grove and disk FFIs |
Perennial/Golang/Defn |
the Go model |
Perennial/Golang/Theory |
its program logic and spec lemmas |
Perennial/Ghost |
ghost-state libraries |
Perennial/TrustedCode |
trusted (hand-written) models of Go code |
Perennial/Code |
GooseLang code (generated by goose) |
Perennial/GeneratedProof |
generated proof boilerplate (generated by proofgen) |
Perennial/Proof |
hand-written program proofs |
Run etc/lean-ci.sh + etc/lean-audit.py for the build and soundness audit
(sorry roots, axioms and opaques).
- Follow the naming of the surrounding code (
wp_load,isMutex,ownSlice). Quote names that are not legal Lean identifiers with «» (e.g.«unsafe»,«_»). - Sealing:
def fooDef,@[irreducible] def foo := fooDef,theorem foo_unseal : foo = fooDef. To make an existing definition opaque, useattribute [irreducible] foo. - GooseLang code notation (
Perennial/GooseLang/Notation.lean):λ: "x", e,let: "x" := e1 in e2,e1 ;; e2,if: c then a else b,rec: "f" "x" := e, Go operatorse1 +⟨t⟩ e2etc. Method calls arercvr @!! T @!! m. - Everything lives in
namespace Perennial. - Every file is a module of Lean's module system:
module, then itspublic imports, then@[expose] public section, so that all declarations are public with their bodies exposed. - An unfinished proof is
sorry, with a comment saying what is missing if it is not obvious. Do not add newaxioms without discussing it first. - Notation:
#xisintoVal x;m !! k,<[k := v]> m,{[k := v]}work on bothgmapandList(on lists they arel[i]?andl.set i v); the set-valued domain of a map isdomSet m;go!"abc"is aGoStringliteral;l +ₗ iis location offset. - Equality on GooseLang syntax and
go.GoTypeis decided classically (noncomputable instance). FfiSyntaxrequiresPos.Countableofffi_opcode/ffi_val, andLoc,GoSlice,val,Expr,GoFunc,GoInterface,go.GoType, ... arePos.Countable(Perennial/GooseLang/Countable.lean, via an injection intoGenTree), so ghost state can store values containing code.- Check a file with
lake build Perennial.Path.To.Module(from the repo root).