Part 6 — Heap & Concurrency

Separation logic, mutable state, and verified concurrent Python

What's real today. As of the latest deppy release the following chapters' APIs are all real runtime-enforced constructs, not metadata stubs:
  • Lock(invariant=..., invariant_sketch=...) — checks the predicate on every acquire / release; raises InvariantViolation on failure (deppy.concurrency)
  • atomic_cas(cell, expected, new) — real CAS that records each step as a cubical PathAbs on the cell's history
  • spawn(fn, reads=..., writes=...) / join_all(patches) — thread-spawn with Čech cocycle-condition enforcement; disjoint writes are accepted, overlapping writes raise RaceViolation
  • @parallel_safe — runs data-race analysis at decoration time and raises on unprotected shared writes
  • @async_requires, @async_ensures, @async_yields, @async_invariant, @terminates, pure(...) — all check predicates at runtime and raise ContractViolation / TerminationViolation on failure (deppy.async_verify)
  • ghost_var, ghost_update, ghost_pts_to, array_pts_to, ghost_assert — runtime ghost tracking with history and points-to assertions (deppy.ghost)

What's not yet enforced at runtime: deadlock detection, happens-before ordering across threads, full separation-logic frame-rule discharge. Those remain as static declarations the safety pipeline consumes. See ARCHITECTURE.md §12 for the current frontier.

Why This Part Matters

