Proving output equivalence of parallel programs with sequential programs — in Lean 4
Anirudh Gupta
IISc Bengaluru · ISP-I · 4th-year BTech
github.com/AnirudhG07/paralever
19 October 2026
We parallelize programs for GPU/TPU, etc. targetting True Parallelism in most cases. The primary question that arises:
seq — the statements run one after another, on one core. par — the branches of a
parallel block run at the same time, and the block ends when all of them have.
Background — Hoare triples, ∗-disjointness, and what CSL's soundness theorem gives you
Our claim — independent branches can be run either way, and the answer does not change
Programs as data — one branching constructor, and a footprint computed from the syntax
The proof — four pieces, and the schedule appears in exactly one of them
The language — memory, registers, one fork, and a rejected program that explains itself
Examples — a sum, a matmul, a merge sort, a neural net — one check each
Automatic parallelization — a ;-chain in, a proved-equivalent tree out
MLIR — a benchmark in C gets a verdict without being translated by hand
Hoare triples, ∗-disjointness, and what Concurrent Separation Logic (CSL)'s soundness theorem gives you
Consider the below counterexample:
A triple {P} C {Q} says that if P holds and C terminates then Q holds. It quantifies over
the starting state and over nothing else — in particular not over what another thread does while
C runs.
So the first triple is proved as though its command ran alone. y := [p] reads back the value
the command itself wrote, which is true of every run in which nothing else touches [p].
The schedule that runs C₂ between that write and that read does touch [p], so the assumption
the proof was built on no longer holds, and y ends at 8.
A triple has no way to say this cell is mine, so it cannot exclude that schedule. Ownership is the missing notion, and it is what separation logic adds.
∗-disjointness
l ↦ v says the heap is exactly one cell, l, and it holds v. Holding it is exclusive: no
other assertion standing beside this one may mention l.
P ∗ P′ says the heap splits into two pieces whose domains are disjoint, one satisfying each
side. The existence of that split is part of the claim, not a remark about it.
Each premise of the rule mentions one thread, and the conclusion holds for every interleaving. Disjointness is what pays for that step: neither thread can reach a cell the other owns, so there is nothing for an interleaving to disturb.
O'Hearn states this as two hypotheses — a thread accesses only what it owns, and at every moment the state partitions into the parts owned by each thread and by each resource.
r is a resource: a name for a region of the heap that no thread owns. Every r carries an
invariant RI_r, the assertion describing the states that region is allowed to be in.
Outside a region a thread knows nothing about those cells. with r when B do C lends RI_r to
the thread on entry; C may break it while it runs, and has to restore it before the region ends.
RI_r must be precise, meaning it pins down one sub-heap rather than any of several. Without
that the hand-back is undefined — the thread would not know how much of the heap it is returning.
Brookes proves both rules sound in a semantics of action traces in which a racing step is a catastrophic error, so a program with a race proves nothing.
The heap splits, and that split is also the hypothesis: it must already hold of σ. Two threads
reading a₀ have no such split, and C₁; C₂ is never mentioned.
Each side needs a₀ ↦ u, so the conclusion needs (a₀ ↦ u) ∗ (a₀ ↦ u) ⇒ false.
A resource r serializes the two reads instead of allowing them.
Fractions do allow them: a₀ ↦_{1/2} u each, carried through every lemma.
Bernstein checks writes only, so read-read is no conflict.
Independent branches can be run either way, and the answer does not change
One theorem about two programs, with a hypothesis the kernel evaluates — rather than a bound on one program's postcondition.
{P₁}C₁{Q₁}, {P₂}C₂{Q₂} ⇒ {P₁∗P₂} C₁∥C₂ {Q₁∗Q₂}C₁∥C₂ from σ₁∗σ₂ with separate footprints is an interleaving of traces of C₁ and C₂C₁, C₂ share no variables ⇒ C₁∥C₂ = C₁;C₂wfB s = true ⇒ execPar s ∥≡ execSeq s — equality of outcome sets, arbitrary programs, kernel-checked@[simp, grind .]
def Indep (f₁ f₂ : Footprint) : Prop :=
Disjoint f₁.writes f₂.writes ∧ Disjoint f₁.writes f₂.reads ∧ Disjoint f₂.writes f₁.reads
w₁∩w₂ = w₁∩r₂ = w₂∩r₁ = ∅ — shared reads are allowed
∗ would forbid them, and so forbid matmul: disjoint C[i*n+j], overlapping rows
footprintOf is a fold over the syntax, so the check is decide, not a proof
WellFormed is that check at every par, along the rundef WellFormed {τ : Type} : ParM τ → State → Prop
| .pure _, _ => True
| .read l k, σ => WellFormed (k (σ l)) σ
| .write l v k, σ => WellFormed (k ()) (σ.update l v)
| .par cs k, σ =>
let r := runChilds cs σ
IndepAll (childFootprints cs σ) ∧ WellFormedChilds cs σ ∧ WellFormed (k r.1) r.2
accesses p σ is the footprint of running p from σ: the cells it reads and the cells it
writes, as two lists.
A par node asks for three things — the children's own footprints are pairwise independent, each
child is itself well-formed, and so is the continuation from the heap the children leave.
A read or write is never a condition; it only advances the heap the next check is measured
against. All of the content sits at par nodes.
Checking the outermost par alone is unsound, which is why the recursion goes to the bottom of
the tree.
p; q under every scheduletheorem par_seq_eq {s : ParProg} (h : wfB s = true) (ρ : Regs) (σ : State) :
ParSeqEq (execPar s ρ) (execSeq s ρ) σ := bys:ParProgh:s.wfB = trueρ:Regsσ:State⊢ s.execPar ρ ∥≡ s.execSeq ρ from σ
ParSeqEq p q σ — the two reach the same outcome set from σ, not one trace
hypothesis is a Bool the kernel evaluates; conclusion quantifies over schedules
time_le rides along: the parallel reading never needs more rounds
The schedule does not appear in the obligation. Proved once, every program afterwards pays one
decide on a footprint rather than a proof about interleavings.
One branching constructor, and a footprint computed from the syntax
ParM is an inductive type, so a program is a tree Lean can take apart; the obligation is a Bool
on two lists of addresses.
seq is a spine; par is the only node that branches
Same leaves, same order, one node different. paralever { s₁; …; sₙ } is right-nested seq, so
every program is a spine until a parallel block puts a fork in it.
footprintOf evaluates it
accesses p σ is the footprint a run actually has. It depends on σ, so a claim about it is a
claim about every heap.
footprintOf s reads the same two lists off the syntax tree and mentions no heap at all, so
Lean evaluates it.
execPar_accesses_sub is the one thing that needed proving, and it is proved once for all
programs: the computed lists contain the cells any run touches.
theorem execPar_accesses_sub (s : ParProg) (ρ : Regs) (σ : State) :
(accesses (execPar s ρ) σ).Sub (footprintOf s) := execPar_accesses_sub_all.1 s ρ σwfB checks every par, not just the topdef wfB : ParProg → Bool
| .load _ _ => true
| .store _ _ => true
| .set _ _ => true
| .seq a b => wfB a && wfB b
| .par ss => indepAllB (threadFootprints ss) && wfAllB ss
| .label _ s => wfB s
| .loadAt _ _ _ => true
| .storeAt _ _ _ => true
-- either branch may run, so both must be well-formed
| .ifElse _ t e => wfB t && wfB e
A check that only looked at the outermost fork would be unsound, which is why wfB is defined by
recursion on the whole tree.
wrong, so the hypothesis is load-bearingracy programs are excluded, not silently assumed away — so the atomic grain is irrelevant
standing obligation: exhibit p, q where the conclusion fails, so the hypothesis is not free
the loop-carried case A[i] := A[i-1] + 1 must stay unprovable
Four pieces, and the schedule appears in exactly one of them
Where the difficulty actually is, and the program on which this equality is false.
ParM is a nested inductive, so Lean's induction refuses it: every step runs through
ParM.mutualInduction, and the N-ary case is an induction on the children list.
these are different heaps, so the two readings of q are a priori different runs
independence is what makes them agree: q reads [1], p writes [0], so the cell q looks at
holds the same value in both
the induction therefore carries the footprint along with the equality — one level down, the footprint conjunct is what pins the replayed heap to the right one
The theorem is that every reachable terminal configuration of a well-formed par is the same
one, and runSeq names it. The schedule enters the proof here and nowhere else.
runSeqtheorem wellFormed_canonical {τ : Type} (p : ParM τ) :
∀ σ r σ', WellFormed p σ → (Reaches (.run p σ) (.run (.pure r) σ') ↔ (r, σ') = runSeq p σ) :=
wellFormed_canonical_all.1 p
One step leaves runSeq P σ unchanged, and can only shrink the footprint — step_combined.
One step keeps WellFormed, whichever branch moved — WellFormed_preserved.
Iterate along the whole chain: every reachable P', σ' has runSeq P' σ' = runSeq P σ —
runSeq_invariant_chain.
At a .pure r endpoint runSeq is (r, σ'), which gives (r, σ') = runSeq P σ: the →.
For ←, run child 1 to completion, then child 2, then step into the continuation —
reachesChilds_canonical.
wfB gives well-formedness at every heap, by one inductiontheorem wfB_wellFormed {s : ParProg} (h : wfB s = true) (ρ : Regs) (σ : State) :
WellFormed (execPar s ρ) σ := wfB_wellFormed_all.1 s h ρ σ
Induction over the syntax. load, store, set are trivial — only par carries a condition.
At a par: indepAllB held of the computed footprints, and Indep is antitone, so it holds of
the real ones at σ — indepAll_childFootprints.
The sequential side has no par node, so its WellFormed is True by structure —
wfB_wellFormedSeq_all.
Hand-written ParM instead of a ParProg: WellFormed_par_intro asks for the same three
obligations, proved rather than decided.
par_reachesseq_runSeq
put both accesses inside with r do … and CSL proves the program, with RI_r = ∃v. 10 ↦ v;
by Brookes's corollary it is race free
race freedom is strictly weaker than determinism: the ∥ rule joins postconditions with ∗,
which records no ordering — the rule is designed to forget the schedule
so ParM has no resource, no lock, and no unstructured fork, and the conclusion would be false
if it had — not_parSeqEq proves the failure, not_indepAll refutes the hypothesis
Both sides terminate normally here and simply disagree, which is the case a reader is entitled to worry about.
max at a fork is derived, not assumedtheorem time_le {s : ParProg} (h : wfB s = true) (ρ : Regs) (σ : State) :
time (execPar s ρ) σ ≤ time (execSeq s ρ) σ := time_execPar_le_all.1 s ρ σ h
work counts memory operations and is unchanged by forking — work_par_eq_seq
time counts rounds, where a round advances every ready child by one step
nothing in the cost semantics stipulates max at a fork: it falls out of the same independence
hypothesis, on the same Bool — and time_par_lt_seq_single makes the inequality strict when both
branches do work
Conflating work with time is the usual mistake. Parallelism buys nothing in total operations, and the theorem says so.
Memory, registers, one fork — and the check run on user code
ParProg is the surface syntax a user writes; wfB is the same obligation, decided.
[2] := %0 · footprint is exactA[B[i]] · footprint is the whole arrayif c {…} else {…} · footprint is the branch union
Over-approximation costs precision, never soundness: execPar_accesses_sub proves the computed
footprint contains every cell actually touched, at every heap.
parallel is checked, not trustedblock [·,·] = truethe only legal placement is in sequencethis is the configuration the whole data-race literature is about
our rewrite can never build it: a RAW edge puts the two in one thread
when the user writes it, wfB is the checker — and rejects
the scan is the obligation, so an empty site list is the race-freedom proof — a false negative
would be a bug in raceSites, not a tuning matter
a wrong guess in phase 2 cannot produce a wrong claim: follow_sound turns any schedule that
replays into a Reaches proof, so a bad guess merely finds nothing
aiming is what makes it cheap — on three 8, three threads where the middle one is innocent,
blind search takes 2.7 s and the aimed search 2 ms
def late : Prog :=
.par (.seq (.wr 10 1) (.seq (.wr 11 1) (.wr 9 1)))
(.seq (.wr 20 1) (.seq (.wr 21 1) (.wr 9 2)))
#evalstep:thread action state
0:T0 write [10] ← 1 10↦1, 11↦0, 9↦0, 20↦0, 21↦0
1:T0 write [11] ← 1 10↦1, 11↦1, 9↦0, 20↦0, 21↦0
2:T1 write [20] ← 1 10↦1, 11↦1, 9↦0, 20↦1, 21↦0
3:T1 write [21] ← 1 10↦1, 11↦1, 9↦0, 20↦1, 21↦1
4:T1 write [9] ← 2 pending
4:T0 write [9] ← 1 pending
Parallel aborts at Step 4. Reason: T0, T1 write to: {9}.
Parallel: 9↦1
Sequential: 9↦2
IO.println (explainProg late σ0)
Both sides do real work before colliding on their last write, so the race is not at the first step —
which is what makes aiming the search worth having. wellFormedB is false, and the report is the
interleaving that realizes it.
Four programs, in increasing order of what they need
A read-only sum, then overlapping reads, then recursion, then an in-place update — and the same two lines discharge every one of them.
def sumRangeSeq (chunks : ChunkPlan) : ParM (List Int) := ParM.seqAll (chunks.map sumRange)
/-! ### par -/
def sumRangePar (chunks : ChunkPlan) : ParM (List Int) := ParM.parAll (chunks.map sumRange)
one thread per chunk, each folding .read l over its own locations — no thread writes anything,
so Bernstein's three clauses are vacuous and ∗-disjointness would still have to split the array
wellFormed_sumRangePar holds for any ChunkPlan, including uneven ones, and any state — no
decide, so it does not care how long the array is
strictly fewer rounds once two chunks are nonempty (time_lt), at identical work
The obligation is a Bool the kernel evaluates; par_seq_eq supplies the rest.
def matmul (n : Nat) : ParProg :=
ParProg.parEach (gridIdx n n) fun ij =>
ParProg.block
([ paralever { %2 := 0 } ]
++ (List.range n).map (fun k => paralever {
%0 := [ij.1 * n + k];
%1 := [n * n + (k * n + ij.2)];
%2 := %2 + %0 * %1 })
++ [ paralever { [n * n + n * n + (ij.1 * n + ij.2)] := %2 } ])
The accumulator is a register, so a thread's whole write set is the one cell C[i,j]. That is what
lets different cells read overlapping rows and columns — and it is the program ∗-disjointness
rejects outright.
example : ParProg.wfB (matmul 2) = true := by⊢ (matmul 2).wfB = true decideAll goals completed! 🐙
example : ParProg.wfB (matmul 3) = true := by⊢ (matmul 3).wfB = true decideAll goals completed! 🐙
/-- …and the equivalence follows with nothing further to supply. -/
theorem par_seq_eq_2 (ρ : Regs) (σ : State) :
ParSeqEq (ParProg.execPar (matmul 2) ρ) (ParProg.execSeq (matmul 2) ρ) σ :=
ParProg.par_seq_eq (byρ:Regsσ:State⊢ (matmul 2).wfB = true decideAll goals completed! 🐙) ρ σparPair against seqPairdef mergeSortPure : List Int → List Int
| [] => []
| [x] => [x]
| x :: y :: rest =>
let xs := x :: y :: rest
let mid := xs.length / 2
mergePure (mergeSortPure (xs.take mid)) (mergeSortPure (xs.drop mid))def mergeSortPar (N : Nat) : Nat → Nat → ParM Unit
| lo, hi =>
if _h : hi - lo ≤ 1 then .pure ()
else
let mid := lo + (hi - lo) / 2
ParM.bind (ParM.parPair (mergeSortPar N lo mid) (mergeSortPar N mid hi)) (fun _ =>
ParM.bind (mergeStep mid hi N (hi - lo) lo mid lo) (fun _ =>
copyBack N (hi - lo) lo lo))
mergeSortSeq is the same definition with seqPair for parPair — nothing else differs
here the obligation cannot be decided: the recursion is in N, lo, hi, so independence comes
from an interval argument — the two halves touch [lo,mid) and [mid,hi), which are disjoint
mergeSortPar_wellformed N lo hi (hi ≤ N) σ holds for every range and every heap, and
seq_eq_par follows for the whole recursion rather than the top split
#eval on σ₄ = [5,3,8,1] agrees with mergeSortPure [5,3,8,1] — the pure function is the spec,
and the check is a #guard in the file.
/-- Forward pass: convolution, ReLU, dense layer. -/
def forward : ParProg :=
ParProg.block
[ ParProg.conv1dK 6 3 X CW C
, ParProg.mapK 6 C Rl (fun v => AExp.max v (.lit 0))
, ParProg.denseK 2 6 W2 Rl B2 Y ]
/-- One SGD step: forward, then the error, then an in-place weight update with learning rate
`1 / lrDen`. The update's thread `(i,j)` reads and writes `W[i,j]` *in place* — admissible only
because each thread owns its own cell. -/
def trainStep (lrDen : Int) : ParProg :=
ParProg.block
[ forward
, ParProg.zipK 2 Y T E (fun y t => y - t)
, ParProg.outerUpdateK 2 6 W2 E Rl lrDen ]
every conv1dK thread reads the whole weight array and its own window — two kinds of read
sharing at once, and neither costs anything, because Indep constrains writes only
the layout is declared, not addressed: Layout.of assigns the bases, so the disjointness the
net depends on holds by construction — §5 of the file shows the bug that used to be writeable
trainStep's update thread (i,j) reads and writes W[i,j] in place, which is admissible
exactly because each thread owns its own cell
forward_par_seq_eq and forward_time_le are both by decide on the same Bool.
A ;-chain in, a proved-equivalent tree out
The analysis is untrusted and the gate is proved, so the pass needs no hypothesis — and the depth it reaches is measured against the dependence DAG's own lower bound.
lastWriter[l] → i · then readers[l] += i RAW · W ∩ RlastWriter[l] → i WAW · W ∩ Wr → i for every r ∈ readers[l], then lastWriter[l] := i, readers[l] := [] WAR · R ∩ W
Bernstein's three clauses are the three kinds of edge. The DAG's longest path (critical) is a
lower bound on the depth of any schedule, so it is what says whether a plan is optimal rather than
merely better.
The DAG's longest path (critical) is a lower bound on the depth of any schedule, so it is what
says whether a plan is optimal rather than merely better.
Found by testing a rewrite that gave M[12],M[13] = 0,0 where the original gave 3,4. A load and
the store consuming its register are one segment and are never split across a fork.
Because no unit moves, planUnits (plan us) = us is a structural theorem — proved by induction on
the pass, not checked at run time — and the soundness proof needs no commutation argument.
worked on [0]:=1; %0:=[0]; [1]:=%0; [2]:=3; %1:=[2]; [3]:=%1 — two chains written interleaved | ||
|---|---|---|
u₁ | — | new thread · cur = [u₁] |
u₂ | conflicts with cur | extend · cur = [u₁,u₂] |
u₃ | independent of everything | new thread · stage = [[u₁,u₂]] |
u₄ | conflicts with cur | extend · cur = [u₃,u₄] |
parallel { u₁; u₂ } | { u₃; u₄ } — depth 3, sequential 6, bound 3 | ||
Greedy prefix grouping gets 5 here: one conflicting unit ends its group even when everything after it is free.
Greedy prefix grouping gets 5 here: one conflicting unit ends its group even when everything after it is free.
rewrite_sound: seq = par for the rewrite, with no hypothesistheorem rewrite_sound (p : ParProg) (ρ : Regs) (σ : State) :
(runSeq (execPar (rewrite p) ρ) σ).2 = (runSeq (execSeq p ρ) σ).2 := byp:ParProgρ:Regsσ:State⊢ (runSeq ((rewrite p).execPar ρ) σ).snd = (runSeq (p.execSeq ρ) σ).snd
execPar and execSeq agree under runSeq — execPar_runSeq_all
execSeq of a par restores the register file, so a chain of forks never advances it: every
thread of the whole plan runs at the initial registers — stages_seqAll
threads in order, each at the initial ρ, are the chain, when no register flows between them —
fork_heap
a chain of blocks runs like the flat chain — runSeq_block_map
the plan's units are the program's statements — plan_units, atoms_flatten, runSeq_flatten
Step 2 carries the proof: a fork restores ρ, so a chain of forks leaves the top-level plan in the
situation fork_heap already handles, one level up. rewrite checks its own plan and returns p unchanged if the check
fails — which is why the theorem needs no hypothesis.
Two independent chains, written interleaved. The pass finds four units, one stage, two threads:
quality ex1 = (3, 3, 6) and optimalB ex1 — depth 3 against a sequential 6, and 3 is the
dependence DAG's critical path, so this is optimal rather than merely better. Greedy prefix
grouping gets 5, because one conflicting unit ends its group even when everything after it is free.
paralever in, paralever out
lang → tree → tree → lang, closed. evalTask (rewrite p) = evalTaskSeq p is gated per program on
real threads, on top of the theorem.
| program | depth | sequential | bound L | optimal |
|---|---|---|---|---|
| two chains, interleaved | 3 | 6 | 3 | ✓ |
| the N | 8 | 12 | 7 | — |
| merge sort, n = 4 | 12 | 16 | 12 | ✓ |
| loop-carried chain | 6 | 6 | 6 | ✓ |
L is the dependence DAG's critical path — a bound for any schedule, so ✓ is a certificate
the N's 7 needs a₃ concurrent with a₂'s write of [1]: wfB of that tree is false
so 8 is the floor for a race-free fork–join rewrite, and the gap is the shape, not the scheduler
divide-and-conquer DAGs are series-parallel — no N — so the plan attains the bound
atoms threw the recursion away with the ;-chain: the tree comes back from footprints alone
splitting the merge per output cell gives depth 9, at work 16 → 24 — the trade, measured
One front end, so a benchmark is not translated by hand
Hand-translating a benchmark suite is the dominant threat to validity for anything measured on one, because the translation is part of what is measured and is itself unverified.
N languages against one converter, instead of one front end per language — and stage 1 is
replaceable, so a hand-written .mlir feeds the same interface
generic form, not each dialect's assembly: --mlir-print-op-generic prints every op the same
way, so one reader covers affine, memref, arith and func
not LLVM IR — unstructured br and phi nodes have no encoding in an inductive ParM, and
going through a loop structurizer destroys the affine maps the lowering depends on
MLIR.lower is a Lean function, so the translation is an object we can state a theorem about.
The target is footprint preservation, which turns "trust the translator" into a proof obligation.
loops are unrolled at lowering time, so the subscript is a literal Nat and the footprint stays
exact — proportional to the number of accesses, not to the size of the array
that matters for evaluability, not only precision: indepB is quadratic in list length, so an
over-approximated footprint on a large array is unevaluable rather than merely imprecise
unrolling has a ceiling — decide on wfB (matmul n) is 6.6 s at n = 8 and 19 s at n = 10,
and native_decide is prohibited — so about a hundred threads, and past that a kernel lemma at
symbolic n
Layout.of places buffers in consecutive ranges, so array disjointness is discharged once by the
allocator rather than once per pair.
doall-no.mlir
The subscript is a literal Nat because the loop is unrolled at lowering time, so the footprint
stays exact rather than becoming the whole array. C = [11, 22, 33, 44, 55, 66, 77, 88] on the
threaded run, and runTask_eq is what makes it sound to look at that heap at all.
Int, the reference over ℚ
the verdict depends on no value: footprintOf and indepB never inspect a Val, so a
memref<8xf64> occupies the same eight cells as a memref<8xi32> and gets the same verdict
floating point becomes Rat, not Real — every finite IEEE value is m · 2ᵉ, so Rat is a
superset of the floats rather than an approximation, and it is computable, so a gate can run
the reference interpreter is allowed to evaluate a par in one canonical order, and what licenses
that is par_seq_eq itself
| MLIR.Corpus.summary | expressible | unsupported | confusion | over-approx | round trip |
|---|---|---|---|---|---|
| seven labelled pilots | 7/7 | [] | tp=3 fp=0 fn=0 tn=4 | indirect-yes | ✓ |
No false negatives is not luck: raceSites_nil_iff makes the scan complete for the obligation.
stencil-f64-no is the Bernstein case — neighbouring threads share two of three read cells, which
∗ rejects outright and Indep accepts without a clause.
decide dies| surface matmul, `wfB` at n | 6 | 8 | 10 | 12 |
|---|---|---|---|---|
decide — pairwise over n² threads | 1.5 s | 6.3 s | 19.3 s | 49.3 s |
wfB_parFor_of_lists — symbolic in n | 0.16 s | 0.16 s | 0.16 s | 0.16 s |
fitted exponent 5.1 — Θ(n²) threads × Θ(n) cells, pairwise
11 kernels have a ∀ n obligation; each also decides at a concrete n, so the routes are checked to agree
the scheduler must emit kernel shape, not an unrolled par list — a Lower bug otherwise
registers cannot be renamed away — yet. noRegFlow is on raw read sets, stronger than the
liveness atoms establishes, so a chain reusing %0 is declined and returned sequential
memory cannot be renamed at all. A Loc is reached by a computed address, so the cell is
the name — privatization, the commonest OpenMP clause, is out of reach
footprints over-approximate. A[B[i]] reports the whole array; whileAtMost reports the
branch union — conservative, never unsound
ParM is inductive, so while true is unrepresentable; and whether order preservation itself
costs depth is open
One theorem, then arithmetic. wfB s = true ⇒ execPar s ∥≡ execSeq s, every schedule,
arbitrary programs, kernel-checked — so each program pays decide, not a proof about interleavings
The rewrite is proved, not checked. rewrite_sound has no hypothesis: the pass is untrusted,
the gate is proved, and the plan's order preservation makes the bookkeeping structural
Optimal where fork–join allows. Depth meets the DAG critical path on interleaved chains and on merge sort; the N's gap of 8 vs 7 is the shape, and the 7-tree is racy
Bernstein, Analysis of programs for parallel processing, IEEE TEC, 1966.
O'Hearn, Resources, concurrency and local reasoning, CONCUR 2004.
Brookes, A semantics for concurrent separation logic, CONCUR 2004, §5–§7.
Reynolds, Toward a grainless semantics for shared-variable concurrency, FSTTCS 2004.
Apt & Olderog, Verification of Sequential and Concurrent Programs, Springer.
Valdes, Tarjan & Lawler, The recognition of series parallel digraphs, SIAM J. Comput. 11(2):298–313, 1982.
Graham, Bounds on multiprocessing timing anomalies, SIAM J. Appl. Math., 1969.