The Iris Tutorial in Lean

5.2. Weakest Precondition🔗

The first construct for specifying program behaviour is the weakest precondition. In Iris, a weakest precondition has the form WP e {{ v, Φ v }}. This asserts that if the HeapLang program e terminates at some value v, then v satisfies the predicate Φ. The double curly brackets are the postcondition.

The Rocq tutorial begins with a pure arithmetic example #1 + #2 * #3 + #4 + #5 and uses the wp_op/wp_pure/wp_pures tactics to symbolically execute it. The Lean version of these tactics is wp_pure/wp_pures, which iterate the wp_pure_step_fupd rule. However, iris-lean's HeapLang only provides PureExec instances for β-reduction, if/case/inj, pair projections, and on integers. The arithmetic operators have no PureExec instance, so wp_pure cannot fire on #2 * #3 or any of the additions.

-- TODO (upstream — iris-lean): register `PureExec` instances for
-- the arithmetic binops on `BaseLit.int` (and ideally also on
-- bit-wise / shift operators). Until then `arith_spec` cannot be
-- proved in the natural way.

-- Rocq tutorial reference:
--   Example arith : expr := #1 + #2 * #3 + #4 + #5.
--   Lemma arith_spec : ⊢ WP arith {{ v, ⌜v = #16⌝ }}.
--   Proof. rewrite /arith. wp_op. wp_pure. wp_pures.
--          iModIntro. iPureIntro. reflexivity. Qed.

To showcase the tactics that do work, here is the same shape of proof on a program that only uses β-reduction and if:

def boolish : Exp := hl( (λ b, if b then #1 else #0) #true) theorem boolish_spec : ⊢@{IProp GF} WP boolish {{ v, v = hl_val(#1) }} := hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GF WP boolish {{ v, v = hl_val(#1) }} hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GF WP hl(let b := #true; if b then #1 else #0) {{ v, v = hl_val(#1) }} hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GF |={}=> hl_val(#1) = hl_val(#1) All goals completed! 🐙

The tactic wp_pures symbolically executes all pure steps for which a PureExec instance exists. Each step internally applies the wp_pure_step_fupd rule from Iris.ProgramLogic.Lifting, which says (informally):

e₁ →pure e₂
──────────────────────────────────
WP e₂ {{ Φ }} ⊢ WP e₁ {{ Φ }}

After every pure step has fired, the goal is reduced to proving the postcondition behind a fancy-update modality |={⊤}=>; itrivial discharges the residual proposition ⌜#1 = #1⌝.

The lambda program from the previous chapter mixes lambda application with arithmetic. Because of the same PureExec gap, we cannot symbolically evaluate it end-to-end in iris-lean today.

def lambda : Exp := hl(let add5 := (λ x, x + #5); let double := (λ x, x * #2); let compose := (λ f g, λ x, g (f x)); compose add5 double #5)
-- TODO (upstream — iris-lean): same as above — `lambda` contains
-- `+` and `*`. The lambda β-reductions all work via
-- `wp_pure`/`wp_pures`, but the arithmetic reductions do not.

-- Rocq tutorial reference:
--   Lemma lambda_spec : ⊢ WP lambda {{ v, ⌜v = #20⌝ }}.
--   Proof. rewrite /lambda. wp_pures. done. Qed.

A purely β-driven analogue does prove cleanly:

def hofun : Exp := hl(let myId := (λ x, x); let myApply := (λ f x, f x); myApply myId #5) theorem hofun_spec : ⊢@{IProp GF} WP hofun {{ v, v = hl_val(#5) }} := hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GF WP hofun {{ v, v = hl_val(#5) }} hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GF WP hl(let myId := (λ x, x); let myApply := (λ f x, f x); myApply myId #5) {{ v, v = hl_val(#5) }} hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GF |={}=> hl_val(#5) = hl_val(#5) All goals completed! 🐙