Everything in Python is an object on the heap. Every variable is a reference. Every assignment is a pointer copy. This makes Python wonderfully flexible — and fiendishly hard to reason about formally. In this part we bring two complementary systems to bear: the cubical heap (Deppy's native model) and classical separation logic (the framework Pulse and Iris use). The two coexist; they describe the same property surface from different angles.

Two heap models, one verifier. The cubical heap (Chapter 6.1) models mutation as paths in a world category: each mutation bumps a global epoch, aliases are DuckPaths at the cell level, reads after mutations require TransportProof. The separation-logic chapter (6.2) presents the older points-to / frame-rule machinery and contrasts it with Pulse's separating conjunction. The remaining chapters apply the cubical heap to references, arrays, threading, and async.

Python's Memory Model

CPython's runtime has three properties that distinguish it from the C / Rust / F* worlds where separation logic was originally developed:

1. Everything Is an Object

Integers, strings, functions, classes — every value lives on the heap and is accessed through a reference (a PyObject* in C terms). There is no "stack allocation" of primitives.

# x and y refer to the *same* heap object
x = [1, 2, 3]
y = x
y.append(4)
assert x == [1, 2, 3, 4]  # surprise!

2. Reference Counting + Cycle GC

Each object carries a reference count. When the count drops to zero, CPython deallocates immediately. A supplementary tracing GC catches reference cycles. For verification, this means:

3. The Global Interpreter Lock (GIL)

Only one thread executes Python bytecode at a time. The GIL guarantees atomicity of individual bytecodes but not atomicity of compound operations. A statement like counter += 1 compiles to multiple bytecodes — LOAD_FAST, BINARY_ADD, STORE_FAST — and a thread switch can occur between any two of them.

The GIL does not make your code thread-safe. It prevents segfaults and corrupted interpreter state, but logical data races are still possible. Deppy's separation logic proofs catch exactly these races.
# Classic GIL misconception — this is NOT thread-safe
import threading

counter = 0

def bump():
    global counter
    for _ in range(100_000):
        counter += 1   # LOAD, ADD, STORE — not atomic!

threads = [threading.Thread(target=bump) for _ in range(4)]
for t in threads: t.start()
for t in threads: t.join()
print(counter)  # Almost certainly != 400_000

Why Separation Logic?

Classical Hoare logic describes a heap as a monolithic map from addresses to values. Separation logic adds a critical operator — the separating conjunction ★ — that asserts two sub-heaps are disjoint. This lets you reason locally: a function that operates on its own slice of the heap cannot corrupt yours.

Concept Classical Hoare Logic Separation Logic (Deppy)
Heap model One big map Disjoint sub-heaps composed with ★
Frame rule ❌ Must mention entire state ✅ Unmentioned state is preserved
Aliasing Must track globally Ownership types guarantee non-aliasing
Concurrency Requires global invariant Lock invariants own disjoint resources

For Python, separation logic is a natural fit because every object reference is a potential alias. By tracking ownership — which references a function is permitted to read or write — we can verify that mutations are safe even in the presence of shared mutable state.

Deppy's Approach

Deppy expresses separation-logic specifications as Python decorators. The proof engine discharges obligations to Z3 (for decidable fragments) or to an LLM oracle (for domain-specific semantics), exactly as in earlier parts of this book. The key new ingredients are:

  1. SLProp — a type of separation-logic propositions (analogous to F*'s slprop).
  2. pts_to(ref, value) — the fundamental "points-to" assertion: ref currently holds value.
  3. @requires / @ensures — pre- and post-conditions in separation logic.
  4. Lock(invariant) — locks that own a piece of the heap described by an SLProp.
  5. @parallel_safe — proof that a function can be called from multiple threads without data races.
from deppy.separation import SLProp, pts_to, emp
from deppy.heap import Ref, requires, ensures

@requires(pts_to(x, 'v'))
@ensures(lambda result: pts_to(x, 'v + 1'))
def incr(x: Ref[int]):
    """Increment a heap-allocated integer."""
    x.value += 1

The body of incr is ordinary Python. The decorators express the contract in separation logic, and Deppy verifies that the implementation satisfies the contract at import time (or via python -m deppy.pipeline.run_verify).

What You'll Learn

This part contains five chapters:

ChapterTopicPulse parallel
6.1 The Cubical Heap — World, epochs, EffectSequence, alias DuckPaths, TransportProof No equivalent (Pulse uses ★)
6.2 Separation Logic (vs Pulse) — SLProp, pts_to, ★, frame rule, side-by-side comparison with cubical Pulse Ch. 1
6.3 Mutable References — aliasing as DuckPath, fractional permissions in cubical form Pulse Ch. 2
6.4 Arrays — Python lists, bounds proofs, NumPy sidecars Pulse Ch. 3
6.5 Threading & GIL — runtime Lock(invariant=…), race-freedom proofs No equivalent
6.6 Async / Await — event loops, coroutine proofs as coalgebras, @async_requires No equivalent
Chapters 6.5 and 6.6 (threading + async) are unique to Deppy. They have no direct counterpart in the F* tutorial because Python's concurrency model (GIL + asyncio) is fundamentally different from the extracted C code that Pulse targets.

Prerequisites

Before starting Part 6 you should be comfortable with:

No prior knowledge of separation logic is assumed — we build it from scratch.

Cubical Heap vs Pulse: The Big Picture

Pulse (Microsoft Research's concurrent separation-logic verifier built on F) is the best-known production tool in the separation-logic tradition. Deppy's cubical heap reaches similar soundness via a different mathematical route. Both are honest, both are sound; they just slice the problem differently.

ConceptPulse (separation logic)Deppy (cubical / HoTT)
Heap composition (separating conjunction) PathComp of world-paths
Aliasing Fractional permissions DuckPath at cell level
Mutation Frame rule + ownership transfer EffectWitness("mutate:X@N")
Read after mutation Permission-tracked TransportProof along path
Atomic invariants with_invariants Cocycle over shared invariants
Concurrency at the kernel Full atomics + locks (Honest) Deppy doesn't model concurrency at the kernel level — uses runtime deppy.concurrency.Lock instead

Pulse's frame rule is an arrow you draw around a Hoare triple; Deppy's PathComp is an arrow you compose through a sequence of effects. Pulse forbids what it cannot prove disjoint; Deppy admits everything but forces transport when a proof is reused across mutations. The two systems are dual, in a precise category-theoretic sense.

Below: the same example expressed in both systems. The Pulse version uses points-to and the frame rule; the Deppy version emits an EffectWitness on the cell and lets the kernel sequence it through PathComp.

Pulse (F)

(* Pulse syntax *)
fn incr (x: ref int)
  requires pts_to x 'i'
  ensures  pts_to x 'i+1'
{
  let v = !x;
  x := v + 1;
}

Deppy (cubical, via PSDL)

# PSDL inside @verify / @psdl_proof
# or inside a .deppy verify block
def push(xs):
    xs.append(1)
# compile_psdl emits:
#   EffectWitness(mutate:xs.append@epoch1, Refl)
# final_epoch == 1; effect_sequence == [(2, 'mutate', 'xs.append')]

Key differences from Pulse:

Key Concepts at a Glance

SLProp — Heap Propositions

A value of type SLProp describes a region of the Python heap. The primitives are emp (empty heap), pts_to(ref, val) (a reference holding a value), and the separating conjunction ** (two disjoint heap regions). These compose to describe arbitrarily complex heap states.

Ownership Transfer

When you call a function whose precondition mentions pts_to(x, v), you transfer ownership of x to that function. It gives you back ownership via its postcondition. This is how Deppy prevents data races and use-after-free without runtime overhead.

The Frame Rule

If {P} f {Q}, then {P ** R} f {Q ** R} for any R. In words: anything the function doesn't mention is automatically preserved. This enables local reasoning — you never have to describe the entire heap in a specification.

Lock Invariants

A lock owns a heap region described by its invariant. Acquiring the lock transfers that region to the acquiring thread; releasing returns it. This is how shared mutable state is safely partitioned among threads.

Exercise 6.0.1: Run the counter threading example above with 4 threads and 100,000 iterations each. What value do you get? Run it five times and record the variation. This demonstrates why formal verification of concurrent code matters.
Exercise 6.0.2: Consider the following Python code:
a = [1, 2, 3]
b = a
b[0] = 99
print(a[0])
What does it print? Write an informal separation-logic specification that would make the aliasing explicit. What would pts_to(a, ???) ★ pts_to(b, ???) mean here? (Hint: it can't hold — a and b point to the same object.)
Exercise 6.0.3: List three Python standard-library modules that perform heap mutation internally (e.g., collections.deque). For each, sketch what separation-logic pre/post-conditions might look like for their most common operation.