6.3. Examples of Persistent Propositions
Thus far, the only basic persistent propositions we have seen are pure propositions, such as equalities. The Rocq tutorial showcases two additional examples: Hoare triples and persistent points-to predicates. The points-to part we can port; the Hoare-triples part depends on an upstream gap.
6.3.1. Hoare Triples
All Hoare triples are persistent. This is because Hoare triples in Iris are defined as
□ (∀ Φ, P -∗ ▷ (∀ x .. y, Q -∗ Φ v) -∗ WP e {{ w, Φ w }})
— with an outermost □, so Hoare triples can be duplicated and
reused. Intuitively, a Hoare triple {{{ P }}} e {{{ Φ }}} does
not claim ownership of any resources; it merely states that if
we own the resources described by P, then we can safely run e,
and we get the resources described by Φ if it terminates. If we
can get ownership of those resources multiple times, we should be
able to run e multiple times.
-- TODO (upstream — iris-lean): the example below relies on the
-- Hoare-triple notation `{{{ P }}} e {{{ ... }}}` (missing) and
-- on a `wp_apply` convenience tactic (also missing).
-- Rocq tutorial reference:
-- Example counter (inc : val) : expr :=
-- let: "c" := ref #0 in
-- inc "c" ;; inc "c" ;; !"c".
--
-- Lemma counter_spec (inc : val) :
-- {{{ ∀ (l : loc) (z : Z),
-- {{{ l ↦ #z }}} inc #l {{{ v, RET v; l ↦ #(z + 1) }}} }}}
-- counter inc
-- {{{ v, RET v; ⌜v = #2⌝ }}}.
-- Proof.
-- iIntros (Φ) "#Hinc_spec HΦ".
-- rewrite /counter.
-- wp_alloc l as "Hl". wp_let.
-- wp_apply ("Hinc_spec" with "Hl"). iIntros (v) "Hl". wp_seq.
-- wp_apply ("Hinc_spec" with "Hl"). iIntros (v') "Hl". wp_seq.
-- wp_load. by iApply "HΦ".
-- Qed.
6.3.2. Persistent Points-to
The resource of heaps is more sophisticated than what we have been
letting on. The general shape of a points-to predicate is actually
l ↦{dq} v, where dq is a discarded fraction
(iris-lean's DFrac: either .own q for a real fraction
q ∈ (0,1], or .discard for the persistent variant). The
predicate l ↦ v is shorthand for l ↦{DFrac.own 1} v. The basic
idea is that points-to predicates can be split up and recombined,
allowing ownership of points-to predicates to be shared. iris-lean
provides this through the Fractional / AsFractional
typeclasses.
-- TODO (upstream — iris-lean): the proof below uses `pt_split`
-- with the fractional notation `l ↦{# 1/2} v` (Rocq syntax). In
-- iris-lean the spelling is `l ↦{.own (1/2)} v` and the
-- `iCombine`/`iDestruct` automation for fractional splits is
-- still maturing. The point is that
-- `pointsto_fractional` / `pointsto_combine` already exist as
-- lemmas (see `Iris.BI.Lib.GenHeap`); a Rocq-style ergonomic
-- port awaits the iris-lean `iCombine` and fractional-pattern
-- tactics.
-- Rocq tutorial reference:
-- Lemma pt_split l v : l ↦ v ⊣⊢ (l ↦{# 1/2 } v) ∗ (l ↦{# 1/2 } v).
-- Proof.
-- iSplit.
-- - iIntros "Hl".
-- iDestruct "Hl" as "[Hl1 Hl2]".
-- iFrame.
-- - iIntros "[Hl1 Hl2]".
-- iCombine "Hl1" "Hl2" as "Hl".
-- iFrame.
-- Qed.
Crucially, a store operation can only take place if the entire
fraction is owned, i.e. dq = .own 1. However, load operations
can occur for any fraction. Fractional points-to predicates are
especially useful in scenarios where a location is read by
multiple threads in parallel but later only used by a single
thread.
-- TODO (upstream — iris-lean): `par_read_write` requires
-- ergonomic fractional-points-to manipulation (splitting `l ↦ v`
-- into `l ↦{.own (1/2)} v ∗ l ↦{.own (1/2)} v` and recombining
-- after the parallel composition). The underlying lemmas
-- (`pointsto_fractional`, `pointsto_combine`) are present in
-- `Iris.BI.Lib.GenHeap`, but `iCombine`-style automation is not
-- yet there. `wp_par` itself works fine (see Specifications
-- chapter); the missing piece is the fractional bookkeeping.
-- Rocq tutorial reference:
-- Example par_read_write (l : loc) : expr :=
-- let: "r" := (!#l ||| !#l) in
-- #l <- #5.
-- Lemma par_read_write_spec (l : loc) (v : val) :
-- {{{ l ↦ v }}}
-- par_read_write l
-- {{{ RET #(); l ↦ #5 }}}.
-- Proof. ... uses (wp_par t_post t_post) ... Qed.
If one owns a fraction of a points-to predicate, one can decide to
discard the fraction. This means that it is no longer possible
to recombine points-to predicates to get the full fraction. As
such, the value in the points-to predicate can never be changed
again — the location has become read-only. iris-lean writes the
persistent points-to as l ↦{.discard} v (the Rocq tutorial writes
l ↦□ v). It is persistent.
The lemma that makes a points-to persistent is
Iris.pointsTo_persist (Rocq alias pointsto_persist):
⊢@{IProp GF} l ↦{dq} v ==∗ l ↦{.discard} v
There are some caveats as to when we can discard fractions; the
proposition P ==∗ Q is equivalent to P -∗ |==> Q, where |==>
is the update modality. The imod tactic can usually remove
this modality, e.g. when the goal is a weakest precondition.
theorem pt_persist (l : Loc) (v : Val) :
l ↦ some v -∗
WP hl(!v(#l))
{{ w, ⌜w = v⌝ ∗ l ↦{.discard} some v }} := hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GFl:Locv:Val⊢ ⊢ l ↦ some v -∗ WP hl(!#l) {{ w, ⌜w = v⌝ ∗ l ↦{DFrac.discard} some v }}
hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GFl:Locv:Val⊢
∗Hl : l ↦ some v
⊢ WP hl(!#l) {{ w, ⌜w = v⌝ ∗ l ↦{DFrac.discard} some v }}
hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GFl:Locv:Val⊢
∗Hl' : l ↦{DFrac.discard} some v
⊢ WP hl(!#l) {{ w, ⌜w = v⌝ ∗ l ↦{DFrac.discard} some v }}
hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GFl:Locv:Val⊢
⊢ ▷ (l ↦{DFrac.discard} some v -∗ ⌜v = v⌝ ∗ l ↦{DFrac.discard} some v)
hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GFl:Locv:Val⊢
∗Hl' : l ↦{DFrac.discard} some v
⊢ ⌜v = v⌝ ∗ l ↦{DFrac.discard} some v
hlc:HasLCGF:BundledGFunctorsinst✝:HeapLangGS hlc GFl:Locv:Val⊢
⊢ ⌜v = v⌝
All goals completed! 🐙
Note: iris-lean does not yet register Persistent (l ↦{.discard} v)
as an instance, so a discarded points-to cannot be moved into the
intuitionistic context with #. The lemmas treat ↦{.discard} as
a (duplicable, but spatially-managed) hypothesis. Once the
Persistent instance lands, the proof above will be eligible for
the cleaner imod ... with #Hl' pattern.
As a more elaborate example, here is a parallel program where two
threads both read from the same location. The key idea is that
once the points-to is made persistent (↦{.discard}), it can be
shared across both threads. (The Rocq tutorial additionally wraps
the result in arithmetic computing 21 * 2 = 42; we drop that
because iris-lean still lacks PureExec for + / *.)
section ParRead
variable [Spawn.SpawnG GF]
open Iris.HeapLang.Par
def parRead (l : Loc) : Exp := hl((!v(#l)) ‖ (!v(#l)))
theorem parRead_spec (l : Loc) (v : Val) :
l ↦{.discard} some v ∗ l ↦{.discard} some v -∗
WP (parRead l) {{ w, ⌜w = hl_val((&v, &v))⌝ }} := hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢ ⊢ l ↦{DFrac.discard} some v ∗ l ↦{DFrac.discard} some v -∗ WP (parRead l) {{ w, ⌜w = hl_val((&v, &v))⌝ }}
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗Hl1 : l ↦{DFrac.discard} some v
∗Hl2 : l ↦{DFrac.discard} some v
⊢ WP (parRead l) {{ w, ⌜w = hl_val((&v, &v))⌝ }}
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗Hl1 : l ↦{DFrac.discard} some v
∗Hl2 : l ↦{DFrac.discard} some v
⊢ WP hl(v(&par) (v(λ _, !#l)) (v(λ _, !#l))) {{ w, ⌜w = hl_val((&v, &v))⌝ }}
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗Hl1 : l ↦{DFrac.discard} some v
⊢ WP hl(!#l) {{ w, ⌜w = v⌝ }}hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗Hl2 : l ↦{DFrac.discard} some v
⊢ WP hl(!#l) {{ w, ⌜w = v⌝ }}hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
⊢ ∀ v1 v2, ⌜v1 = v⌝ ∗ ⌜v2 = v⌝ -∗ ▷ ⌜hl_val((&v1, &v2)) = hl_val((&v, &v))⌝
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗Hl1 : l ↦{DFrac.discard} some v
⊢ WP hl(!#l) {{ w, ⌜w = v⌝ }} hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
⊢ ▷ (l ↦{DFrac.discard} some v -∗ ⌜v = v⌝)
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗_Hl : l ↦{DFrac.discard} some v
⊢ ⌜v = v⌝
All goals completed! 🐙
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗Hl2 : l ↦{DFrac.discard} some v
⊢ WP hl(!#l) {{ w, ⌜w = v⌝ }} hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
⊢ ▷ (l ↦{DFrac.discard} some v -∗ ⌜v = v⌝)
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
∗_Hl : l ↦{DFrac.discard} some v
⊢ ⌜v = v⌝
All goals completed! 🐙
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Val⊢
⊢ ∀ v1 v2, ⌜v1 = v⌝ ∗ ⌜v2 = v⌝ -∗ ▷ ⌜hl_val((&v1, &v2)) = hl_val((&v, &v))⌝ hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locv:Valw1:Valw2:ValH1:w1 = vH2:w2 = v⊢
⊢ ▷ ⌜hl_val((&w1, &w2)) = hl_val((&v, &v))⌝
hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locw1:Valw2:ValH2:w2 = w1⊢
⊢ ▷ ⌜hl_val((&w1, &w2)) = hl_val((&w1, &w1))⌝; hlc:HasLCGF:BundledGFunctorsinst✝¹:HeapLangGS hlc GFinst✝:Spawn.SpawnG GFl:Locw2:Val⊢
⊢ ▷ ⌜hl_val((&w2, &w2)) = hl_val((&w2, &w2))⌝
All goals completed! 🐙
end ParRead
Ideally we would write the precondition as a single
l ↦{.discard} some v and let iris-lean duplicate it across both
threads automatically. That requires registering a
Persistent (l ↦{.discard} v) instance — see TODO below.
-- TODO (upstream — iris-lean): register a
-- `Persistent (l ↦{DFrac.discard} v)` instance in
-- `Iris.BI.Lib.GenHeap`. Once registered, the `parRead_spec`
-- proof above can be tightened so the caller passes a single
-- discarded points-to and iris-lean splits it automatically.
-- Rocq tutorial reference (full version with arithmetic):
-- Example par_read : expr :=
-- let: "l" := ref #7 in
-- let: "r" := ( (!"l" + #14) ||| (!"l" * #3) ) in
-- Fst "r" + Snd "r".
-- Lemma par_read_spec :
-- {{{ True }}} par_read {{{ v, RET v; ⌜v = #42⌝ }}}.
-- TODO (upstream — iris-lean): once `PureExec` for arithmetic
-- lands, restore the Rocq tutorial's full `par_read` example with
-- its `21 * 2 = 42` postcondition.
-- Rocq tutorial reference:
-- Example par_read : expr :=
-- let: "l" := ref #7 in
-- let: "r" := ( (!"l" + #14) ||| (!"l" * #3) ) in
-- Fst "r" + Snd "r".
-- Lemma par_read_spec :
-- {{{ True }}} par_read {{{ v, RET v; ⌜v = #42⌝ }}}.
end Persistently