From 5909d1bae7648600e86ab49730835ce9d336ca95 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:07:14 -0500 Subject: [PATCH 01/18] feat(DataStructure/Memory): word-addressed memory oracle and named field arrays Memory.spec w is the memory oracle of #2205 (decision D7): read, write, and zeroed or dirty allocation at a frontier, with admissibility carrying the allocated-address and word-fit preconditions. Field arrays are WordRAM Blocks accessed by Block.get/Block.set programs over the oracle. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity.lean | 2 + Complexity/DataStructure/Memory/Field.lean | 183 +++++++++++++++ Complexity/DataStructure/Memory/Spec.lean | 258 +++++++++++++++++++++ ComplexityTests/DataStructureMemory.lean | 117 ++++++++++ 4 files changed, 560 insertions(+) create mode 100644 Complexity/DataStructure/Memory/Field.lean create mode 100644 Complexity/DataStructure/Memory/Spec.lean create mode 100644 ComplexityTests/DataStructureMemory.lean diff --git a/Complexity.lean b/Complexity.lean index 7f9a77746d..00f4bf2faf 100644 --- a/Complexity.lean +++ b/Complexity.lean @@ -2873,6 +2873,8 @@ public import Complexity.DataStructure.HashFamily.WordRAM.DoubleTabulation public import Complexity.DataStructure.HashFamily.WordRAM.KWise public import Complexity.DataStructure.HashFamily.WordRAM.MultiplyShift public import Complexity.DataStructure.HashFamily.WordRAM.Tabulation +public import Complexity.DataStructure.Memory.Field +public import Complexity.DataStructure.Memory.Spec public import Complexity.DataStructure.ObliviousSequence public import Complexity.DataStructure.Online.Basic public import Complexity.DataStructure.OrderedSet.Basic diff --git a/Complexity/DataStructure/Memory/Field.lean b/Complexity/DataStructure/Memory/Field.lean new file mode 100644 index 0000000000..f065798d88 --- /dev/null +++ b/Complexity/DataStructure/Memory/Field.lean @@ -0,0 +1,183 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.Computability.WordRAM.ListArray +public import Complexity.DataStructure.Memory.Spec + +/-! +# Named field arrays over the memory oracle + +A field array is a `Cslib.WordRAM.Block`: a base address and a length. A data structure lays out +its storage as a structure whose fields are blocks (a struct of arrays) and accesses entry `i` of +a field `f` with the programs `f.get i` and `f.set i v` over `Memory.spec w`. Addresses are thus +named (`par.get v`) rather than computed by hand (`base + n + v`), while computed indices, pointer +chasing (`do let p ← next.get v; key.get p`) and reuse of a block across operations remain +ordinary programs. + +The lemmas here evaluate these programs, discharge their admissibility from `Block.Allocated`, +and describe their frame: a write to a field leaves every other address unchanged. +`Block.entries` reads the first `n` entries of a field as a list, which is how an abstraction +function usually sees an array. It realizes `Block.HoldsList`. + +## Main definitions + +* `Cslib.WordRAM.Block.get`, `Cslib.WordRAM.Block.set`: entry access through the oracle. +* `Cslib.WordRAM.Block.Allocated`: the block lies below the frontier. +* `Cslib.WordRAM.Block.entries`: the first `n` entries of a field in index order. +* `Cslib.DataStructure.Memory.allocBlock`, `Cslib.DataStructure.Memory.allocBlockDirty`: + allocate a field. + +## Main results + +* `Cslib.WordRAM.Block.runClient_get_bind`, `Cslib.WordRAM.Block.runClient_set_bind`: one-step + evaluation of field accesses. +* `Cslib.WordRAM.Block.Allocated.lt_frontier`, `Cslib.WordRAM.Block.Allocated.mono`: allocated + entries are admissible addresses, and stay allocated. +* `Cslib.WordRAM.Block.entries_succ`, `Cslib.WordRAM.Block.entries_congr`, + `Cslib.WordRAM.Block.entries_update_of_le`, `Cslib.WordRAM.Block.entries_update_of_not_owns`: + the list view under appends and writes. +* `Cslib.WordRAM.Block.holdsList_entries`: the list view realizes `Block.HoldsList`. +* `Cslib.DataStructure.Memory.runClient_allocBlock_bind`: allocation hands out a zeroed, + allocated block at the frontier. +-/ + +@[expose] public section + +namespace Cslib.WordRAM.Block + +open Cslib.DataStructure + +/-- Read entry `i` of the field `b` through the memory oracle. -/ +abbrev get (b : Block) (i : ℕ) : Memory.Prog ℕ := Memory.read (b.base + i) + +/-- Write `v` to entry `i` of the field `b` through the memory oracle. -/ +abbrev set (b : Block) (i v : ℕ) : Memory.Prog Unit := Memory.write (b.base + i) v + +/-- The block lies below the frontier, so all of its entries are allocated. -/ +def Allocated (b : Block) (s : Memory.State) : Prop := b.base + b.length ≤ s.frontier + +/-- An entry of an allocated block is an allocated address. -/ +theorem Allocated.lt_frontier {b : Block} {s : Memory.State} (h : b.Allocated s) {i : ℕ} + (hi : i < b.length) : b.base + i < s.frontier := by + unfold Allocated at h + omega + +/-- A block stays allocated as the frontier grows. -/ +theorem Allocated.mono {b : Block} {s s' : Memory.State} (h : b.Allocated s) + (hs : s.frontier ≤ s'.frontier) : b.Allocated s' := + le_trans h hs + +/-- A block owns the address of each of its entries. -/ +theorem owns_add {b : Block} {i : ℕ} (hi : i < b.length) : b.Owns (b.base + i) := + ⟨Nat.le_add_right _ _, Nat.add_lt_add_left hi _⟩ + +/-- Reading an entry returns the cell and leaves memory unchanged. -/ +@[simp] theorem runClient_get_bind {w : ℕ} {α : Type} (b : Block) (i : ℕ) (k : ℕ → Memory.Prog α) + (s : Memory.State) : + (Memory.spec w).runClient s (b.get i >>= k) = + let (v, s', os) := (Memory.spec w).runClient s (k (s.mem (b.base + i))) + (v, s', .read (b.base + i) :: os) := rfl + +/-- Writing an entry updates its cell. -/ +@[simp] theorem runClient_set_bind {w : ℕ} {α : Type} (b : Block) (i v : ℕ) + (k : Unit → Memory.Prog α) (s : Memory.State) : + (Memory.spec w).runClient s (b.set i v >>= k) = + let (r, s', os) := (Memory.spec w).runClient + ⟨Function.update s.mem (b.base + i) v, s.frontier⟩ (k ()) + (r, s', .write (b.base + i) v :: os) := rfl + +/-! ## The list of entries -/ + +/-- The first `n` entries of the field `b` in memory `m`, in index order. -/ +def entries (b : Block) (n : ℕ) (m : ℕ → ℕ) : List ℕ := + (List.range n).map fun i => m (b.base + i) + +@[simp] theorem length_entries (b : Block) (n : ℕ) (m : ℕ → ℕ) : (b.entries n m).length = n := by + simp [entries] + +@[simp] theorem entries_zero (b : Block) (m : ℕ → ℕ) : b.entries 0 m = [] := rfl + +/-- Entry `i` of the list view is the cell at `b.base + i`. -/ +@[simp] theorem getElem_entries (b : Block) (n : ℕ) (m : ℕ → ℕ) (i : ℕ) + (hi : i < (b.entries n m).length) : (b.entries n m)[i] = m (b.base + i) := by + simp [entries] + +/-- One more entry appends its cell. -/ +theorem entries_succ (b : Block) (n : ℕ) (m : ℕ → ℕ) : + b.entries (n + 1) m = b.entries n m ++ [m (b.base + n)] := by + simp [entries, List.range_succ] + +/-- The list view depends only on the cells of its entries. -/ +theorem entries_congr (b : Block) (n : ℕ) {m m' : ℕ → ℕ} + (h : ∀ i < n, m' (b.base + i) = m (b.base + i)) : b.entries n m' = b.entries n m := + List.map_congr_left fun i hi => h i (List.mem_range.mp hi) + +/-- Writing at or beyond entry `n` leaves the first `n` entries unchanged. -/ +theorem entries_update_of_le (b : Block) {n i : ℕ} (hi : n ≤ i) (m : ℕ → ℕ) (v : ℕ) : + b.entries n (Function.update m (b.base + i) v) = b.entries n m := + b.entries_congr n fun j hj => Function.update_of_ne (by omega) v m + +/-- Writing outside the block leaves every entry unchanged. -/ +theorem entries_update_of_not_owns (b : Block) {n : ℕ} (hn : n ≤ b.length) {a : ℕ} + (ha : ¬b.Owns a) (m : ℕ → ℕ) (v : ℕ) : + b.entries n (Function.update m a v) = b.entries n m := + b.entries_congr n fun j hj => + Function.update_of_ne (fun h => ha (by rw [← h]; exact owns_add (by omega))) v m + +/-- The list view of `n` entries realizes the list at the block's base. -/ +theorem holdsList_entries (b : Block) (n : ℕ) (m : ℕ → ℕ) : + HoldsList b.base (b.entries n m) m := fun i => by + simp [Block.read] + +end Cslib.WordRAM.Block + +namespace Cslib.DataStructure.Memory + +open Cslib.WordRAM + +/-- Allocate a zeroed field of `k` cells. -/ +def allocBlock (k : ℕ) : Prog Block := do + let base ← alloc k + pure ⟨base, k⟩ + +/-- Allocate a field of `k` cells without clearing it. -/ +def allocBlockDirty (k : ℕ) : Prog Block := do + let base ← allocDirty k + pure ⟨base, k⟩ + +/-- A zeroed allocation hands out the block at the frontier, clears it, and advances the +frontier past it. -/ +@[simp] theorem runClient_allocBlock_bind {w : ℕ} {α : Type} (k : ℕ) (next : Block → Prog α) + (s : State) : + (spec w).runClient s (allocBlock k >>= next) = + let (v, s', os) := (spec w).runClient + ⟨clear s.mem s.frontier k, s.frontier + k⟩ (next ⟨s.frontier, k⟩) + (v, s', .alloc k :: os) := rfl + +/-- A dirty allocation hands out the block at the frontier without changing memory. -/ +@[simp] theorem runClient_allocBlockDirty_bind {w : ℕ} {α : Type} (k : ℕ) + (next : Block → Prog α) (s : State) : + (spec w).runClient s (allocBlockDirty k >>= next) = + let (v, s', os) := (spec w).runClient ⟨s.mem, s.frontier + k⟩ (next ⟨s.frontier, k⟩) + (v, s', .allocDirty k :: os) := rfl + +/-- A freshly allocated block is allocated in the new state. -/ +theorem allocated_alloc (s : State) (k : ℕ) : + (Block.mk s.frontier k).Allocated ⟨clear s.mem s.frontier k, s.frontier + k⟩ := + le_rfl + +/-- A freshly allocated zeroed block holds zeros. -/ +theorem entries_alloc (s : State) (k : ℕ) {n : ℕ} (hn : n ≤ k) : + (Block.mk s.frontier k).entries n (clear s.mem s.frontier k) = List.replicate n 0 := by + refine List.ext_getElem (by simp) fun i hi _ => ?_ + simp only [Block.length_entries] at hi + simp only [Block.getElem_entries, List.getElem_replicate] + exact clear_of_owns (Block.owns_add (b := ⟨s.frontier, k⟩) (by simp only; omega)) + +end Cslib.DataStructure.Memory diff --git a/Complexity/DataStructure/Memory/Spec.lean b/Complexity/DataStructure/Memory/Spec.lean new file mode 100644 index 0000000000..3507072aee --- /dev/null +++ b/Complexity/DataStructure/Memory/Spec.lean @@ -0,0 +1,258 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.Computability.WordRAM.Heap +public import Complexity.DataStructure.StateTransition + +/-! +# Word-addressed memory as a state-transition specification + +`Memory.spec w` is a memory oracle: the interface through which a data-structure implementation +reaches storage. Its state is the contents of every cell together with an allocation frontier. +Every address below the frontier is allocated, and allocation hands out the block that starts at +the frontier. The operations read a cell, write a cell, and allocate a block, either zeroed +(`alloc`, charged per cell) or with its old contents (`allocDirty`, charged one unit). + +Admissibility carries the preconditions a word RAM needs. Accessed addresses must be allocated, +written values must fit a `w`-bit word, and the frontier after an allocation must fit. Under +these preconditions the frontier and every cell keep fitting a word (`WellFormed.step`), so an +allocated address fits too (`WellFormed.fits_of_lt_frontier`). + +An implementation over this interface is an ordinary `Memory.Prog` program. Its correctness and +its operation charges are proved against `step` and `charge`, without registers or instruction +counts. `charge` is the memory-level currency; executing such a program on the word RAM is a +separate step that exchanges it into instructions. Allocation never reuses addresses, as in +`Cslib.WordRAM.alloc`, so blocks allocated at different times are disjoint +(`Cslib.WordRAM.Block.disjoint_of_le`). A program reuses memory by keeping a block it allocated. + +## Main definitions + +* `Memory.State`, `Memory.Op`, `Memory.step`, `Memory.valid`: cells and frontier, operations + with their answers, the transition function and the admissibility preconditions. +* `Memory.spec`: the deterministic specification at word size `w`. +* `Memory.charge`: one unit per read, write and dirty allocation; `k + 1` for a zeroed block of + `k` cells. +* `Memory.Prog`, `Memory.read`, `Memory.write`, `Memory.alloc`, `Memory.allocDirty`: programs + over the oracle and its four one-call programs. +* `Memory.WellFormed`: the frontier and all cell contents fit a `w`-bit word. +* `Memory.State.init`: the word RAM's input convention as a memory state. + +## Main results + +* `Memory.WellFormed.step`, `Memory.WellFormed.run`: admissible operations preserve + well-formedness. +* `Memory.WellFormed.fits_of_lt_frontier`: allocated addresses fit a word. +* `Memory.frontier_le_step`, `Memory.frontier_le_run`: the frontier never decreases. +* `Memory.mem_step_of_lt_frontier`: an operation changes an allocated cell only by writing it. +* `Memory.wellFormed_init`: the input convention is well formed. +-/ + +@[expose] public section + +namespace Cslib.DataStructure.Memory + +open Cslib.WordRAM + +/-- Memory: the contents of every cell, and the frontier below which cells are allocated. -/ +@[ext] +structure State where + /-- The contents of the cell at each address. -/ + mem : ℕ → ℕ + /-- Every address below the frontier is allocated; the next block starts here. -/ + frontier : ℕ + +/-- Operations on memory. Addresses are words computed by the caller. -/ +inductive Op where + /-- Read the cell at an address. -/ + | read (addr : ℕ) + /-- Write a value to the cell at an address. -/ + | write (addr value : ℕ) + /-- Allocate `count` zeroed cells at the frontier and return their base address. -/ + | alloc (count : ℕ) + /-- Allocate `count` cells at the frontier without clearing them and return their base + address. -/ + | allocDirty (count : ℕ) + deriving DecidableEq + +/-- The answer type of each operation: a read and an allocation return a word. -/ +abbrev Op.Result : Op → Type + | .read _ => ℕ + | .write _ _ => Unit + | .alloc _ => ℕ + | .allocDirty _ => ℕ + +/-- The transition of each operation. An allocation returns the old frontier. -/ +def step (s : State) : (o : Op) → State × o.Result + | .read a => (s, s.mem a) + | .write a v => (⟨Function.update s.mem a v, s.frontier⟩, ()) + | .alloc k => (⟨clear s.mem s.frontier k, s.frontier + k⟩, s.frontier) + | .allocDirty k => (⟨s.mem, s.frontier + k⟩, s.frontier) + +/-- Admissibility at word size `w`: accessed addresses are allocated, written values fit a word, +and an allocation leaves a fitting frontier. -/ +def valid (w : ℕ) (s : State) : Op → Prop + | .read a => a < s.frontier + | .write a v => a < s.frontier ∧ Fits w v + | .alloc k => Fits w (s.frontier + k) + | .allocDirty k => Fits w (s.frontier + k) + +instance (w : ℕ) (s : State) (o : Op) : Decidable (valid w s o) := by + cases o <;> unfold valid <;> infer_instance + +/-- Word-addressed memory at word size `w`. -/ +abbrev spec (w : ℕ) : StateTransitionSpec := + StateTransitionSpec.ofFn (State := State) (Op := Op) (Result := Op.Result) step (valid w) + +/-- The memory-level currency: one unit per read, write and dirty allocation, and `k + 1` units +for a zeroed block of `k` cells. -/ +def charge (_ : State) : Op → ℕ + | .alloc k => k + 1 + | _ => 1 + +/-- Programs over the memory oracle. They are the clients of `Memory.spec w` at every word size. -/ +abbrev Prog (α : Type) : Type := PFunctor.FreeM ⟨Op, Op.Result⟩ α + +/-- Read the cell at address `a`. -/ +abbrev read (a : ℕ) : Prog ℕ := PFunctor.FreeM.lift (P := ⟨Op, Op.Result⟩) (.read a) + +/-- Write `v` to the cell at address `a`. -/ +abbrev write (a v : ℕ) : Prog Unit := PFunctor.FreeM.lift (P := ⟨Op, Op.Result⟩) (.write a v) + +/-- Allocate `k` zeroed cells and return their base address. -/ +abbrev alloc (k : ℕ) : Prog ℕ := PFunctor.FreeM.lift (P := ⟨Op, Op.Result⟩) (.alloc k) + +/-- Allocate `k` cells without clearing them and return their base address. -/ +abbrev allocDirty (k : ℕ) : Prog ℕ := + PFunctor.FreeM.lift (P := ⟨Op, Op.Result⟩) (.allocDirty k) + +@[simp] theorem step_read (s : State) (a : ℕ) : step s (.read a) = (s, s.mem a) := rfl + +@[simp] theorem step_write (s : State) (a v : ℕ) : + step s (.write a v) = (⟨Function.update s.mem a v, s.frontier⟩, ()) := rfl + +@[simp] theorem step_alloc (s : State) (k : ℕ) : + step s (.alloc k) = (⟨clear s.mem s.frontier k, s.frontier + k⟩, s.frontier) := rfl + +@[simp] theorem step_allocDirty (s : State) (k : ℕ) : + step s (.allocDirty k) = (⟨s.mem, s.frontier + k⟩, s.frontier) := rfl + +@[simp] theorem valid_read (w : ℕ) (s : State) (a : ℕ) : valid w s (.read a) ↔ a < s.frontier := + Iff.rfl + +@[simp] theorem valid_write (w : ℕ) (s : State) (a v : ℕ) : + valid w s (.write a v) ↔ a < s.frontier ∧ Fits w v := Iff.rfl + +@[simp] theorem valid_alloc (w : ℕ) (s : State) (k : ℕ) : + valid w s (.alloc k) ↔ Fits w (s.frontier + k) := Iff.rfl + +@[simp] theorem valid_allocDirty (w : ℕ) (s : State) (k : ℕ) : + valid w s (.allocDirty k) ↔ Fits w (s.frontier + k) := Iff.rfl + +@[simp] theorem charge_read (s : State) (a : ℕ) : charge s (.read a) = 1 := rfl + +@[simp] theorem charge_write (s : State) (a v : ℕ) : charge s (.write a v) = 1 := rfl + +@[simp] theorem charge_alloc (s : State) (k : ℕ) : charge s (.alloc k) = k + 1 := rfl + +@[simp] theorem charge_allocDirty (s : State) (k : ℕ) : charge s (.allocDirty k) = 1 := rfl + +/-- Admissibility of a concrete history is decidable. -/ +instance decidableValidHistory (w : ℕ) (s : State) (os : List Op) : + Decidable ((spec w).ValidHistory s os) := by + induction os generalizing s with + | nil => exact isTrue trivial + | cons o os ih => + exact decidable_of_iff (valid w s o ∧ (spec w).ValidHistory (step s o).1 os) Iff.rfl + +/-! ## Frontier and frame -/ + +/-- No operation lowers the frontier. -/ +theorem frontier_le_step (s : State) (o : Op) : s.frontier ≤ (step s o).1.frontier := by + cases o <;> simp + +/-- No sequence of operations lowers the frontier. -/ +theorem frontier_le_run {w : ℕ} (s : State) (os : List Op) : + s.frontier ≤ ((spec w).run s os).1.frontier := by + induction os generalizing s with + | nil => exact le_rfl + | cons o os ih => exact (frontier_le_step s o).trans (ih _) + +/-- An operation changes an allocated cell only by writing to it. Allocation clears cells at or +above the frontier only. -/ +theorem mem_step_of_lt_frontier (s : State) (o : Op) {a : ℕ} (ha : a < s.frontier) + (hw : ∀ v, o ≠ .write a v) : (step s o).1.mem a = s.mem a := by + cases o with + | read => rfl + | write b v => + have hab : a ≠ b := fun h => hw v (h ▸ rfl) + exact Function.update_of_ne hab v s.mem + | alloc k => exact clear_of_not_owns (by simp only [Block.Owns]; omega) + | allocDirty => rfl + +/-! ## Word fit -/ + +/-- The frontier and every cell fit a `w`-bit word. Admissible operations preserve this. -/ +structure WellFormed (w : ℕ) (s : State) : Prop where + /-- The frontier fits a word, hence so does every allocated address. -/ + frontier_fits : Fits w s.frontier + /-- Every cell holds a word. -/ + mem_fits : ∀ a, Fits w (s.mem a) + +/-- Allocated addresses fit a word. -/ +theorem WellFormed.fits_of_lt_frontier {w : ℕ} {s : State} (h : WellFormed w s) {a : ℕ} + (ha : a < s.frontier) : Fits w a := + h.frontier_fits.of_le ha.le + +/-- Admissible operations preserve well-formedness. -/ +theorem WellFormed.step {w : ℕ} {s : State} (h : WellFormed w s) {o : Op} (hv : valid w s o) : + WellFormed w (Memory.step s o).1 := by + cases o with + | read => exact h + | write a v => + refine ⟨h.frontier_fits, fun b => ?_⟩ + by_cases hb : b = a + · subst hb + simpa using hv.2 + · simpa [Function.update_of_ne hb] using h.mem_fits b + | alloc k => + refine ⟨hv, fun b => ?_⟩ + simp only [step_alloc, clear] + split + · exact fits_zero w + · exact h.mem_fits b + | allocDirty k => exact ⟨hv, h.mem_fits⟩ + +/-- Admissible histories preserve well-formedness. -/ +theorem WellFormed.run {w : ℕ} {s : State} (h : WellFormed w s) {os : List Op} + (hv : (spec w).ValidHistory s os) : WellFormed w ((spec w).run s os).1 := by + induction os generalizing s with + | nil => exact h + | cons o os ih => exact ih (h.step hv.1) hv.2 + +/-! ## The input convention -/ + +/-- The word RAM's input convention (`Cslib.WordRAM.Data.init`) as a memory state: the input +length at address `0`, the input words at addresses `1, …, n`, and everything up to the input +allocated. -/ +def State.init (input : List ℕ) : State := + ⟨(Data.init (r := 0) input).mem, input.length + 1⟩ + +/-- The input convention is well formed when the input and its frontier fit a word. -/ +theorem wellFormed_init {w : ℕ} {input : List ℕ} (hin : InputFits w input) + (hf : Fits w (input.length + 1)) : WellFormed w (State.init input) := by + refine ⟨hf, fun a => ?_⟩ + change Fits w (if a = 0 then input.length else input.getD (a - 1) 0) + split + · exact hin.1 + · rw [List.getD_eq_getElem?_getD] + cases hi : input[a - 1]? with + | none => exact fits_zero w + | some v => exact hin.2 v (List.mem_of_getElem? hi) + +end Cslib.DataStructure.Memory diff --git a/ComplexityTests/DataStructureMemory.lean b/ComplexityTests/DataStructureMemory.lean new file mode 100644 index 0000000000..01791c6332 --- /dev/null +++ b/ComplexityTests/DataStructureMemory.lean @@ -0,0 +1,117 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: local-only (test driver) + +public import Complexity.DataStructure.Memory.Field + +/-! +# Memory-oracle programs: named fields, pointer chasing and block reuse + +A singly linked list is stored as a struct of arrays with a `key` field and a `next` field; +pointers are indices, with `0` as null. The tests allocate the layout, build a two-node list, +chase pointers to sum the keys, and reuse one scratch block across two rounds. They check the +answers, the operation histories and charges, admissibility at an 8-bit word size, and two +rejected operations: a read above the frontier and a write of an oversized value. +-/ + +@[expose] public section + +namespace ComplexityTests.DataStructureMemory + +open Cslib.WordRAM Cslib.DataStructure Cslib.DataStructure.Memory + +/-- A linked-list node store as a struct of arrays. -/ +structure Nodes where + /-- The key of each node. -/ + key : Block + /-- The successor of each node, as an index; `0` is null. -/ + next : Block + +/-- Allocate both fields for `n` nodes. -/ +def Nodes.alloc (n : ℕ) : Prog Nodes := do + let key ← allocBlock n + let next ← allocBlock n + pure ⟨key, next⟩ + +/-- Follow at most `fuel` successors from node `v`, summing keys. Every address is computed from +an answer: the next node is read from memory. -/ +def Nodes.sumFrom (L : Nodes) : ℕ → ℕ → Prog ℕ + | 0, _ => pure 0 + | fuel + 1, v => do + let k ← L.key.get v + let nxt ← L.next.get v + if nxt = 0 then pure k else do + let rest ← L.sumFrom fuel nxt + pure (k + rest) + +/-- Build the list `1 → 2` with keys `5` and `7` (node `0` is reserved as null), then sum it. -/ +def linkedSum : Prog ℕ := do + let L ← Nodes.alloc 3 + L.key.set 1 5 + L.key.set 2 7 + L.next.set 1 2 + L.sumFrom 3 1 + +/-- The empty memory with nothing allocated. -/ +def empty : State := ⟨fun _ => 0, 0⟩ + +example : (spec 8).clientResult empty linkedSum = 12 := by decide + +/-- The history: two zeroed allocations, three writes and four reads by pointer chasing. -/ +example : (spec 8).clientHistory empty linkedSum = + [.alloc 3, .alloc 3, .write 1 5, .write 2 7, .write 4 2, + .read 1, .read 4, .read 2, .read 5] := by decide + +/-- The two zeroed allocations of three cells cost four each; seven unit operations follow. -/ +example : (spec 8).totalCost charge empty ((spec 8).clientHistory empty linkedSum) = 15 := by + decide + +/-- Every operation is admissible at an 8-bit word size. -/ +example : (spec 8).ValidHistory empty ((spec 8).clientHistory empty linkedSum) := by decide + +/-- One scratch block, allocated without clearing, is reused across two rounds. -/ +def reuse : Prog (ℕ × ℕ) := do + let scratch ← allocBlockDirty 1 + scratch.set 0 3 + let first ← scratch.get 0 + scratch.set 0 (first + 4) + let second ← scratch.get 0 + pure (first, second) + +example : (spec 8).clientResult empty reuse = (3, 7) := by decide + +example : ((spec 8).clientState empty reuse).frontier = 1 := by decide + +example : (spec 8).totalCost charge empty ((spec 8).clientHistory empty reuse) = 5 := by decide + +/-- A read above the frontier is rejected. -/ +example : ¬(spec 8).ValidHistory empty [.read 0] := by decide + +/-- A write of a value wider than the word is rejected, even at an allocated address. -/ +example : ¬(spec 8).ValidHistory ⟨fun _ => 0, 1⟩ [.write 0 256] := by decide + +/-- An allocation whose end does not fit the word is rejected. -/ +example : ¬(spec 8).ValidHistory empty [.alloc 256] := by decide + +/-- Well-formedness survives the pointer-chasing program. -/ +example : WellFormed 8 ((spec 8).clientState empty linkedSum) := by + have h : WellFormed 8 empty := ⟨by decide, fun _ => fits_zero 8⟩ + rw [← (spec 8).run_runClient] + exact h.run (by decide) + +/-- The input convention: the length at address `0`, the words from address `1`, and the +frontier just past them. -/ +example : (spec 8).clientResult (State.init [4, 9]) (do + let n ← read 0 + let x ← read 2 + pure (n, x)) = (2, 9) := by decide + +/-- The list view reads the live prefix of a field. -/ +example : (Block.mk 1 3).entries 2 (fun a => a * 10) = [10, 20] := by decide + +end ComplexityTests.DataStructureMemory From 412f614dacfeba6565117ed6b5eb8f586b7d0888 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:16:21 -0500 Subject: [PATCH 02/18] =?UTF-8?q?doc(bib):=20add=20Gu=C3=A9neau,=20Chargu?= =?UTF-8?q?=C3=A9raud=20and=20Pottier=20(ESOP=202018)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- references.bib | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/references.bib b/references.bib index e33942d5a5..0b4758dbf6 100644 --- a/references.bib +++ b/references.bib @@ -2303,6 +2303,18 @@ @inproceedings{Grover1996 doi = {10.1145/237814.237866} } +@inproceedings{GueneauChargueraudPottier2018, + author = {Gu{\'e}neau, Arma{\"e}l and Chargu{\'e}raud, Arthur and Pottier, Fran{\c{c}}ois}, + title = {A Fistful of Dollars: Formalizing Asymptotic Complexity Claims via Deductive + Program Verification}, + booktitle = {Programming Languages and Systems (ESOP 2018)}, + series = {Lecture Notes in Computer Science}, + pages = {533--560}, + publisher = {Springer}, + year = {2018}, + doi = {10.1007/978-3-319-89884-1_19} +} + @inproceedings{GuoKoppartySudan2013, author = {Guo, Alan and Kopparty, Swastik and Sudan, Madhu}, title = {New affine-invariant codes from lifting}, From 16a647fee63724bc26032a05d4bbf7a682232e1f Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:16:21 -0500 Subject: [PATCH 03/18] feat(DataStructure/StateTransition): refinements by abstraction functions with stacked potentials Refinement D E is a Module whose representation is an invariant plus an abstraction function (#2205 D2). Refinement.Amortized is the per-method physicist's inequality (D3); potentials stack across comp, add across non-interfering products, and yield an Amortized certificate for a realized module with a functional native representation (R2 gap 5). CostIsBigO is the exported asymptotic form (D6). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity.lean | 2 + .../StateTransition/Refinement.lean | 384 ++++++++++++++++++ .../Analysis/Asymptotics/CostBound.lean | 81 ++++ .../StateTransitionRefinement.lean | 143 +++++++ 4 files changed, 610 insertions(+) create mode 100644 Complexity/DataStructure/StateTransition/Refinement.lean create mode 100644 Complexity/Foundations/Analysis/Asymptotics/CostBound.lean create mode 100644 ComplexityTests/StateTransitionRefinement.lean diff --git a/Complexity.lean b/Complexity.lean index 7f9a77746d..9c6c1c30b9 100644 --- a/Complexity.lean +++ b/Complexity.lean @@ -2921,6 +2921,7 @@ public import Complexity.DataStructure.StateTransition.CostHoare public import Complexity.DataStructure.StateTransition.Hoare public import Complexity.DataStructure.StateTransition.Impl public import Complexity.DataStructure.StateTransition.Module +public import Complexity.DataStructure.StateTransition.Refinement public import Complexity.DataStructure.StateTransition.Resource public import Complexity.DataStructure.VanEmdeBoas.Basic public import Complexity.DataStructure.VanEmdeBoas.Elements @@ -2944,6 +2945,7 @@ public import Complexity.DataStructure.WordRAM.StateTransition public import Complexity.Foundations.Algebra.FiniteMagnitude public import Complexity.Foundations.Algebra.GeometricTail public import Complexity.Foundations.Algebra.Shear +public import Complexity.Foundations.Analysis.Asymptotics.CostBound public import Complexity.Foundations.Analysis.Asymptotics.Exponential public import Complexity.Foundations.Analysis.BernoulliInequality public import Complexity.Foundations.Analysis.ENNRealTsum diff --git a/Complexity/DataStructure/StateTransition/Refinement.lean b/Complexity/DataStructure/StateTransition/Refinement.lean new file mode 100644 index 0000000000..a70028a61c --- /dev/null +++ b/Complexity/DataStructure/StateTransition/Refinement.lean @@ -0,0 +1,384 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.DataStructure.StateTransition.Amortized +public import Complexity.DataStructure.StateTransition.CostHoare + +/-! +# Refinements by abstraction functions and stacked potentials + +A `Refinement D E` is a `Module D E` whose representation is single-valued: an implementation +invariant on lower states together with an abstraction function `abs : E.State → D.State`. Each +method, run from a lower state satisfying the invariant, must realize the specified step at the +abstract state, ending in a state satisfying the invariant. On deterministic specifications +(`Refinement.ofFn`) the obligation reads `D.stepFn (abs e) o = (abs e', a)`. + +A single-valued representation is what makes amortized costs compose. `Refinement.Amortized` +asks that every method satisfy the physicist's inequality +`lower cost + potential e' ≤ upper (abs e) o + potential e`, with a potential on lower states. +Composing two refinements adds their potentials, the upper one read through the lower +abstraction (`Refinement.Amortized.comp`). A relational representation would leave the +intermediate state existential, and the composed potential would not be a function of the lower +state. For the same reason an amortized refinement over a native implementation whose +representation is functional yields an `Amortized` certificate of the realized module +(`Refinement.Amortized.realize`). + +Products of refinements over one lower interface (`Refinement.prod`) need non-interference: the +methods of each side leave the other side's abstraction, invariant and potential unchanged. + +## Main definitions + +* `Refinement`: a module given by methods, an implementation invariant and an abstraction + function. +* `Refinement.ofFn`: the constructor for deterministic specifications. +* `Refinement.toModule`: the underlying relational module. +* `Refinement.id`, `Refinement.comp`, `Refinement.prod`: identity, composition, and products under + non-interference. +* `Refinement.Amortized`: per-method amortized cost refinement with a potential on lower states. +* `productCharge`: componentwise charges of a product specification. + +## Main results + +* `Refinement.simulates_demonic`, `Refinement.simulates_client`: whole adaptive clients run + through the abstraction function. +* `Refinement.comp_rep_iff`: composition agrees with `Module.comp` of the underlying modules. +* `Refinement.Amortized.demonic`, `Refinement.Amortized.client`: potentials telescope along + adaptive clients. +* `Refinement.Amortized.comp`: potentials stack across composition. +* `Refinement.Amortized.prod`: potentials add across non-interfering products. +* `Refinement.Amortized.realize`: an amortized certificate for the realized module. +* `Refinement.Amortized.id`: the identity refinement passes charges through. +* `Refinement.Amortized.chargeBound`, `Refinement.Amortized.of_chargeBound`: with zero + potential, the existing worst-case `Module.ChargeBound`. + +## References + +* [Cormen, Leiserson, Rivest and Stein, *Introduction to Algorithms*, Chapter 17][CLRS2009]. +-/ + +@[expose] public section + +namespace Cslib.DataStructure + +universe u v w x + +variable {D E F D₁ D₂ : StateTransitionSpec.{u, v, w}} {α : Type w} + +/-- A module whose representation is an implementation invariant `inv` together with an +abstraction function `abs`. From a lower state satisfying `inv`, every run of a method on an +admissible operation realizes a permitted step of the abstract state and preserves `inv`. -/ +structure Refinement (D E : StateTransitionSpec.{u, v, w}) where + /-- Bodies implementing the upper interface using lower-interface calls. -/ + methods : Methods D E + /-- The implementation invariant on lower states. -/ + inv : E.State → Prop + /-- The abstraction function from lower states to upper states. -/ + abs : E.State → D.State + /-- Each method is demonically admissible from a state satisfying the invariant. -/ + valid : ∀ {e} o, inv e → D.valid (abs e) o → E.DemonicValid e (methods o) + /-- Each method run realizes the specified step through `abs` and preserves `inv`. -/ + correct : ∀ {e} o {a e' tr}, inv e → D.valid (abs e) o → + E.ClientRun e (methods o) a e' tr → D.step (abs e) o (abs e') a ∧ inv e' + +namespace Refinement + +/-- The constructor for deterministic specifications: each method's history is admissible, and +`D.stepFn (abs e) o = (abs e', a)` for its final state `e'` and result `a`. -/ +abbrev ofFn {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} + {fE : (e : E.State) → (o : E.Op) → E.State × E.Result o} + [D.HasStepFn fD] [E.HasStepFn fE] + (methods : Methods D E) (inv : E.State → Prop) (abs : E.State → D.State) + (valid : ∀ {e} o, inv e → D.valid (abs e) o → + E.ValidHistory e (E.clientHistory e (methods o))) + (correct : ∀ {e} o, inv e → D.valid (abs e) o → + D.stepFn (abs e) o = (abs (E.clientState e (methods o)), E.clientResult e (methods o)) ∧ + inv (E.clientState e (methods o))) : + Refinement D E where + methods := methods + inv := inv + abs := abs + valid o hi hv := (E.demonicValid_iff_validHistory_runClient _ _).mpr (valid o hi hv) + correct {e} o {a e' tr} hi hv hrun := by + obtain ⟨_, rfl, rfl, _⟩ := (E.clientRun_iff_runClient e (methods o) a e' tr).mp hrun + obtain ⟨hstep, hi'⟩ := correct o hi hv + exact ⟨(D.step_iff_stepFn _ _ _ _).mpr hstep.symm, hi'⟩ + +variable (M : Refinement D E) + +/-- The underlying relational module: `s` is represented by `e` when `e` satisfies the invariant +and abstracts to `s`. -/ +def toModule : Module D E where + methods := M.methods + rep s e := M.inv e ∧ M.abs e = s + valid {_ _} o hr hv := by + obtain ⟨hi, rfl⟩ := hr + exact M.valid o hi hv + correct {_ _} o {_ _ _} hr hv hrun := by + obtain ⟨hi, rfl⟩ := hr + obtain ⟨hstep, hi'⟩ := M.correct o hi hv hrun + exact ⟨_, hstep, hi', rfl⟩ + +/-- Every run of a linked admissible client corresponds to a run of the client at the abstract +state, ending at the abstraction of the final lower state. -/ +theorem simulates_demonic (p : D.Client α) {e : E.State} (he : M.inv e) + (hv : D.DemonicValid (M.abs e) p) : + E.DemonicValid e (link M.methods p) ∧ + ∀ a e' tr_E, E.ClientRun e (link M.methods p) a e' tr_E → + M.inv e' ∧ ∃ tr_D, D.ClientRun (M.abs e) p a (M.abs e') tr_D := by + obtain ⟨hvE, hrun⟩ := M.toModule.simulates_demonic p ⟨he, rfl⟩ hv + refine ⟨hvE, fun a e' tr hr => ?_⟩ + obtain ⟨_, tr_D, hD, hi', rfl⟩ := hrun a e' tr hr + exact ⟨hi', tr_D, hD⟩ + +/-- On deterministic specifications, a linked client returns the specified result, and its final +lower state satisfies the invariant and abstracts to the specified final state. -/ +theorem simulates_client {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} + {fE : (e : E.State) → (o : E.Op) → E.State × E.Result o} + [D.HasStepFn fD] [E.HasStepFn fE] (p : D.Client α) {e : E.State} (he : M.inv e) + (hv : D.ValidHistory (M.abs e) (D.clientHistory (M.abs e) p)) : + E.ValidHistory e (E.clientHistory e (link M.methods p)) ∧ + E.clientResult e (link M.methods p) = D.clientResult (M.abs e) p ∧ + M.abs (E.clientState e (link M.methods p)) = D.clientState (M.abs e) p ∧ + M.inv (E.clientState e (link M.methods p)) := by + obtain ⟨hvE, hres, hi, hs⟩ := M.toModule.simulates_client p ⟨he, rfl⟩ hv + exact ⟨hvE, hres, hs, hi⟩ + +/-- The identity refinement: each operation calls itself, with no invariant. -/ +def id (D : StateTransitionSpec.{u, v, w}) : Refinement D D where + methods o := .liftBind o fun a => .pure a + inv _ := True + abs e := e + valid o _ hv := .liftBind o _ hv fun s' a _ => .pure s' a + correct {_} o {_ _ _} _ _ hrun := by + cases hrun with + | liftBind _ _ a _ hstep hrest => + cases hrest + exact ⟨hstep, trivial⟩ + +/-- Composition: the upper methods are linked through the lower ones, the invariants conjoin, +and the abstraction functions compose. -/ +def comp (M : Refinement D E) (N : Refinement E F) : Refinement D F where + methods o := link N.methods (M.methods o) + inv f := N.inv f ∧ M.inv (N.abs f) + abs f := M.abs (N.abs f) + valid o hi hv := (N.simulates_demonic (M.methods o) hi.1 (M.valid o hi.2 hv)).1 + correct {_} o {a f' tr} hi hv hrun := by + obtain ⟨hN', _, hE⟩ := + (N.simulates_demonic (M.methods o) hi.1 (M.valid o hi.2 hv)).2 a f' tr hrun + obtain ⟨hstep, hM'⟩ := M.correct o hi.2 hv hE + exact ⟨hstep, hN', hM'⟩ + +/-- Composing refinements represents the same states as composing their modules. -/ +theorem comp_rep_iff (M : Refinement D E) (N : Refinement E F) (s : D.State) (f : F.State) : + (M.comp N).toModule.rep s f ↔ (M.toModule.comp N.toModule).rep s f := by + constructor + · rintro ⟨⟨hN, hM⟩, rfl⟩ + exact ⟨_, ⟨hM, rfl⟩, hN, rfl⟩ + · rintro ⟨_, ⟨hM, rfl⟩, hN, rfl⟩ + exact ⟨⟨hN, hM⟩, rfl⟩ + +/-- Linking through a composition links through each refinement in turn. -/ +theorem link_comp (M : Refinement D E) (N : Refinement E F) (p : D.Client α) : + link (M.comp N).methods p = link N.methods (link M.methods p) := + (DataStructure.link_comp M.methods N.methods p).symm + +/-- Two refinements over one lower interface implement the product of their specifications, +provided the methods of each leave the other's abstraction and invariant unchanged. -/ +def prod (M₁ : Refinement D₁ E) (M₂ : Refinement D₂ E) + (frame₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₁.valid (M₁.abs e) o → + E.ClientRun e (M₁.methods o) a e' tr → M₂.abs e' = M₂.abs e ∧ M₂.inv e') + (frame₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₂.valid (M₂.abs e) o → + E.ClientRun e (M₂.methods o) a e' tr → M₁.abs e' = M₁.abs e ∧ M₁.inv e') : + Refinement (product D₁ D₂) E where + methods + | .inl o => M₁.methods o + | .inr o => M₂.methods o + inv e := M₁.inv e ∧ M₂.inv e + abs e := (M₁.abs e, M₂.abs e) + valid {_} o hi hv := by + cases o with + | inl o => exact M₁.valid o hi.1 hv + | inr o => exact M₂.valid o hi.2 hv + correct {_} o {_ _ _} hi hv hrun := by + cases o with + | inl o => + obtain ⟨hstep, hi₁⟩ := M₁.correct o hi.1 hv hrun + obtain ⟨habs, hi₂⟩ := frame₁ o hi.1 hi.2 hv hrun + exact ⟨⟨hstep, habs⟩, hi₁, hi₂⟩ + | inr o => + obtain ⟨hstep, hi₂⟩ := M₂.correct o hi.2 hv hrun + obtain ⟨habs, hi₁⟩ := frame₂ o hi.1 hi.2 hv hrun + exact ⟨⟨habs, hstep⟩, hi₁, hi₂⟩ + +/-! ## Amortized costs -/ + +/-- Amortized cost refinement: the lower charges of every method run, plus the potential after it, +are at most the upper charge of the operation plus the potential before it. Potentials are +natural numbers, hence nonnegative; a client bound subtracts nothing when the initial potential +is zero. -/ +def Amortized (lower : E.State → E.Op → ℕ) (upper : D.State → D.Op → ℕ) + (potential : E.State → ℕ) : Prop := + ∀ {e} o {a e' tr}, M.inv e → D.valid (M.abs e) o → E.ClientRun e (M.methods o) a e' tr → + E.totalCostTranscript lower e tr + potential e' ≤ upper (M.abs e) o + potential e + +variable {M} {lower : E.State → E.Op → ℕ} {upper : D.State → D.Op → ℕ} + {potential : E.State → ℕ} + +/-- The amortized inequality on a deterministic lower specification, along each method's +history. -/ +theorem Amortized.ofFn {fE : (e : E.State) → (o : E.Op) → E.State × E.Result o} + [E.HasStepFn fE] + (h : ∀ {e} o, M.inv e → D.valid (M.abs e) o → + E.totalCost lower e (E.clientHistory e (M.methods o)) + + potential (E.clientState e (M.methods o)) ≤ + upper (M.abs e) o + potential e) : + M.Amortized lower upper potential := by + intro e o a e' tr hi hv hrun + obtain ⟨_, _, rfl, rfl⟩ := (E.clientRun_iff_runClient e (M.methods o) a e' tr).mp hrun + rw [E.totalCostTranscript_canonicalTranscript] + exact h o hi hv + +/-- Potentials telescope along any run of a linked admissible client. -/ +theorem Amortized.demonic (A : M.Amortized lower upper potential) (p : D.Client α) + {e e' : E.State} {a : α} {tr_E : E.Transcript} (he : M.inv e) + (hv : D.DemonicValid (M.abs e) p) (hrun : E.ClientRun e (link M.methods p) a e' tr_E) : + M.inv e' ∧ ∃ tr_D, D.ClientRun (M.abs e) p a (M.abs e') tr_D ∧ + E.totalCostTranscript lower e tr_E + potential e' ≤ + D.totalCostTranscript upper (M.abs e) tr_D + potential e := by + induction p generalizing e tr_E with + | pure b => + cases hrun + exact ⟨he, [], .pure _ _, by simp⟩ + | liftBind o k ih => + obtain ⟨hv_o, hv_k⟩ := (D.demonicValid_liftBind_iff _ o k).mp hv + have heq : link M.methods (.liftBind o k) = + (M.methods o).bind (fun r => link M.methods (k r)) := rfl + rw [heq] at hrun + obtain ⟨res, e₁, tr₁, tr₂, hrun₁, hrun₂, rfl⟩ := E.clientRun_bind_iff.mp hrun + obtain ⟨hstep₁, he₁⟩ := M.correct o he hv_o hrun₁ + have hc₁ := A o he hv_o hrun₁ + obtain ⟨he₂, tr_D, hrun_D, hc₂⟩ := ih res he₁ (hv_k _ res hstep₁) hrun₂ + refine ⟨he₂, ⟨o, res, M.abs e₁⟩ :: tr_D, .liftBind o k res hv_o hstep₁ hrun_D, ?_⟩ + rw [E.totalCostTranscript_append lower hrun₁.validTranscript tr₂] + simp only [StateTransitionSpec.totalCostTranscript_cons] + omega + +/-- On deterministic specifications, the lower charges of a linked client plus the final +potential are at most the upper charges of its history plus the initial potential. -/ +theorem Amortized.client {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} + {fE : (e : E.State) → (o : E.Op) → E.State × E.Result o} + [D.HasStepFn fD] [E.HasStepFn fE] (A : M.Amortized lower upper potential) + (p : D.Client α) {e : E.State} (he : M.inv e) + (hv : D.ValidHistory (M.abs e) (D.clientHistory (M.abs e) p)) : + E.totalCost lower e (E.clientHistory e (link M.methods p)) + + potential (E.clientState e (link M.methods p)) ≤ + D.totalCost upper (M.abs e) (D.clientHistory (M.abs e) p) + potential e := by + have hvD := (D.demonicValid_iff_validHistory_runClient _ p).mpr hv + have hvE := (M.simulates_client p he hv).1 + obtain ⟨_, tr_D, hrun_D, hcost⟩ := A.demonic p he hvD (E.clientRun_canonical e _ hvE) + obtain ⟨_, _, _, rfl⟩ := (D.clientRun_iff_runClient _ p _ _ tr_D).mp hrun_D + simpa only [StateTransitionSpec.totalCostTranscript_canonicalTranscript] using hcost + +/-- **Potentials stack across composition.** If the upper refinement is amortized against the +middle charges and the lower refinement is amortized from the middle charges, the composite is +amortized with the sum of the upper potential, read through the lower abstraction, and the +lower potential. -/ +theorem Amortized.comp {N : Refinement E F} {lower : F.State → F.Op → ℕ} + {middle : E.State → E.Op → ℕ} {Φ : E.State → ℕ} {Ψ : F.State → ℕ} + (A : M.Amortized middle upper Φ) (B : N.Amortized lower middle Ψ) : + (M.comp N).Amortized lower upper (fun f => Φ (N.abs f) + Ψ f) := by + intro f o a f' tr hi hv hrun + obtain ⟨_, tr_E, hrun_E, hN⟩ := B.demonic (M.methods o) hi.1 (M.valid o hi.2 hv) hrun + have hM := A o hi.2 hv hrun_E + change F.totalCostTranscript lower f tr + (Φ (N.abs f') + Ψ f') ≤ + upper (M.abs (N.abs f)) o + (Φ (N.abs f) + Ψ f) + omega + +/-- Componentwise charges of a product specification. -/ +def productCharge (c₁ : D₁.State → D₁.Op → ℕ) (c₂ : D₂.State → D₂.Op → ℕ) : + (product D₁ D₂).State → (product D₁ D₂).Op → ℕ + | (s₁, _), .inl o => c₁ s₁ o + | (_, s₂), .inr o => c₂ s₂ o + +/-- **Potentials add across non-interfering products**, provided the methods of each side leave +the other side's potential unchanged. -/ +theorem Amortized.prod {M₁ : Refinement D₁ E} {M₂ : Refinement D₂ E} + {frame₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₁.valid (M₁.abs e) o → + E.ClientRun e (M₁.methods o) a e' tr → M₂.abs e' = M₂.abs e ∧ M₂.inv e'} + {frame₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₂.valid (M₂.abs e) o → + E.ClientRun e (M₂.methods o) a e' tr → M₁.abs e' = M₁.abs e ∧ M₁.inv e'} + {upper₁ : D₁.State → D₁.Op → ℕ} {upper₂ : D₂.State → D₂.Op → ℕ} + {Φ₁ Φ₂ : E.State → ℕ} + (A₁ : M₁.Amortized lower upper₁ Φ₁) (A₂ : M₂.Amortized lower upper₂ Φ₂) + (potential₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₁.valid (M₁.abs e) o → + E.ClientRun e (M₁.methods o) a e' tr → Φ₂ e' = Φ₂ e) + (potential₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₂.valid (M₂.abs e) o → + E.ClientRun e (M₂.methods o) a e' tr → Φ₁ e' = Φ₁ e) : + (M₁.prod M₂ frame₁ frame₂).Amortized lower (productCharge upper₁ upper₂) + (fun e => Φ₁ e + Φ₂ e) := by + intro e o a e' tr hi hv hrun + cases o with + | inl o => + have h := A₁ o hi.1 hv hrun + have hΦ := potential₁ o hi.1 hi.2 hv hrun + change E.totalCostTranscript lower e tr + (Φ₁ e' + Φ₂ e') ≤ + upper₁ (M₁.abs e) o + (Φ₁ e + Φ₂ e) + omega + | inr o => + have h := A₂ o hi.2 hv hrun + have hΦ := potential₂ o hi.1 hi.2 hv hrun + change E.totalCostTranscript lower e tr + (Φ₁ e' + Φ₂ e') ≤ + upper₂ (M₂.abs e) o + (Φ₁ e + Φ₂ e) + omega + +/-- **Potentials stack through a realization.** An amortized refinement over a native +implementation with an amortized certificate yields an amortized certificate of the realized +module, provided the native representation is functional: the represented lower state is +`absR c`. -/ +theorem Amortized.realize (A : M.Amortized lower upper potential) + {C τ : Type x} {exec : Execution E C τ} {R : StateTransitionImpl E exec} + {weight : τ → ℕ} {ψ : E.State → C → ℕ} (B : DataStructure.Amortized R weight ψ lower) + (absR : C → E.State) (hR : ∀ {e c}, R.rep e c → e = absR c) : + DataStructure.Amortized (M.toModule.realize R) (total weight) + (fun _ c => potential (absR c) + ψ (absR c) c) upper where + step {_ c o a c' ts} hr hv he := by + obtain ⟨e, ⟨hi, rfl⟩, hR₀⟩ := hr + obtain ⟨e', tr, hrun, hR', _, hcost⟩ := B.demonic hR₀ (M.valid o hi hv) he + obtain ⟨hstep, hi'⟩ := M.correct o hi hv hrun + have hc := A o hi hv hrun + refine ⟨M.abs e', hstep, ⟨e', ⟨hi', rfl⟩, hR'⟩, ?_⟩ + rw [← hR hR₀, ← hR hR'] + omega + +/-- The identity refinement passes each charge through unchanged. -/ +theorem Amortized.id (charge : D.State → D.Op → ℕ) : + (Refinement.id D).Amortized charge charge fun _ => 0 := by + intro e o a e' tr _ _ hrun + cases hrun with + | liftBind _ _ r _ _ hrest => + cases hrest + simp [Refinement.id] + +/-- With zero potential, an amortized refinement is a worst-case `Module.ChargeBound` of its +module. -/ +theorem Amortized.chargeBound (A : M.Amortized lower upper fun _ => 0) : + M.toModule.ChargeBound lower upper := by + rintro s e o a e' tr ⟨hi, rfl⟩ hv hrun + simpa using A o hi hv hrun + +/-- A worst-case `Module.ChargeBound` of the module is an amortized refinement with zero +potential. -/ +theorem Amortized.of_chargeBound (B : M.toModule.ChargeBound lower upper) : + M.Amortized lower upper fun _ => 0 := by + intro e o a e' tr hi hv hrun + simpa using B o ⟨hi, rfl⟩ hv hrun + +end Refinement + +end Cslib.DataStructure diff --git a/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean b/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean new file mode 100644 index 0000000000..d17a2b7965 --- /dev/null +++ b/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean @@ -0,0 +1,81 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.Init +public import Mathlib.Analysis.Asymptotics.Ring + +/-! +# Asymptotic cost bounds + +`Cslib.CostIsBigO l g size cost` says that the cost of every input is bounded by one cost +function of its size, chosen before the input: some monotone, nonnegative `f` with `f =O[l] g` +satisfies `cost a ≤ f (size a)` for all `a`. This is the form of asymptotic cost specification +used by Guéneau, Charguéraud and Pottier [GueneauChargueraudPottier2018]. Because `f` is chosen +once, the statement is `∃ f, ∀ a`. Inductive reasoning with "`O(1) + O(1) = O(1)`" proves +`∀ a, ∃ c` instead, which is unsound as a cost claim. The filter makes multivariate bounds +explicit: for example, `atTop` on `ℕ × ℕ` for a bound in two size parameters. + +Exact constants are proved internally and exported in this form: `CostIsBigO.of_le_nat` turns an +exact natural-number bound into an asymptotic one. + +## Main definitions + +* `Cslib.CostIsBigO`: a cost bounded by a monotone, nonnegative function that is `O(g)` along a + filter. + +## Main results + +* `Cslib.CostIsBigO.of_le_nat`: export an exact monotone natural-number bound. +* `Cslib.CostIsBigO.mono`: a smaller cost has the same bound. +* `Cslib.CostIsBigO.trans_isBigO`: weaken the comparison function. + +## References + +* [A. Guéneau, A. Charguéraud and F. Pottier, *A Fistful of Dollars: Formalizing Asymptotic + Complexity Claims via Deductive Program Verification*][GueneauChargueraudPottier2018] +-/ + +@[expose] public section + +open Asymptotics + +namespace Cslib + +variable {α ι : Type*} [Preorder ι] + +/-- The cost of every input `a` is at most `f (size a)` for one monotone, nonnegative cost +function `f` with `f =O[l] g`. -/ +def CostIsBigO (l : Filter ι) (g : ι → ℝ) (size : α → ι) (cost : α → ℕ) : Prop := + ∃ f : ι → ℝ, Monotone f ∧ 0 ≤ f ∧ f =O[l] g ∧ ∀ a, (cost a : ℝ) ≤ f (size a) + +namespace CostIsBigO + +variable {l : Filter ι} {g : ι → ℝ} {size : α → ι} {cost : α → ℕ} + +/-- An exact monotone natural-number bound that is `O(g)` gives an asymptotic cost bound. -/ +theorem of_le_nat {F : ι → ℕ} (hF : Monotone F) (hO : (fun n => (F n : ℝ)) =O[l] g) + (h : ∀ a, cost a ≤ F (size a)) : CostIsBigO l g size cost := + ⟨fun n => F n, fun _ _ hab => Nat.cast_le.mpr (hF hab), fun _ => Nat.cast_nonneg _, hO, + fun a => Nat.cast_le.mpr (h a)⟩ + +/-- A cost below a bounded cost has the same bound. -/ +theorem mono {cost' : α → ℕ} (h : CostIsBigO l g size cost) (hle : ∀ a, cost' a ≤ cost a) : + CostIsBigO l g size cost' := by + obtain ⟨f, hf, hf0, hO, hb⟩ := h + exact ⟨f, hf, hf0, hO, fun a => (Nat.cast_le.mpr (hle a)).trans (hb a)⟩ + +/-- A cost that is `O(g)` is `O(g')` whenever `g =O[l] g'`. -/ +theorem trans_isBigO {g' : ι → ℝ} (h : CostIsBigO l g size cost) (hg : g =O[l] g') : + CostIsBigO l g' size cost := by + obtain ⟨f, hf, hf0, hO, hb⟩ := h + exact ⟨f, hf, hf0, hO.trans hg, hb⟩ + +end CostIsBigO + +end Cslib diff --git a/ComplexityTests/StateTransitionRefinement.lean b/ComplexityTests/StateTransitionRefinement.lean new file mode 100644 index 0000000000..7288b6efcc --- /dev/null +++ b/ComplexityTests/StateTransitionRefinement.lean @@ -0,0 +1,143 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: local-only (test driver) + +public import Complexity.DataStructure.Queue.Spec +public import Complexity.DataStructure.StateTransition.Refinement +public import Complexity.Foundations.Analysis.Asymptotics.CostBound + +/-! +# Amortized refinements with a nonzero potential + +A bag supports adding an element and clearing. It is implemented over the stack interface: +`add` pushes, and `clear` asks for the size and pops that many times. A clear can cost +arbitrarily many stack calls, but each element pays for its own pop when it is added, so with +the potential "one credit per stacked element" every bag operation costs at most two stack calls. +The tests check this per-operation certificate, telescope it along a concrete client, stack it +over the identity refinement, and export the linear bound as `CostIsBigO`. +-/ + +@[expose] public section + +namespace ComplexityTests.StateTransitionRefinement + +open Cslib Cslib.DataStructure Cslib.DataStructure.Queue + +/-- Bag operations. -/ +inductive BagOp where + | add (n : ℕ) + | clear + deriving DecidableEq + +/-- A bag with `clear`, recording its elements newest first. -/ +abbrev bag : StateTransitionSpec := + StateTransitionSpec.ofFn (State := List ℕ) (Op := BagOp) (Result := fun _ => Unit) + fun s o => match o with + | .add n => (n :: s, ()) + | .clear => ([], ()) + +/-- Pop `n` times. -/ +def popAll : ℕ → (stack ℕ).Client Unit + | 0 => pure () + | n + 1 => .liftBind .pop fun _ => popAll n + +/-- Bag operations as stack programs. -/ +def methods : Methods bag (stack ℕ) + | .add n => .liftBind (.push n) fun _ => .pure () + | .clear => .liftBind .size fun n => popAll n + +/-- Popping as many times as the stack is long empties it, one call per element. -/ +theorem runClient_popAll (s : List ℕ) : + (stack ℕ).runClient s (popAll s.length) = ((), [], List.replicate s.length .pop) := by + induction s with + | nil => rfl + | cons a s ih => + change (stack ℕ).runClient (a :: s) (.liftBind StackOp.pop fun _ => popAll s.length) = _ + rw [StateTransitionSpec.runClient_liftBind] + change (let (v, s', os) := (stack ℕ).runClient s (popAll s.length) + (v, s', StackOp.pop :: os)) = _ + rw [ih] + rfl + +/-- The stack holds the bag itself; there is no invariant. -/ +def refinement : Refinement bag (stack ℕ) := + Refinement.ofFn methods (fun _ => True) id + (fun _ _ _ => (stack ℕ).validHistory_of_forall_valid (fun _ _ => trivial) _ _) + (by + intro s o _ _ + cases o with + | add n => exact ⟨rfl, trivial⟩ + | clear => + refine ⟨?_, trivial⟩ + simp [methods, StateTransitionSpec.clientState, runClient_popAll]) + +/-- Two calls per `add`, one per `clear`. -/ +def bagCharge (_ : List ℕ) : BagOp → ℕ + | .add _ => 2 + | .clear => 1 + +/-- One credit per stacked element pays for its eventual pop. -/ +theorem amortized : refinement.Amortized (fun _ _ => 1) bagCharge List.length := by + apply Refinement.Amortized.ofFn + intro s o _ _ + cases o with + | add n => + simp [refinement, methods, bagCharge, StateTransitionSpec.clientState, + StateTransitionSpec.clientHistory] + omega + | clear => + simp [refinement, methods, bagCharge, StateTransitionSpec.clientState, + StateTransitionSpec.clientHistory, runClient_popAll, StateTransitionSpec.totalCost_const] + +/-- A client with one expensive clear. -/ +def client : bag.Client Unit := do + bag.call (.add 1) + bag.call (.add 2) + bag.call (.add 3) + bag.call .clear + bag.call (.add 4) + +/-- The clear costs four stack calls, yet the five bag operations stay within their nine +amortized units, with one credit left for the remaining element. -/ +example : (stack ℕ).totalCost (fun _ _ => 1) [] + ((stack ℕ).clientHistory [] (link refinement.methods client)) = 8 ∧ + bag.totalCost bagCharge [] (bag.clientHistory [] client) = 9 ∧ + ((stack ℕ).clientState [] (link refinement.methods client)).length = 1 := by + decide + +/-- The general telescoped bound from an empty stack. -/ +theorem client_bound (p : bag.Client Unit) : + (stack ℕ).totalCost (fun _ _ => 1) [] ((stack ℕ).clientHistory [] (link refinement.methods p)) + + ((stack ℕ).clientState [] (link refinement.methods p)).length ≤ + bag.totalCost bagCharge [] (bag.clientHistory [] p) := by + have h := amortized.client p (e := []) trivial + (bag.validHistory_of_forall_valid (fun _ _ => trivial) _ _) + exact h + +/-- Stacking over the identity refinement adds a zero potential. -/ +example : (refinement.comp (Refinement.id (stack ℕ))).Amortized (fun _ _ => 1) bagCharge + (fun s => s.length + 0) := + amortized.comp (Refinement.Amortized.id _) + +/-- The exported form: the stack calls of a bag client from an empty stack are `O(m)` in the +number `m` of bag operations, with the constant `2`. -/ +example : CostIsBigO Filter.atTop (fun m : ℕ => (m : ℝ)) + (fun p : bag.Client Unit => (bag.clientHistory [] p).length) + (fun p => (stack ℕ).totalCost (fun _ _ => 1) [] + ((stack ℕ).clientHistory [] (link refinement.methods p))) := by + refine CostIsBigO.of_le_nat (F := fun m => 2 * m) (fun _ _ h => Nat.mul_le_mul_left 2 h) + ?_ fun p => ?_ + · simpa using (Asymptotics.isBigO_refl (fun m : ℕ => (m : ℝ)) _).const_mul_left 2 + · have h := client_bound p + have hc : bag.totalCost bagCharge [] (bag.clientHistory [] p) ≤ + (bag.clientHistory [] p).length * 2 := + bag.totalCost_le_length_mul bagCharge 2 (fun _ o _ => by cases o <;> simp [bagCharge]) + (bag.validHistory_of_forall_valid (fun _ _ => trivial) _ _) + omega + +end ComplexityTests.StateTransitionRefinement From 2f12c49d3c05a6905a3484b2b0efa0591cda0021 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:23:20 -0500 Subject: [PATCH 04/18] feat(DataStructure/StateTransition): weaken and rescale amortized refinements Amortized.mono and Amortized.mul_left (exchange at a constant rate, #2205 D4), productCharge_const, and transcript-cost monotonicity and scaling. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- .../StateTransition/Refinement.lean | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/Complexity/DataStructure/StateTransition/Refinement.lean b/Complexity/DataStructure/StateTransition/Refinement.lean index a70028a61c..ee80ab1581 100644 --- a/Complexity/DataStructure/StateTransition/Refinement.lean +++ b/Complexity/DataStructure/StateTransition/Refinement.lean @@ -43,6 +43,10 @@ methods of each side leave the other side's abstraction, invariant and potential * `Refinement.Amortized`: per-method amortized cost refinement with a potential on lower states. * `productCharge`: componentwise charges of a product specification. +The two transcript-cost lemmas `StateTransitionSpec.totalCostTranscript_mono` and +`StateTransitionSpec.totalCostTranscript_mul_left` sit here, beside their only clients, rather +than in the widely imported `StateTransition` module. + ## Main results * `Refinement.simulates_demonic`, `Refinement.simulates_client`: whole adaptive clients run @@ -54,6 +58,12 @@ methods of each side leave the other side's abstraction, invariant and potential * `Refinement.Amortized.prod`: potentials add across non-interfering products. * `Refinement.Amortized.realize`: an amortized certificate for the realized module. * `Refinement.Amortized.id`: the identity refinement passes charges through. +* `Refinement.Amortized.mono`, `Refinement.Amortized.mul_left`: weaken charges, and exchange + both currencies at a constant rate. +* `productCharge_const`: equal constant charges on a product. +* `StateTransitionSpec.totalCostTranscript_mono`, + `StateTransitionSpec.totalCostTranscript_mul_left`: monotonicity and scaling of transcript + costs. * `Refinement.Amortized.chargeBound`, `Refinement.Amortized.of_chargeBound`: with zero potential, the existing worst-case `Module.ChargeBound`. @@ -86,6 +96,25 @@ structure Refinement (D E : StateTransitionSpec.{u, v, w}) where correct : ∀ {e} o {a e' tr}, inv e → D.valid (abs e) o → E.ClientRun e (methods o) a e' tr → D.step (abs e) o (abs e') a ∧ inv e' +/-- Smaller charges total less along any transcript. -/ +theorem StateTransitionSpec.totalCostTranscript_mono {c c' : D.State → D.Op → ℕ} + (h : ∀ s o, c s o ≤ c' s o) (s : D.State) (tr : D.Transcript) : + D.totalCostTranscript c s tr ≤ D.totalCostTranscript c' s tr := by + induction tr generalizing s with + | nil => simp + | cons st tr ih => + simp only [StateTransitionSpec.totalCostTranscript_cons] + exact Nat.add_le_add (h _ _) (ih _) + +/-- Scaling every charge by `K` scales the total by `K`. -/ +theorem StateTransitionSpec.totalCostTranscript_mul_left (K : ℕ) (c : D.State → D.Op → ℕ) + (s : D.State) (tr : D.Transcript) : + D.totalCostTranscript (fun s o => K * c s o) s tr = K * D.totalCostTranscript c s tr := by + induction tr generalizing s with + | nil => simp + | cons st tr ih => + simp only [StateTransitionSpec.totalCostTranscript_cons, ih, Nat.mul_add] + namespace Refinement /-- The constructor for deterministic specifications: each method's history is admissible, and @@ -244,6 +273,27 @@ theorem Amortized.ofFn {fE : (e : E.State) → (o : E.Op) → E.State × E.Resul rw [E.totalCostTranscript_canonicalTranscript] exact h o hi hv +/-- Weaker charges: cheaper lower charges and dearer upper charges on admissible operations. -/ +theorem Amortized.mono (A : M.Amortized lower upper potential) + {lower' : E.State → E.Op → ℕ} {upper' : D.State → D.Op → ℕ} + (hlower : ∀ e o, lower' e o ≤ lower e o) + (hupper : ∀ s o, D.valid s o → upper s o ≤ upper' s o) : + M.Amortized lower' upper' potential := by + intro e o a e' tr hi hv hrun + have h := A o hi hv hrun + have hl := E.totalCostTranscript_mono hlower e tr + have hu := hupper _ o hv + omega + +/-- **Exchange at a constant rate.** Scaling both currencies and the potential by `K` preserves +the amortized inequality. -/ +theorem Amortized.mul_left (A : M.Amortized lower upper potential) (K : ℕ) : + M.Amortized (fun e o => K * lower e o) (fun s o => K * upper s o) + (fun e => K * potential e) := by + intro e o a e' tr hi hv hrun + rw [E.totalCostTranscript_mul_left, ← Nat.mul_add, ← Nat.mul_add] + exact Nat.mul_le_mul_left K (A o hi hv hrun) + /-- Potentials telescope along any run of a linked admissible client. -/ theorem Amortized.demonic (A : M.Amortized lower upper potential) (p : D.Client α) {e e' : E.State} {a : α} {tr_E : E.Transcript} (he : M.inv e) @@ -303,8 +353,14 @@ theorem Amortized.comp {N : Refinement E F} {lower : F.State → F.Op → ℕ} /-- Componentwise charges of a product specification. -/ def productCharge (c₁ : D₁.State → D₁.Op → ℕ) (c₂ : D₂.State → D₂.Op → ℕ) : (product D₁ D₂).State → (product D₁ D₂).Op → ℕ - | (s₁, _), .inl o => c₁ s₁ o - | (_, s₂), .inr o => c₂ s₂ o + | s, .inl o => c₁ s.1 o + | s, .inr o => c₂ s.2 o + +/-- Equal constant charges on both components are that constant charge on the product. -/ +@[simp] theorem productCharge_const (K : ℕ) : + productCharge (D₁ := D₁) (D₂ := D₂) (fun _ _ => K) (fun _ _ => K) = fun _ _ => K := by + funext s o + cases o <;> rfl /-- **Potentials add across non-interfering products**, provided the methods of each side leave the other side's potential unchanged. -/ From 98e3290dbc1bda9719bde60ccf0b57ecc992f5da Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:27:24 -0500 Subject: [PATCH 05/18] fix(DataStructure/StateTransition): place productCharge beside product, outside Refinement Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- .../StateTransition/Refinement.lean | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/Complexity/DataStructure/StateTransition/Refinement.lean b/Complexity/DataStructure/StateTransition/Refinement.lean index ee80ab1581..08e10929f6 100644 --- a/Complexity/DataStructure/StateTransition/Refinement.lean +++ b/Complexity/DataStructure/StateTransition/Refinement.lean @@ -96,6 +96,18 @@ structure Refinement (D E : StateTransitionSpec.{u, v, w}) where correct : ∀ {e} o {a e' tr}, inv e → D.valid (abs e) o → E.ClientRun e (methods o) a e' tr → D.step (abs e) o (abs e') a ∧ inv e' +/-- Componentwise charges of a product specification. -/ +def productCharge (c₁ : D₁.State → D₁.Op → ℕ) (c₂ : D₂.State → D₂.Op → ℕ) : + (product D₁ D₂).State → (product D₁ D₂).Op → ℕ + | s, .inl o => c₁ s.1 o + | s, .inr o => c₂ s.2 o + +/-- Equal constant charges on both components are that constant charge on the product. -/ +@[simp] theorem productCharge_const (K : ℕ) : + productCharge (D₁ := D₁) (D₂ := D₂) (fun _ _ => K) (fun _ _ => K) = fun _ _ => K := by + funext s o + cases o <;> rfl + /-- Smaller charges total less along any transcript. -/ theorem StateTransitionSpec.totalCostTranscript_mono {c c' : D.State → D.Op → ℕ} (h : ∀ s o, c s o ≤ c' s o) (s : D.State) (tr : D.Transcript) : @@ -350,18 +362,6 @@ theorem Amortized.comp {N : Refinement E F} {lower : F.State → F.Op → ℕ} upper (M.abs (N.abs f)) o + (Φ (N.abs f) + Ψ f) omega -/-- Componentwise charges of a product specification. -/ -def productCharge (c₁ : D₁.State → D₁.Op → ℕ) (c₂ : D₂.State → D₂.Op → ℕ) : - (product D₁ D₂).State → (product D₁ D₂).Op → ℕ - | s, .inl o => c₁ s.1 o - | s, .inr o => c₂ s.2 o - -/-- Equal constant charges on both components are that constant charge on the product. -/ -@[simp] theorem productCharge_const (K : ℕ) : - productCharge (D₁ := D₁) (D₂ := D₂) (fun _ _ => K) (fun _ _ => K) = fun _ _ => K := by - funext s o - cases o <;> rfl - /-- **Potentials add across non-interfering products**, provided the methods of each side leave the other side's potential unchanged. -/ theorem Amortized.prod {M₁ : Refinement D₁ E} {M₂ : Refinement D₂ E} From 56232f9738389bd1f3c285dfddc6b4cd1048c766 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:28:34 -0500 Subject: [PATCH 06/18] refactor(DataStructure/StateTransition): make Refinement.Amortized a structure A Prop-valued def whose body starts with an implicit binder is unfolded and auto-introduced during elaboration against an expected type, which broke term-mode uses such as Amortized.prod. A one-field structure, like the existing DataStructure.Amortized, avoids this. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- .../StateTransition/Refinement.lean | 40 ++++++++++--------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/Complexity/DataStructure/StateTransition/Refinement.lean b/Complexity/DataStructure/StateTransition/Refinement.lean index 08e10929f6..e6d94e5d59 100644 --- a/Complexity/DataStructure/StateTransition/Refinement.lean +++ b/Complexity/DataStructure/StateTransition/Refinement.lean @@ -263,10 +263,12 @@ def prod (M₁ : Refinement D₁ E) (M₂ : Refinement D₂ E) are at most the upper charge of the operation plus the potential before it. Potentials are natural numbers, hence nonnegative; a client bound subtracts nothing when the initial potential is zero. -/ -def Amortized (lower : E.State → E.Op → ℕ) (upper : D.State → D.Op → ℕ) - (potential : E.State → ℕ) : Prop := - ∀ {e} o {a e' tr}, M.inv e → D.valid (M.abs e) o → E.ClientRun e (M.methods o) a e' tr → - E.totalCostTranscript lower e tr + potential e' ≤ upper (M.abs e) o + potential e +structure Amortized (lower : E.State → E.Op → ℕ) (upper : D.State → D.Op → ℕ) + (potential : E.State → ℕ) : Prop where + /-- The physicist's inequality for one method run. -/ + step : ∀ {e} o {a e' tr}, M.inv e → D.valid (M.abs e) o → + E.ClientRun e (M.methods o) a e' tr → + E.totalCostTranscript lower e tr + potential e' ≤ upper (M.abs e) o + potential e variable {M} {lower : E.State → E.Op → ℕ} {upper : D.State → D.Op → ℕ} {potential : E.State → ℕ} @@ -280,7 +282,7 @@ theorem Amortized.ofFn {fE : (e : E.State) → (o : E.Op) → E.State × E.Resul potential (E.clientState e (M.methods o)) ≤ upper (M.abs e) o + potential e) : M.Amortized lower upper potential := by - intro e o a e' tr hi hv hrun + refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ obtain ⟨_, _, rfl, rfl⟩ := (E.clientRun_iff_runClient e (M.methods o) a e' tr).mp hrun rw [E.totalCostTranscript_canonicalTranscript] exact h o hi hv @@ -291,8 +293,8 @@ theorem Amortized.mono (A : M.Amortized lower upper potential) (hlower : ∀ e o, lower' e o ≤ lower e o) (hupper : ∀ s o, D.valid s o → upper s o ≤ upper' s o) : M.Amortized lower' upper' potential := by - intro e o a e' tr hi hv hrun - have h := A o hi hv hrun + refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ + have h := A.step o hi hv hrun have hl := E.totalCostTranscript_mono hlower e tr have hu := hupper _ o hv omega @@ -302,9 +304,9 @@ the amortized inequality. -/ theorem Amortized.mul_left (A : M.Amortized lower upper potential) (K : ℕ) : M.Amortized (fun e o => K * lower e o) (fun s o => K * upper s o) (fun e => K * potential e) := by - intro e o a e' tr hi hv hrun + refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ rw [E.totalCostTranscript_mul_left, ← Nat.mul_add, ← Nat.mul_add] - exact Nat.mul_le_mul_left K (A o hi hv hrun) + exact Nat.mul_le_mul_left K (A.step o hi hv hrun) /-- Potentials telescope along any run of a linked admissible client. -/ theorem Amortized.demonic (A : M.Amortized lower upper potential) (p : D.Client α) @@ -324,7 +326,7 @@ theorem Amortized.demonic (A : M.Amortized lower upper potential) (p : D.Client rw [heq] at hrun obtain ⟨res, e₁, tr₁, tr₂, hrun₁, hrun₂, rfl⟩ := E.clientRun_bind_iff.mp hrun obtain ⟨hstep₁, he₁⟩ := M.correct o he hv_o hrun₁ - have hc₁ := A o he hv_o hrun₁ + have hc₁ := A.step o he hv_o hrun₁ obtain ⟨he₂, tr_D, hrun_D, hc₂⟩ := ih res he₁ (hv_k _ res hstep₁) hrun₂ refine ⟨he₂, ⟨o, res, M.abs e₁⟩ :: tr_D, .liftBind o k res hv_o hstep₁ hrun_D, ?_⟩ rw [E.totalCostTranscript_append lower hrun₁.validTranscript tr₂] @@ -355,9 +357,9 @@ theorem Amortized.comp {N : Refinement E F} {lower : F.State → F.Op → ℕ} {middle : E.State → E.Op → ℕ} {Φ : E.State → ℕ} {Ψ : F.State → ℕ} (A : M.Amortized middle upper Φ) (B : N.Amortized lower middle Ψ) : (M.comp N).Amortized lower upper (fun f => Φ (N.abs f) + Ψ f) := by - intro f o a f' tr hi hv hrun + refine ⟨fun {f} o {a f' tr} hi hv hrun => ?_⟩ obtain ⟨_, tr_E, hrun_E, hN⟩ := B.demonic (M.methods o) hi.1 (M.valid o hi.2 hv) hrun - have hM := A o hi.2 hv hrun_E + have hM := A.step o hi.2 hv hrun_E change F.totalCostTranscript lower f tr + (Φ (N.abs f') + Ψ f') ≤ upper (M.abs (N.abs f)) o + (Φ (N.abs f) + Ψ f) omega @@ -378,16 +380,16 @@ theorem Amortized.prod {M₁ : Refinement D₁ E} {M₂ : Refinement D₂ E} E.ClientRun e (M₂.methods o) a e' tr → Φ₁ e' = Φ₁ e) : (M₁.prod M₂ frame₁ frame₂).Amortized lower (productCharge upper₁ upper₂) (fun e => Φ₁ e + Φ₂ e) := by - intro e o a e' tr hi hv hrun + refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ cases o with | inl o => - have h := A₁ o hi.1 hv hrun + have h := A₁.step o hi.1 hv hrun have hΦ := potential₁ o hi.1 hi.2 hv hrun change E.totalCostTranscript lower e tr + (Φ₁ e' + Φ₂ e') ≤ upper₁ (M₁.abs e) o + (Φ₁ e + Φ₂ e) omega | inr o => - have h := A₂ o hi.2 hv hrun + have h := A₂.step o hi.2 hv hrun have hΦ := potential₂ o hi.1 hi.2 hv hrun change E.totalCostTranscript lower e tr + (Φ₁ e' + Φ₂ e') ≤ upper₂ (M₂.abs e) o + (Φ₁ e + Φ₂ e) @@ -407,7 +409,7 @@ theorem Amortized.realize (A : M.Amortized lower upper potential) obtain ⟨e, ⟨hi, rfl⟩, hR₀⟩ := hr obtain ⟨e', tr, hrun, hR', _, hcost⟩ := B.demonic hR₀ (M.valid o hi hv) he obtain ⟨hstep, hi'⟩ := M.correct o hi hv hrun - have hc := A o hi hv hrun + have hc := A.step o hi hv hrun refine ⟨M.abs e', hstep, ⟨e', ⟨hi', rfl⟩, hR'⟩, ?_⟩ rw [← hR hR₀, ← hR hR'] omega @@ -415,7 +417,7 @@ theorem Amortized.realize (A : M.Amortized lower upper potential) /-- The identity refinement passes each charge through unchanged. -/ theorem Amortized.id (charge : D.State → D.Op → ℕ) : (Refinement.id D).Amortized charge charge fun _ => 0 := by - intro e o a e' tr _ _ hrun + refine ⟨fun {e} o {a e' tr} _ _ hrun => ?_⟩ cases hrun with | liftBind _ _ r _ _ hrest => cases hrest @@ -426,13 +428,13 @@ module. -/ theorem Amortized.chargeBound (A : M.Amortized lower upper fun _ => 0) : M.toModule.ChargeBound lower upper := by rintro s e o a e' tr ⟨hi, rfl⟩ hv hrun - simpa using A o hi hv hrun + simpa using A.step o hi hv hrun /-- A worst-case `Module.ChargeBound` of the module is an amortized refinement with zero potential. -/ theorem Amortized.of_chargeBound (B : M.toModule.ChargeBound lower upper) : M.Amortized lower upper fun _ => 0 := by - intro e o a e' tr hi hv hrun + refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ simpa using B o ⟨hi, rfl⟩ hv hrun end Refinement From fe7d60dfa1f451062f231851e21fa89d4b413f61 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:31:13 -0500 Subject: [PATCH 07/18] wip: array stack over memory and queue method lemmas Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity/DataStructure/Queue/Spec.lean | 59 +-- Complexity/DataStructure/Stack/Memory.lean | 397 +++++++++++++++++++++ 2 files changed, 434 insertions(+), 22 deletions(-) create mode 100644 Complexity/DataStructure/Stack/Memory.lean diff --git a/Complexity/DataStructure/Queue/Spec.lean b/Complexity/DataStructure/Queue/Spec.lean index 7a0ec18c25..ef315844d5 100644 --- a/Complexity/DataStructure/Queue/Spec.lean +++ b/Complexity/DataStructure/Queue/Spec.lean @@ -28,6 +28,8 @@ method is described in Chapter 17. ## Main results * `transfer_run`: stack operations reverse the rear contents into the front stack. +* `runClient_dequeue_nil`, `stepFn_methods`: the methods realize the FIFO step on the represented + list. * `potential_step`: at most three amortized stack calls, including the size query. * `realized_client`: every adaptive client has a correct backend execution with its bound. * `realized_client_bound`: every supplied linked execution satisfies correctness and the bound. @@ -123,6 +125,36 @@ theorem transfer_run (front back : List α) : (fun r : Unit × (stackPair α).State × List (stackPair α).Op => (r.1, r.2.1, .inr .pop :: .inl (.push a) :: r.2.2)) (ih (a :: front)) +/-- A dequeue from an empty front stack asks for the rear size, transfers the rear, and pops the +front again. -/ +theorem runClient_dequeue_nil (back : List α) : + (stackPair α).runClient ([], back) (methods .dequeue) = + (back.reverse.head?, (back.reverse.tail, []), + .inl .pop :: .inr .size :: (transferTrace back ++ [.inl .pop])) := by + simp only [methods, bind, StateTransitionSpec.runClient_liftBind, stackPair, stack, + List.tail_nil, List.head?_nil] + rw [StateTransitionSpec.runClient_bind, transfer_run] + simp + +/-- Each method returns the FIFO answer, and the stacks then represent the FIFO successor of the +represented list. -/ +theorem stepFn_methods (front back : List α) (o : QueueOp α) : + (queue α).stepFn (front ++ back.reverse) o = + (((stackPair α).clientState (front, back) (methods o)).1 ++ + ((stackPair α).clientState (front, back) (methods o)).2.reverse, + (stackPair α).clientResult (front, back) (methods o)) := by + cases o with + | enqueue n => + change ((front ++ back.reverse) ++ [n], ()) = (front ++ (n :: back).reverse, ()) + simp [List.reverse_cons, List.append_assoc] + | dequeue => + cases front with + | cons a front => rfl + | nil => + simp only [StateTransitionSpec.clientState, StateTransitionSpec.clientResult] + rw [runClient_dequeue_nil] + simp + /-- Representation and behavior are proved once, before selecting a backend or cost model. -/ def implementation : Module (queue α) (stackPair α) := Module.ofFn @@ -134,21 +166,8 @@ def implementation : Module (queue α) (stackPair α) := intro q s o hr _ subst q obtain ⟨front, back⟩ := s - cases o with - | enqueue n => - constructor - · rfl - · change (front ++ back.reverse) ++ [n] = front ++ (n :: back).reverse - simp [List.reverse_cons, List.append_assoc] - | dequeue => - cases front with - | cons a front => exact ⟨rfl, rfl⟩ - | nil => - simp only [methods, bind, StateTransitionSpec.clientResult, - StateTransitionSpec.clientState, StateTransitionSpec.runClient_liftBind, - stackPair, stack, List.tail_nil, List.head?_nil] - rw [StateTransitionSpec.runClient_bind, transfer_run] - simp) + have h := stepFn_methods front back o + exact ⟨(congrArg Prod.snd h).symm, congrArg Prod.fst h⟩) /-- Each transferred element incurs one pop and one push. -/ theorem transferTrace_length (back : List α) : (transferTrace back).length = 2 * back.length := by @@ -173,13 +192,9 @@ theorem potential_step (s : (stackPair α).State) (o : QueueOp α) : change 1 + 2 * back.length ≤ 3 + 2 * back.length omega | nil => - simp only [methods, bind, StateTransitionSpec.runClient_liftBind, stackPair, - stack, List.tail_nil, List.head?_nil] - rw [StateTransitionSpec.runClient_bind, transfer_run] - simp only [StateTransitionSpec.runClient_liftBind, StateTransitionSpec.runClient_pure] - change (transferTrace back ++ [Sum.inl StackOp.pop]).length + 2 ≤ 3 + 2 * back.length - rw [List.length_append, transferTrace_length] - simp only [List.length_singleton] + rw [runClient_dequeue_nil] + simp only [List.length_cons, List.length_append, List.length_nil] + rw [transferTrace_length] omega private theorem weighted_potential_step (K : Nat) {q : (queue α).State} {s : (stackPair α).State} diff --git a/Complexity/DataStructure/Stack/Memory.lean b/Complexity/DataStructure/Stack/Memory.lean new file mode 100644 index 0000000000..b70e19923f --- /dev/null +++ b/Complexity/DataStructure/Stack/Memory.lean @@ -0,0 +1,397 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.DataStructure.Memory.Field +public import Complexity.DataStructure.StateTransition.Refinement +public import Complexity.DataStructure.Stack.WordRAM.Basic + +/-! +# An array stack over the memory oracle + +The bounded stack `Stack.WordRAM.spec w C` is implemented by three short programs over +`Memory.spec w`. The storage is a `Layout` with two named fields: a one-cell `count` and an +`items` array of `C` cells, bottom first. The abstraction function reads the first `count` items +and reverses them, so the top comes first. Each operation is one read, two writes at most, and a +single branch; its evaluation is definitional (`runClient_push`, `runClient_pop_of_pos`, …), and +the refinement obligations follow from those equations and the list view of the array. + +Every operation costs at most three memory units, with no potential (`amortized`). The methods +write only the layout's cells and never allocate (`modifies_methods`), and the abstraction and +invariant depend only on those cells (`abs_congr`, `Inv.congr`). Two stacks with disjoint +layouts therefore implement the product of two stacks (`pairRefinement`). + +This is the same specification that `Stack.WordRAM` implements by a hand-stepped nine-register +dispatcher. Here no register, instruction count or word-fit chain appears; fits enter only +through the admissibility of `Memory.spec w`. + +## Main definitions + +* `Layout`, `Layout.footprint`, `Layout.Valid`: the two fields, the cells they own, and their + static well-formedness. +* `push`, `pop`, `size`, `methods`: the operations as memory programs. +* `abs`, `Inv`: the abstraction function and the implementation invariant. +* `refinement`: the array stack as a `Refinement` of the bounded stack. +* `pairRefinement`: two array stacks with disjoint layouts implement the pair of stacks. +* `allocLayout`: allocate an empty stack. + +## Main results + +* `amortized`: every operation costs at most three memory units. +* `pair_amortized`: so does every operation of the pair. +* `modifies_methods`, `abs_congr`, `Inv.congr`: the frame of a stack. +* `runClient_allocLayout_bind`, `inv_allocLayout`, `abs_allocLayout`: allocation yields an empty + stack in three memory units. + +## References + +* [T. H. Cormen, C. E. Leiserson, R. L. Rivest and C. Stein, *Introduction to Algorithms*, + Section 10.1][CLRS2009] +-/ + +@[expose] public section + +namespace Cslib.DataStructure.Stack.Memory + +open Cslib.WordRAM Cslib.DataStructure Cslib.DataStructure.Memory Cslib.DataStructure.Queue + +/-- The storage of an array stack: a one-cell field holding the number of items, and the item +array, bottom first. -/ +structure Layout where + /-- One cell holding the number of stored items. -/ + count : Block + /-- The items, bottom first. -/ + items : Block + +namespace Layout + +/-- The cells a stack owns. -/ +def footprint (L : Layout) : Finset ℕ := L.count.footprint ∪ L.items.footprint + +/-- Static well-formedness for capacity `C` at word size `w`: the count field is one cell outside +the item array, the array has `C` cells, and `C` fits a word. -/ +structure Valid (L : Layout) (w C : ℕ) : Prop where + /-- The count field is one cell. -/ + count_length : L.count.length = 1 + /-- The count cell is not an item. -/ + count_not_owns : ¬L.items.Owns L.count.base + /-- The item array has the capacity. -/ + items_length : L.items.length = C + /-- The capacity fits a word, so every count does. -/ + capacity_fits : Fits w C + +/-- The count cell belongs to the footprint. -/ +theorem count_mem_footprint (L : Layout) (hL : L.count.length = 1) : + L.count.base ∈ L.footprint := + Finset.mem_union_left _ (Block.mem_footprint.mpr ⟨le_rfl, by omega⟩) + +/-- Every item cell belongs to the footprint. -/ +theorem item_mem_footprint (L : Layout) {i : ℕ} (hi : i < L.items.length) : + L.items.base + i ∈ L.footprint := + Finset.mem_union_right _ (Block.mem_footprint.mpr (Block.owns_add hi)) + +end Layout + +variable (L : Layout) + +/-- Push: write the item above the top, then increment the count. -/ +def push (v : ℕ) : Prog Unit := do + let n ← L.count.get 0 + L.items.set n v + L.count.set 0 (n + 1) + +/-- Pop: an empty stack returns `none`; otherwise read the top item and decrement the count. -/ +def pop : Prog (Option ℕ) := do + let n ← L.count.get 0 + if n = 0 then pure none else do + let v ← L.items.get (n - 1) + L.count.set 0 (n - 1) + pure (some v) + +/-- Size: read the count. -/ +def size : Prog ℕ := L.count.get 0 + +/-- The stack operations as memory programs. -/ +def methods (w C : ℕ) : Methods (Stack.WordRAM.spec w C) (Memory.spec w) + | .push v => push L v + | .pop => pop L + | .size => size L + +/-- The stored stack, top first: the first `count` items, reversed. -/ +def abs (s : Memory.State) : List ℕ := (L.items.entries (s.mem L.count.base) s.mem).reverse + +/-- The implementation invariant: both fields are allocated and the count is within capacity. -/ +structure Inv (s : Memory.State) : Prop where + /-- The count cell is allocated. -/ + count_allocated : L.count.Allocated s + /-- The item array is allocated. -/ + items_allocated : L.items.Allocated s + /-- The count is at most the capacity. -/ + count_le : s.mem L.count.base ≤ L.items.length + +/-! ## Evaluation -/ + +variable {w : ℕ} + +/-- A push reads the count, writes the item and writes the new count. -/ +theorem runClient_push (s : Memory.State) (v : ℕ) : + (Memory.spec w).runClient s (push L v) = + ((), ⟨Function.update (Function.update s.mem (L.items.base + s.mem L.count.base) v) + L.count.base (s.mem L.count.base + 1), s.frontier⟩, + [.read L.count.base, .write (L.items.base + s.mem L.count.base) v, + .write L.count.base (s.mem L.count.base + 1)]) := rfl + +/-- A pop from an empty stack reads the count only. -/ +theorem runClient_pop_of_eq_zero (s : Memory.State) (h : s.mem L.count.base = 0) : + (Memory.spec w).runClient s (pop L) = (none, s, [.read L.count.base]) := by + simp [pop, h] + +/-- A pop from a nonempty stack reads the count and the top item and writes the new count. -/ +theorem runClient_pop_of_pos (s : Memory.State) (h : s.mem L.count.base ≠ 0) : + (Memory.spec w).runClient s (pop L) = + (some (s.mem (L.items.base + (s.mem L.count.base - 1))), + ⟨Function.update s.mem L.count.base (s.mem L.count.base - 1), s.frontier⟩, + [.read L.count.base, .read (L.items.base + (s.mem L.count.base - 1)), + .write L.count.base (s.mem L.count.base - 1)]) := by + unfold pop + simp only [Block.runClient_get_bind, Nat.add_zero, h, ↓reduceIte] + rfl + +/-- A size query reads the count. -/ +theorem runClient_size (s : Memory.State) : + (Memory.spec w).runClient s (size L) = (s.mem L.count.base, s, [.read L.count.base]) := rfl + +/-! ## The list view -/ + +variable {L} {C : ℕ} + +/-- The stack has as many elements as the count. -/ +@[simp] theorem length_abs (s : Memory.State) : (abs L s).length = s.mem L.count.base := by + simp [abs] + +/-- Writing the count leaves the items in place. -/ +theorem abs_update_count (hL : L.Valid w C) (m : ℕ → ℕ) (f : ℕ) {n : ℕ} (hn : n ≤ C) : + abs L ⟨Function.update m L.count.base n, f⟩ = (L.items.entries n m).reverse := by + simp only [abs, Function.update_self] + rw [Block.entries_update_of_not_owns _ (hL.items_length ▸ hn) hL.count_not_owns] + +/-- The abstraction and the invariant read only the layout's cells and the frontier. -/ +theorem abs_congr {s s' : Memory.State} (hL : L.Valid w C) (hs : Inv L s) + (h : ∀ a ∈ L.footprint, s'.mem a = s.mem a) : abs L s' = abs L s := by + have hc : s'.mem L.count.base = s.mem L.count.base := + h _ (L.count_mem_footprint hL.count_length) + simp only [abs, hc] + rw [Block.entries_congr _ _ fun i hi => h _ (L.item_mem_footprint (by + have := hs.count_le + omega))] + +/-- The invariant reads only the layout's cells and survives a growing frontier. -/ +theorem Inv.congr {s s' : Memory.State} (hL : L.Valid w C) (hs : Inv L s) + (h : ∀ a ∈ L.footprint, s'.mem a = s.mem a) (hf : s.frontier ≤ s'.frontier) : Inv L s' where + count_allocated := hs.count_allocated.mono hf + items_allocated := hs.items_allocated.mono hf + count_le := by + rw [h _ (L.count_mem_footprint hL.count_length)] + exact hs.count_le + +/-! ## The refinement -/ + +/-- The count cell is an admissible address. -/ +theorem Inv.count_lt (hL : L.Valid w C) {s : Memory.State} (hs : Inv L s) : + L.count.base < s.frontier := by + simpa using hs.count_allocated.lt_frontier (i := 0) (by rw [hL.count_length]; omega) + +/-- Item `i` below the capacity is an admissible address. -/ +theorem Inv.item_lt {s : Memory.State} (hs : Inv L s) {i : ℕ} (hi : i < L.items.length) : + L.items.base + i < s.frontier := + hs.items_allocated.lt_frontier hi + +/-- The array stack implements the bounded stack of capacity `C`. -/ +def refinement (hL : L.Valid w C) : Refinement (Stack.WordRAM.spec w C) (Memory.spec w) := + Refinement.ofFn (methods L w C) (Inv L) (abs L) + (by + intro s o hs hv + have hcount := hs.count_lt hL + have hC := hL.items_length + cases o with + | push v => + obtain ⟨hlen, hfit⟩ := hv + rw [length_abs] at hlen + simp only [methods, StateTransitionSpec.clientHistory, runClient_push] + simp only [StateTransitionSpec.validHistory_cons, StateTransitionSpec.stepFn, + step_read, step_write, valid_read, valid_write] + exact ⟨hcount, ⟨hs.item_lt (by omega), hfit⟩, ⟨hcount, + hL.capacity_fits.of_le (by omega)⟩, trivial⟩ + | pop => + by_cases h0 : s.mem L.count.base = 0 + · simp only [methods, StateTransitionSpec.clientHistory, runClient_pop_of_eq_zero L s h0] + exact ⟨hcount, trivial⟩ + · simp only [methods, StateTransitionSpec.clientHistory, runClient_pop_of_pos L s h0] + simp only [StateTransitionSpec.validHistory_cons, StateTransitionSpec.stepFn, + step_read, step_write, valid_read, valid_write] + have := hs.count_le + exact ⟨hcount, hs.item_lt (by omega), ⟨hcount, + hL.capacity_fits.of_le (by omega)⟩, trivial⟩ + | size => + simp only [methods, StateTransitionSpec.clientHistory, runClient_size] + exact ⟨hcount, trivial⟩) + (by + intro s o hs hv + have hle := hs.count_le + have hC := hL.items_length + cases o with + | push v => + obtain ⟨hlen, -⟩ := hv + rw [length_abs] at hlen + simp only [methods, StateTransitionSpec.clientState, StateTransitionSpec.clientResult, + runClient_push] + refine ⟨?_, ⟨hs.count_allocated, hs.items_allocated, by simp; omega⟩⟩ + rw [abs_update_count hL _ _ (by omega), Block.entries_succ, + Block.entries_update_of_le _ le_rfl, Function.update_self] + simp [abs] + | pop => + by_cases h0 : s.mem L.count.base = 0 + · simp only [methods, StateTransitionSpec.clientState, StateTransitionSpec.clientResult, + runClient_pop_of_eq_zero L s h0] + have he : abs L s = [] := List.eq_nil_of_length_eq_zero (by simp [h0]) + simp [he, hs] + · simp only [methods, StateTransitionSpec.clientState, StateTransitionSpec.clientResult, + runClient_pop_of_pos L s h0] + refine ⟨?_, ⟨hs.count_allocated, hs.items_allocated, by simp; omega⟩⟩ + obtain ⟨n, hn⟩ := Nat.exists_eq_succ_of_ne_zero h0 + have habs : abs L s = + s.mem (L.items.base + n) :: (L.items.entries n s.mem).reverse := by + simp [abs, hn, Block.entries_succ] + rw [abs_update_count hL _ _ (by omega), habs] + simp [hn] + | size => + simp only [methods, StateTransitionSpec.clientState, StateTransitionSpec.clientResult, + runClient_size] + exact ⟨by simp, hs⟩) + +/-- Every operation costs at most three memory units, so the stack needs no potential. -/ +theorem amortized (hL : L.Valid w C) : + (refinement hL).Amortized charge (fun _ _ => 3) fun _ => 0 := by + apply Refinement.Amortized.ofFn + intro s o _ _ + cases o with + | push v => simp [refinement, methods, StateTransitionSpec.clientHistory, runClient_push] + | pop => + by_cases h0 : s.mem L.count.base = 0 + · simp [refinement, methods, StateTransitionSpec.clientHistory, + runClient_pop_of_eq_zero L s h0] + · simp [refinement, methods, StateTransitionSpec.clientHistory, runClient_pop_of_pos L s h0] + | size => simp [refinement, methods, StateTransitionSpec.clientHistory, runClient_size] + +/-! ## The frame of a stack -/ + +/-- Each admissible operation writes only the layout's cells and never allocates. -/ +theorem modifies_methods (hL : L.Valid w C) {s : Memory.State} (o : StackOp ℕ) + (hv : (Stack.WordRAM.spec w C).valid (abs L s) o) : + (∀ a ∉ L.footprint, + ((Memory.spec w).clientState s (methods L w C o)).mem a = s.mem a) ∧ + ((Memory.spec w).clientState s (methods L w C o)).frontier = s.frontier := by + have hc : L.count.base ∈ L.footprint := L.count_mem_footprint hL.count_length + cases o with + | push v => + obtain ⟨hlen, -⟩ := hv + rw [length_abs] at hlen + have hi : L.items.base + s.mem L.count.base ∈ L.footprint := + L.item_mem_footprint (by rw [hL.items_length]; exact hlen) + simp only [methods, StateTransitionSpec.clientState, runClient_push] + exact ⟨fun a ha => by + rw [Function.update_of_ne (ne_of_mem_of_not_mem hc ha).symm, + Function.update_of_ne (ne_of_mem_of_not_mem hi ha).symm], trivial⟩ + | pop => + by_cases h0 : s.mem L.count.base = 0 + · simp only [methods, StateTransitionSpec.clientState, runClient_pop_of_eq_zero L s h0] + exact ⟨fun _ _ => trivial, trivial⟩ + · simp only [methods, StateTransitionSpec.clientState, runClient_pop_of_pos L s h0] + exact ⟨fun a ha => Function.update_of_ne (ne_of_mem_of_not_mem hc ha).symm _ _, trivial⟩ + | size => exact ⟨fun _ _ => rfl, rfl⟩ + +/-- A stack's operations leave a stack with a disjoint layout unchanged. -/ +theorem frame {L' : Layout} (hL : L.Valid w C) (hL' : L'.Valid w C) + (hd : Disjoint L.footprint L'.footprint) {s : Memory.State} (hs' : Inv L' s) + (o : StackOp ℕ) (hv : (Stack.WordRAM.spec w C).valid (abs L s) o) : + abs L' ((Memory.spec w).clientState s (methods L w C o)) = abs L' s ∧ + Inv L' ((Memory.spec w).clientState s (methods L w C o)) := by + obtain ⟨hm, hf⟩ := modifies_methods hL o hv + have h : ∀ a ∈ L'.footprint, + ((Memory.spec w).clientState s (methods L w C o)).mem a = s.mem a := + fun a ha => hm a (Finset.disjoint_right.mp hd ha) + exact ⟨abs_congr hL' hs' h, hs'.congr hL' h hf.ge⟩ + +/-! ## Two stacks in one memory -/ + +/-- Two array stacks with disjoint layouts implement a pair of bounded stacks. -/ +def pairRefinement {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) + (hd : Disjoint L₁.footprint L₂.footprint) : + Refinement (product (Stack.WordRAM.spec w C) (Stack.WordRAM.spec w C)) (Memory.spec w) := + (refinement h₁).prod (refinement h₂) + (fun {s} o {a s' tr} hs₁ hs₂ hv hrun => by + obtain ⟨_, _, rfl, _⟩ := ((Memory.spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun + exact frame h₁ h₂ hd hs₂ o hv) + (fun {s} o {a s' tr} hs₁ hs₂ hv hrun => by + obtain ⟨_, _, rfl, _⟩ := ((Memory.spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun + exact frame h₂ h₁ hd.symm hs₁ o hv) + +/-- Every operation of the pair costs at most three memory units. -/ +theorem pair_amortized {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) + (hd : Disjoint L₁.footprint L₂.footprint) : + (pairRefinement h₁ h₂ hd).Amortized charge (fun _ _ => 3) fun _ => 0 := by + have h : (pairRefinement h₁ h₂ hd).Amortized charge + (productCharge (fun _ _ => 3) (fun _ _ => 3)) fun _ => 0 := + (amortized h₁).prod (amortized h₂) (fun _ _ _ _ _ _ _ _ => rfl) + (fun _ _ _ _ _ _ _ _ => rfl) + rwa [productCharge_const] at h + +/-! ## Allocation -/ + +/-- Allocate an empty stack of capacity `C`: a zeroed count cell and an item array that is not +cleared. -/ +def allocLayout (C : ℕ) : Prog Layout := do + let count ← allocBlock 1 + let items ← allocBlockDirty C + pure ⟨count, items⟩ + +/-- Allocation places the count at the frontier and the items just above it. -/ +theorem runClient_allocLayout_bind {α : Type} (C : ℕ) (next : Layout → Prog α) + (s : Memory.State) : + (Memory.spec w).runClient s (allocLayout C >>= next) = + let (v, s', os) := (Memory.spec w).runClient + ⟨clear s.mem s.frontier 1, s.frontier + 1 + C⟩ + (next ⟨⟨s.frontier, 1⟩, ⟨s.frontier + 1, C⟩⟩) + (v, s', .alloc 1 :: .allocDirty C :: os) := rfl + +/-- The allocated layout is well formed when its end fits a word. -/ +theorem valid_allocLayout {f : ℕ} (hf : Fits w (f + 1 + C)) : + Layout.Valid ⟨⟨f, 1⟩, ⟨f + 1, C⟩⟩ w C where + count_length := rfl + count_not_owns := by simp [Block.Owns] + items_length := rfl + capacity_fits := hf.of_le (by omega) + +/-- The allocated stack satisfies the invariant and is empty. -/ +theorem inv_allocLayout (s : Memory.State) : + Inv ⟨⟨s.frontier, 1⟩, ⟨s.frontier + 1, C⟩⟩ ⟨clear s.mem s.frontier 1, s.frontier + 1 + C⟩ ∧ + abs ⟨⟨s.frontier, 1⟩, ⟨s.frontier + 1, C⟩⟩ + ⟨clear s.mem s.frontier 1, s.frontier + 1 + C⟩ = [] := by + have h0 : clear s.mem s.frontier 1 s.frontier = 0 := + clear_of_owns ⟨le_rfl, show s.frontier < s.frontier + 1 from Nat.lt_add_one _⟩ + refine ⟨⟨?_, ?_, ?_⟩, ?_⟩ + · change s.frontier + 1 ≤ s.frontier + 1 + C + omega + · change s.frontier + 1 + C ≤ s.frontier + 1 + C + exact le_rfl + · change clear s.mem s.frontier 1 s.frontier ≤ C + rw [h0] + exact Nat.zero_le C + · simp [abs, h0] + +end Cslib.DataStructure.Stack.Memory From aad5f3ee860b445a7bde96ab39ab69d50b6f6a0f Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:31:23 -0500 Subject: [PATCH 08/18] feat(Foundations): affine cost bounds are O(size) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- .../Foundations/Analysis/Asymptotics/CostBound.lean | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean b/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean index d17a2b7965..3a99eace52 100644 --- a/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean +++ b/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean @@ -32,6 +32,7 @@ exact natural-number bound into an asymptotic one. ## Main results * `Cslib.CostIsBigO.of_le_nat`: export an exact monotone natural-number bound. +* `Cslib.CostIsBigO.of_le_add_mul`: an affine bound `a + b * size` is `O(size)`. * `Cslib.CostIsBigO.mono`: a smaller cost has the same bound. * `Cslib.CostIsBigO.trans_isBigO`: weaken the comparison function. @@ -76,6 +77,18 @@ theorem trans_isBigO {g' : ι → ℝ} (h : CostIsBigO l g size cost) (hg : g =O obtain ⟨f, hf, hf0, hO, hb⟩ := h exact ⟨f, hf, hf0, hO.trans hg, hb⟩ +/-- A cost at most `a + b * size` is `O(size)` along `atTop`. -/ +theorem of_le_add_mul {size : α → ℕ} (a b : ℕ) (h : ∀ x, cost x ≤ a + b * size x) : + CostIsBigO Filter.atTop (fun n : ℕ => (n : ℝ)) size cost := by + refine of_le_nat (F := fun n => a + b * n) + (fun _ _ hmn => Nat.add_le_add_left (Nat.mul_le_mul_left b hmn) a) ?_ h + refine IsBigO.of_bound (a + b) ?_ + filter_upwards [Filter.eventually_ge_atTop 1] with n hn + have hn' : (1 : ℝ) ≤ n := by exact_mod_cast hn + rw [Real.norm_of_nonneg (by positivity), Real.norm_of_nonneg (by positivity)] + push_cast + nlinarith + end CostIsBigO end Cslib From 6698e86529717d14e635feadf264726885f9cbb9 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:34:38 -0500 Subject: [PATCH 09/18] wip: queue over memory Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity/DataStructure/Queue/Memory.lean | 185 +++++++++++++++++++++ Complexity/DataStructure/Stack/Memory.lean | 17 +- 2 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 Complexity/DataStructure/Queue/Memory.lean diff --git a/Complexity/DataStructure/Queue/Memory.lean b/Complexity/DataStructure/Queue/Memory.lean new file mode 100644 index 0000000000..28a37a7a87 --- /dev/null +++ b/Complexity/DataStructure/Queue/Memory.lean @@ -0,0 +1,185 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.DataStructure.Queue.WordRAM.Model +public import Complexity.DataStructure.Stack.Memory +public import Complexity.Foundations.Analysis.Asymptotics.CostBound + +/-! +# A queue over two array stacks over the memory oracle + +The bounded FIFO queue `Queue.WordRAM.spec w C` is implemented in two refinement steps, each +proved once: + +1. `refinement`: the two-stack queue `Queue.methods` over a pair of bounded stacks. It reuses the + queue's existing methods and their behavior (`Queue.stepFn_methods`) and potential argument + (`Queue.potential_step`); only the capacity and word-fit admissibility of the bounded stacks is + new. Each queue operation costs at most three stack calls, amortized, with two credits per + rear element. +2. `Stack.Memory.pairRefinement`: two array stacks with disjoint layouts over `Memory.spec w`, + each stack call costing at most three memory units. + +`memRefinement` composes them, and `mem_amortized` stacks the potentials across the composition +(`Refinement.Amortized.comp`): every queue operation costs at most nine memory units, amortized, +with six credits per rear element. `run` allocates the two stacks and then runs a queue client; +`run_correct` and `run_cost` state its result and its exact memory cost from any well-formed memory, +and `run_cost_isBigO` exports the cost as `O(m)` in the number `m` of queue operations. + +The machine-level counterpart is `Queue.WordRAM`, whose dispatcher is hand-stepped instruction by +instruction. Here the proofs mention neither registers nor instructions; executing the memory +programs on the word RAM is the separate realization step of #2205. + +## Main definitions + +* `boundedStacks`: the pair of bounded stacks. +* `Inv`: a FIFO list within capacity holding words. +* `refinement`: the two-stack queue over the pair of bounded stacks. +* `memRefinement`: the queue over two array stacks over memory. +* `run`: allocate the stacks, then run a queue client. + +## Main results + +* `amortized`: three stack calls per queue operation, with two credits per rear element. +* `mem_amortized`: nine memory units per queue operation, with six credits per rear element. +* `run_correct`, `run_cost`: the allocated queue answers like the specification and costs at + most `6 + 9 m` memory units for `m` queue operations. +* `run_cost_isBigO`: the same cost as `O(m)`. + +## References + +* [T. H. Cormen, C. E. Leiserson, R. L. Rivest and C. Stein, *Introduction to Algorithms*, + Exercise 10.1-6 and Chapter 17][CLRS2009] +-/ + +@[expose] public section + +namespace Cslib.DataStructure.Queue.Memory + +open Cslib.WordRAM Cslib.DataStructure Cslib.DataStructure.Memory Cslib.DataStructure.Queue + +/-- Two bounded stacks of capacity `C` at word size `w`. -/ +abbrev boundedStacks (w C : ℕ) : StateTransitionSpec := + product (Stack.WordRAM.spec w C) (Stack.WordRAM.spec w C) + +/-- A FIFO list within capacity `C` whose entries fit a `w`-bit word. -/ +structure Inv (w C : ℕ) (q : List ℕ) : Prop where + /-- The list is within capacity. -/ + length_le : q.length ≤ C + /-- Every entry fits a word. -/ + fits : ∀ x ∈ q, Fits w x + +namespace Inv + +variable {w C : ℕ} + +/-- Admissible FIFO steps keep the list within capacity and made of words. -/ +theorem step {q : List ℕ} (h : Inv w C q) {o : QueueOp ℕ} + (hv : (Queue.WordRAM.spec w C).valid q o) : Inv w C ((queue ℕ).stepFn q o).1 := by + cases o with + | enqueue n => + obtain ⟨hlen, hfit⟩ := hv + refine ⟨by simp; omega, fun x hx => ?_⟩ + simp only [List.mem_append, List.mem_singleton] at hx + rcases hx with hx | rfl + · exact h.fits x hx + · exact hfit + | dequeue => + exact ⟨by simp; have := h.length_le; omega, fun x hx => h.fits x (List.mem_of_mem_tail hx)⟩ + +end Inv + +variable {w C : ℕ} + +/-- The bounded stacks step exactly as the unbounded ones; only admissibility differs. -/ +theorem runClient_boundedStacks {β : Type} (s : List ℕ × List ℕ) (p : (stackPair ℕ).Client β) : + (boundedStacks w C).runClient s p = (stackPair ℕ).runClient s p := by + induction p generalizing s with + | pure a => rfl + | liftBind o k ih => + have hstep : (boundedStacks w C).stepFn s o = (stackPair ℕ).stepFn s o := by cases o <;> rfl + simp only [StateTransitionSpec.runClient_liftBind, hstep, ih] + +/-- Moving the rear into the front is admissible for bounded stacks when both fit together and +the rear holds words. -/ +theorem validHistory_transferTrace {front back : List ℕ} (hlen : front.length + back.length ≤ C) + (hfit : ∀ x ∈ back, Fits w x) : (boundedStacks w C).ValidHistory (front, back) (transferTrace back) := by + induction back generalizing front with + | nil => trivial + | cons a back ih => + simp only [List.length_cons] at hlen + refine ⟨trivial, ⟨by simp only; omega, hfit a (by simp)⟩, ?_⟩ + exact ih (front := a :: front) (by simp only [List.length_cons]; omega) + fun x hx => hfit x (by simp [hx]) + +/-- The two-stack queue over a pair of bounded stacks: the stacks represent `front ++ rear⁻¹`. -/ +def refinement (w C : ℕ) : Refinement (Queue.WordRAM.spec w C) (boundedStacks w C) := + Refinement.ofFn (methods (α := ℕ)) (fun s => Inv w C (Queue.WordRAM.contents s)) + Queue.WordRAM.contents + (by + rintro ⟨front, back⟩ o hs hv + have hlen := hs.length_le + simp only [Queue.WordRAM.contents, List.length_append, List.length_reverse] at hlen + cases o with + | enqueue n => + obtain ⟨hq, hfit⟩ := hv + simp only [Queue.WordRAM.contents, List.length_append, List.length_reverse] at hq + exact ⟨⟨by omega, hfit⟩, trivial⟩ + | dequeue => + cases front with + | cons a front => exact ⟨trivial, trivial⟩ + | nil => + change (boundedStacks w C).ValidHistory ([], back) + ((boundedStacks w C).runClient ([], back) (methods .dequeue)).2.2 + rw [runClient_boundedStacks, runClient_dequeue_nil] + refine ⟨trivial, trivial, ?_⟩ + rw [StateTransitionSpec.validHistory_append] + refine ⟨validHistory_transferTrace (by simpa using hlen) fun x hx => + hs.fits x (by simp [Queue.WordRAM.contents, hx]), trivial, trivial⟩) + (by + rintro ⟨front, back⟩ o hs hv + have h := stepFn_methods front back o + refine ⟨h, ?_⟩ + have hstep := hs.step hv + rw [show Queue.WordRAM.contents (front, back) = front ++ back.reverse from rfl, h] at hstep + exact hstep) + +/-- Each queue operation costs at most three stack calls, amortized, with two credits per rear +element. -/ +theorem amortized : + (refinement w C).Amortized (fun _ _ => 1) (fun _ _ => 3) fun s => 2 * s.2.length := + Refinement.Amortized.ofFn fun {s} o _ _ => by + have h := potential_step s o + rw [← runClient_boundedStacks (w := w) (C := C)] at h + rw [StateTransitionSpec.totalCost_one] + exact h + +/-! ## Over memory -/ + +/-- The queue over two array stacks over memory. -/ +def memRefinement {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) + (hd : Disjoint L₁.footprint L₂.footprint) : + Refinement (Queue.WordRAM.spec w C) (Memory.spec w) := + (refinement w C).comp (Stack.Memory.pairRefinement h₁ h₂ hd) + +/-- **Stacked potentials.** Every queue operation costs at most nine memory units, amortized, +with six credits per element of the rear stack: the queue's potential, at three memory units per +stack call, read through the stacks' abstraction. -/ +theorem mem_amortized {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) + (hd : Disjoint L₁.footprint L₂.footprint) : + (memRefinement h₁ h₂ hd).Amortized charge (fun _ _ => 9) + fun s => 6 * (Stack.Memory.abs L₂ s).length := by + have h := (amortized.mul_left 3).comp (Stack.Memory.pair_amortized h₁ h₂ hd) + have hΦ : (fun s : Memory.State => + 3 * (2 * (Stack.Memory.abs L₂ s).length) + 0) = + fun s => 6 * (Stack.Memory.abs L₂ s).length := by + funext s + omega + exact hΦ ▸ h + +end Cslib.DataStructure.Queue.Memory diff --git a/Complexity/DataStructure/Stack/Memory.lean b/Complexity/DataStructure/Stack/Memory.lean index b70e19923f..8f5069fe44 100644 --- a/Complexity/DataStructure/Stack/Memory.lean +++ b/Complexity/DataStructure/Stack/Memory.lean @@ -37,13 +37,15 @@ through the admissibility of `Memory.spec w`. * `push`, `pop`, `size`, `methods`: the operations as memory programs. * `abs`, `Inv`: the abstraction function and the implementation invariant. * `refinement`: the array stack as a `Refinement` of the bounded stack. -* `pairRefinement`: two array stacks with disjoint layouts implement the pair of stacks. +* `pairMethods`, `pairRefinement`: two array stacks with disjoint layouts implement the pair of + stacks. * `allocLayout`: allocate an empty stack. ## Main results * `amortized`: every operation costs at most three memory units. * `pair_amortized`: so does every operation of the pair. +* `pairRefinement_methods`: the pair's methods do not depend on the proofs about its layouts. * `modifies_methods`, `abs_congr`, `Inv.congr`: the frame of a stack. * `runClient_allocLayout_bind`, `inv_allocLayout`, `abs_allocLayout`: allocation yields an empty stack in three memory units. @@ -329,6 +331,12 @@ theorem frame {L' : Layout} (hL : L.Valid w C) (hL' : L'.Valid w C) /-! ## Two stacks in one memory -/ +/-- The operations of two stacks, each on its own layout. -/ +def pairMethods (L₁ L₂ : Layout) (w C : ℕ) : + Methods (product (Stack.WordRAM.spec w C) (Stack.WordRAM.spec w C)) (Memory.spec w) + | .inl o => methods L₁ w C o + | .inr o => methods L₂ w C o + /-- Two array stacks with disjoint layouts implement a pair of bounded stacks. -/ def pairRefinement {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) (hd : Disjoint L₁.footprint L₂.footprint) : @@ -341,6 +349,13 @@ def pairRefinement {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Val obtain ⟨_, _, rfl, _⟩ := ((Memory.spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun exact frame h₂ h₁ hd.symm hs₁ o hv) +/-- The pair's methods are `pairMethods`, whatever the proofs about the layouts. -/ +theorem pairRefinement_methods {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) + (hd : Disjoint L₁.footprint L₂.footprint) : + (pairRefinement h₁ h₂ hd).methods = pairMethods L₁ L₂ w C := by + funext o + cases o <;> rfl + /-- Every operation of the pair costs at most three memory units. -/ theorem pair_amortized {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) (hd : Disjoint L₁.footprint L₂.footprint) : From 7dda2489654c6b0a3a10ca4687f4147aae32e819 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:35:57 -0500 Subject: [PATCH 10/18] refactor(DataStructure/StateTransition): product answer types via Sum.elim With a match, the answer types of product D E mention D and E through the matcher's parameters, so the elaborator cannot identify clients of two products whose components differ only in admissibility (for example the pair of stacks and the pair of bounded stacks). Sum.elim D.Result E.Result depends only on the component answer types. Queue.Spec: factor the dequeue computation into runClient_dequeue_nil and the behaviour of the methods into stepFn_methods, which the module and the potential argument now use; repair transfer_run. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity/DataStructure/Queue/Spec.lean | 67 ++++++++++++------- Complexity/DataStructure/StateTransition.lean | 9 +-- 2 files changed, 46 insertions(+), 30 deletions(-) diff --git a/Complexity/DataStructure/Queue/Spec.lean b/Complexity/DataStructure/Queue/Spec.lean index 7a0ec18c25..0986e07b05 100644 --- a/Complexity/DataStructure/Queue/Spec.lean +++ b/Complexity/DataStructure/Queue/Spec.lean @@ -28,6 +28,8 @@ method is described in Chapter 17. ## Main results * `transfer_run`: stack operations reverse the rear contents into the front stack. +* `runClient_dequeue_nil`, `stepFn_methods`: the methods realize the FIFO step on the represented + list. * `potential_step`: at most three amortized stack calls, including the size query. * `realized_client`: every adaptive client has a correct backend execution with its bound. * `realized_client_bound`: every supplied linked execution satisfies correctness and the bound. @@ -118,10 +120,40 @@ theorem transfer_run (front back : List α) : induction back generalizing front with | nil => rfl | cons a back ih => - simpa [transfer, stackPair, stack, transferTrace, - List.reverse_cons, List.append_assoc] using congrArg - (fun r : Unit × (stackPair α).State × List (stackPair α).Op => - (r.1, r.2.1, .inr .pop :: .inl (.push a) :: r.2.2)) (ih (a :: front)) + change (let (v, s, os) := (stackPair α).runClient (a :: front, back) (transfer back.length) + (v, s, (Sum.inr StackOp.pop : (stackPair α).Op) :: Sum.inl (StackOp.push a) :: os)) = _ + rw [ih] + simp [transferTrace, List.reverse_cons, List.append_assoc] + +/-- A dequeue from an empty front stack asks for the rear size, transfers the rear, and pops the +front again. -/ +theorem runClient_dequeue_nil (back : List α) : + (stackPair α).runClient ([], back) (methods .dequeue) = + (back.reverse.head?, (back.reverse.tail, []), + .inl .pop :: .inr .size :: (transferTrace back ++ [.inl .pop])) := by + simp only [methods, bind, StateTransitionSpec.runClient_liftBind, stackPair, stack, + List.tail_nil, List.head?_nil] + rw [StateTransitionSpec.runClient_bind, transfer_run, List.append_nil] + rfl + +/-- Each method returns the FIFO answer, and the stacks then represent the FIFO successor of the +represented list. -/ +theorem stepFn_methods (front back : List α) (o : QueueOp α) : + (queue α).stepFn (front ++ back.reverse) o = + (((stackPair α).clientState (front, back) (methods o)).1 ++ + ((stackPair α).clientState (front, back) (methods o)).2.reverse, + (stackPair α).clientResult (front, back) (methods o)) := by + cases o with + | enqueue n => + change ((front ++ back.reverse) ++ [n], ()) = (front ++ (n :: back).reverse, ()) + simp [List.reverse_cons, List.append_assoc] + | dequeue => + cases front with + | cons a front => rfl + | nil => + simp only [StateTransitionSpec.clientState, StateTransitionSpec.clientResult] + rw [runClient_dequeue_nil] + simp /-- Representation and behavior are proved once, before selecting a backend or cost model. -/ def implementation : Module (queue α) (stackPair α) := @@ -134,21 +166,8 @@ def implementation : Module (queue α) (stackPair α) := intro q s o hr _ subst q obtain ⟨front, back⟩ := s - cases o with - | enqueue n => - constructor - · rfl - · change (front ++ back.reverse) ++ [n] = front ++ (n :: back).reverse - simp [List.reverse_cons, List.append_assoc] - | dequeue => - cases front with - | cons a front => exact ⟨rfl, rfl⟩ - | nil => - simp only [methods, bind, StateTransitionSpec.clientResult, - StateTransitionSpec.clientState, StateTransitionSpec.runClient_liftBind, - stackPair, stack, List.tail_nil, List.head?_nil] - rw [StateTransitionSpec.runClient_bind, transfer_run] - simp) + have h := stepFn_methods front back o + exact ⟨(congrArg Prod.snd h).symm, congrArg Prod.fst h⟩) /-- Each transferred element incurs one pop and one push. -/ theorem transferTrace_length (back : List α) : (transferTrace back).length = 2 * back.length := by @@ -173,13 +192,9 @@ theorem potential_step (s : (stackPair α).State) (o : QueueOp α) : change 1 + 2 * back.length ≤ 3 + 2 * back.length omega | nil => - simp only [methods, bind, StateTransitionSpec.runClient_liftBind, stackPair, - stack, List.tail_nil, List.head?_nil] - rw [StateTransitionSpec.runClient_bind, transfer_run] - simp only [StateTransitionSpec.runClient_liftBind, StateTransitionSpec.runClient_pure] - change (transferTrace back ++ [Sum.inl StackOp.pop]).length + 2 ≤ 3 + 2 * back.length - rw [List.length_append, transferTrace_length] - simp only [List.length_singleton] + rw [runClient_dequeue_nil] + simp only [List.length_cons, List.length_append, List.length_nil] + rw [transferTrace_length] omega private theorem weighted_potential_step (K : Nat) {q : (queue α).State} {s : (stackPair α).State} diff --git a/Complexity/DataStructure/StateTransition.lean b/Complexity/DataStructure/StateTransition.lean index 114e38b86c..8c0ae6041b 100644 --- a/Complexity/DataStructure/StateTransition.lean +++ b/Complexity/DataStructure/StateTransition.lean @@ -1098,13 +1098,14 @@ theorem eval_eq_liftM {D : StateTransitionSpec.{u, v, u}} | pure a => rfl | liftBind o k ih => exact ih (D.stepFn s o).2 (D.stepFn s o).1 -/-- Two independent abstract components. A backend must separately justify their independence. -/ +/-- Two independent abstract components. A backend must separately justify their independence. +The answer types are `Sum.elim D.Result E.Result`, so the signature of a product depends only on +the signatures of its components: clients of `product D E` are clients of `product D' E'` when +`D`, `D'` and `E`, `E'` differ only in their states' admissibility. -/ abbrev product (D E : StateTransitionSpec.{u, v, w}) : StateTransitionSpec where State := D.State × E.State Op := D.Op ⊕ E.Op - Result - | .inl o => D.Result o - | .inr o => E.Result o + Result := Sum.elim D.Result E.Result step s | .inl o, s', a => D.step s.1 o s'.1 a ∧ s'.2 = s.2 | .inr o, s', a => s'.1 = s.1 ∧ E.step s.2 o s'.2 a From 412ad374dc88c09a00992d8f8318792accd69144 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 17:42:57 -0500 Subject: [PATCH 11/18] feat(DataStructure): queue over two array stacks over the memory oracle The M1 slice of #2205. Stack.Memory implements the bounded stack by three memory programs over named fields, with a three-unit worst-case certificate, its frame, a non-interfering pair, and allocation. Queue.Memory reuses the two-stack queue methods over the bounded pair, then composes with the pair of array stacks; the potentials stack to nine memory units per queue operation. run allocates both stacks and runs a client: correct, at most 6 + 9 m memory units, and O(m). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity.lean | 2 + Complexity/DataStructure/Queue/Memory.lean | 181 ++++++++++++++++-- Complexity/DataStructure/Queue/Spec.lean | 30 --- Complexity/DataStructure/Stack/Memory.lean | 18 +- ComplexityTests/DataStructureQueueMemory.lean | 79 ++++++++ 5 files changed, 259 insertions(+), 51 deletions(-) create mode 100644 ComplexityTests/DataStructureQueueMemory.lean diff --git a/Complexity.lean b/Complexity.lean index b6a10c1394..8599d2709f 100644 --- a/Complexity.lean +++ b/Complexity.lean @@ -2902,6 +2902,7 @@ public import Complexity.DataStructure.PriorityQueue.WordRAM.FibonacciHeap.MinPQ public import Complexity.DataStructure.PriorityQueue.WordRAM.FibonacciHeap.Model public import Complexity.DataStructure.PriorityQueue.WordRAM.FibonacciHeap.Program public import Complexity.DataStructure.PriorityQueue.WordRAM.FibonacciHeap.Trees +public import Complexity.DataStructure.Queue.Memory public import Complexity.DataStructure.Queue.Spec public import Complexity.DataStructure.Queue.WordRAM.Basic public import Complexity.DataStructure.Queue.WordRAM.Dynamic @@ -2914,6 +2915,7 @@ public import Complexity.DataStructure.SearchTree.Impl public import Complexity.DataStructure.SplayTree.AccessLemma public import Complexity.DataStructure.SplayTree.Basic public import Complexity.DataStructure.SplayTree.Impl +public import Complexity.DataStructure.Stack.Memory public import Complexity.DataStructure.Stack.WordRAM.Basic public import Complexity.DataStructure.Stack.WordRAM.Controller public import Complexity.DataStructure.StateTransition diff --git a/Complexity/DataStructure/Queue/Memory.lean b/Complexity/DataStructure/Queue/Memory.lean index 28a37a7a87..d3fa6805cf 100644 --- a/Complexity/DataStructure/Queue/Memory.lean +++ b/Complexity/DataStructure/Queue/Memory.lean @@ -27,24 +27,28 @@ proved once: `memRefinement` composes them, and `mem_amortized` stacks the potentials across the composition (`Refinement.Amortized.comp`): every queue operation costs at most nine memory units, amortized, -with six credits per rear element. `run` allocates the two stacks and then runs a queue client; -`run_correct` and `run_cost` state its result and its exact memory cost from any well-formed memory, -and `run_cost_isBigO` exports the cost as `O(m)` in the number `m` of queue operations. +with six credits per rear element. `run` allocates the two stacks and then runs a queue client. +From any memory with room for both stacks, `run_correct` gives its answer and the admissibility of +its memory operations, `run_cost` bounds its memory cost by `6 + 9 m` for `m` queue operations, +and `run_cost_isBigO` exports that bound as `O(m)`. The machine-level counterpart is `Queue.WordRAM`, whose dispatcher is hand-stepped instruction by -instruction. Here the proofs mention neither registers nor instructions; executing the memory -programs on the word RAM is the separate realization step of #2205. +instruction. Here the proofs mention neither registers nor instructions. Executing the memory +programs on the word RAM is a separate, generic realization step, which this file does not +provide. ## Main definitions * `boundedStacks`: the pair of bounded stacks. * `Inv`: a FIFO list within capacity holding words. * `refinement`: the two-stack queue over the pair of bounded stacks. -* `memRefinement`: the queue over two array stacks over memory. +* `memRefinement`, `memMethods`: the queue over two array stacks over memory, and its methods as + plain memory programs. * `run`: allocate the stacks, then run a queue client. ## Main results +* `runClient_boundedStacks`: bounded stacks run clients exactly as unbounded ones. * `amortized`: three stack calls per queue operation, with two credits per rear element. * `mem_amortized`: nine memory units per queue operation, with six credits per rear element. * `run_correct`, `run_cost`: the allocated queue answers like the specification and costs at @@ -105,10 +109,29 @@ theorem runClient_boundedStacks {β : Type} (s : List ℕ × List ℕ) (p : (sta have hstep : (boundedStacks w C).stepFn s o = (stackPair ℕ).stepFn s o := by cases o <;> rfl simp only [StateTransitionSpec.runClient_liftBind, hstep, ih] +/-- The final state of a client of the bounded stacks. -/ +theorem clientState_boundedStacks {β : Type} (s : List ℕ × List ℕ) + (p : (stackPair ℕ).Client β) : + (boundedStacks w C).clientState s p = (stackPair ℕ).clientState s p := by + simp only [StateTransitionSpec.clientState, runClient_boundedStacks] + +/-- The result of a client of the bounded stacks. -/ +theorem clientResult_boundedStacks {β : Type} (s : List ℕ × List ℕ) + (p : (stackPair ℕ).Client β) : + (boundedStacks w C).clientResult s p = (stackPair ℕ).clientResult s p := by + simp only [StateTransitionSpec.clientResult, runClient_boundedStacks] + +/-- The history of a client of the bounded stacks. -/ +theorem clientHistory_boundedStacks {β : Type} (s : List ℕ × List ℕ) + (p : (stackPair ℕ).Client β) : + (boundedStacks w C).clientHistory s p = (stackPair ℕ).clientHistory s p := by + simp only [StateTransitionSpec.clientHistory, runClient_boundedStacks] + /-- Moving the rear into the front is admissible for bounded stacks when both fit together and the rear holds words. -/ theorem validHistory_transferTrace {front back : List ℕ} (hlen : front.length + back.length ≤ C) - (hfit : ∀ x ∈ back, Fits w x) : (boundedStacks w C).ValidHistory (front, back) (transferTrace back) := by + (hfit : ∀ x ∈ back, Fits w x) : + (boundedStacks w C).ValidHistory (front, back) (transferTrace back) := by induction back generalizing front with | nil => trivial | cons a back ih => @@ -129,14 +152,15 @@ def refinement (w C : ℕ) : Refinement (Queue.WordRAM.spec w C) (boundedStacks | enqueue n => obtain ⟨hq, hfit⟩ := hv simp only [Queue.WordRAM.contents, List.length_append, List.length_reverse] at hq - exact ⟨⟨by omega, hfit⟩, trivial⟩ + refine ⟨⟨?_, hfit⟩, trivial⟩ + change back.length < C + omega | dequeue => cases front with | cons a front => exact ⟨trivial, trivial⟩ | nil => - change (boundedStacks w C).ValidHistory ([], back) - ((boundedStacks w C).runClient ([], back) (methods .dequeue)).2.2 - rw [runClient_boundedStacks, runClient_dequeue_nil] + rw [clientHistory_boundedStacks, StateTransitionSpec.clientHistory, + runClient_dequeue_nil] refine ⟨trivial, trivial, ?_⟩ rw [StateTransitionSpec.validHistory_append] refine ⟨validHistory_transferTrace (by simpa using hlen) fun x hx => @@ -144,6 +168,7 @@ def refinement (w C : ℕ) : Refinement (Queue.WordRAM.spec w C) (boundedStacks (by rintro ⟨front, back⟩ o hs hv have h := stepFn_methods front back o + rw [clientState_boundedStacks, clientResult_boundedStacks] refine ⟨h, ?_⟩ have hstep := hs.step hv rw [show Queue.WordRAM.contents (front, back) = front ++ back.reverse from rfl, h] at hstep @@ -155,8 +180,10 @@ theorem amortized : (refinement w C).Amortized (fun _ _ => 1) (fun _ _ => 3) fun s => 2 * s.2.length := Refinement.Amortized.ofFn fun {s} o _ _ => by have h := potential_step s o - rw [← runClient_boundedStacks (w := w) (C := C)] at h - rw [StateTransitionSpec.totalCost_one] + change (boundedStacks w C).totalCost (fun _ _ => 1) s + ((boundedStacks w C).clientHistory s (methods o)) + + 2 * ((boundedStacks w C).clientState s (methods o)).2.length ≤ 3 + 2 * s.2.length + rw [StateTransitionSpec.totalCost_one, clientHistory_boundedStacks, clientState_boundedStacks] exact h /-! ## Over memory -/ @@ -182,4 +209,132 @@ theorem mem_amortized {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) omega exact hΦ ▸ h +/-! ## Allocation and a whole run -/ + +/-- The queue methods over two array stacks, as plain memory programs. -/ +def memMethods (L₁ L₂ : Stack.Memory.Layout) (w C : ℕ) : + Methods (Queue.WordRAM.spec w C) (Memory.spec w) := + fun o => link (Stack.Memory.pairMethods L₁ L₂ w C) (methods o) + +/-- The composite's methods are `memMethods`, whatever the proofs about the layouts. -/ +theorem memRefinement_methods {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) + (h₂ : L₂.Valid w C) (hd : Disjoint L₁.footprint L₂.footprint) : + (memRefinement h₁ h₂ hd).methods = memMethods L₁ L₂ w C := by + funext o + change link (Stack.Memory.pairRefinement h₁ h₂ hd).methods (methods o) = _ + rw [Stack.Memory.pairRefinement_methods] + rfl + +/-- Allocate two empty stacks of capacity `C`, then run a queue client on them. -/ +def run (w C : ℕ) {β : Type} (p : (Queue.WordRAM.spec w C).Client β) : Prog β := do + let L₁ ← Stack.Memory.allocLayout C + let L₂ ← Stack.Memory.allocLayout C + link (memMethods L₁ L₂ w C) p + +/-- The first stack allocated from `s`. -/ +abbrev frontLayout (s : Memory.State) (C : ℕ) : Stack.Memory.Layout := + ⟨⟨s.frontier, 1⟩, ⟨s.frontier + 1, C⟩⟩ + +/-- The second stack allocated from `s`. -/ +abbrev rearLayout (s : Memory.State) (C : ℕ) : Stack.Memory.Layout := + ⟨⟨s.frontier + 1 + C, 1⟩, ⟨s.frontier + 1 + C + 1, C⟩⟩ + +/-- The memory after both allocations. -/ +abbrev allocated (s : Memory.State) (C : ℕ) : Memory.State := + ⟨clear (clear s.mem s.frontier 1) (s.frontier + 1 + C) 1, s.frontier + 1 + C + 1 + C⟩ + +/-- A run allocates both stacks, then runs the linked client from the allocated memory. -/ +theorem runClient_run {β : Type} (p : (Queue.WordRAM.spec w C).Client β) (s : Memory.State) : + (Memory.spec w).runClient s (run w C p) = + let (v, s', os) := (Memory.spec w).runClient (allocated s C) + (link (memMethods (frontLayout s C) (rearLayout s C) w C) p) + (v, s', .alloc 1 :: .allocDirty C :: .alloc 1 :: .allocDirty C :: os) := rfl + +/-- The facts that start the refinement after allocation. -/ +private theorem setup (s : Memory.State) (hfit : Fits w (s.frontier + 2 * (C + 1))) : + ∃ (h₁ : (frontLayout s C).Valid w C) (h₂ : (rearLayout s C).Valid w C) + (hd : Disjoint (frontLayout s C).footprint (rearLayout s C).footprint), + (memRefinement h₁ h₂ hd).inv (allocated s C) ∧ + (memRefinement h₁ h₂ hd).abs (allocated s C) = [] ∧ + Stack.Memory.abs (rearLayout s C) (allocated s C) = [] := by + have h₁ : (frontLayout s C).Valid w C := Stack.Memory.valid_allocLayout (hfit.of_le (by omega)) + have h₂ : (rearLayout s C).Valid w C := Stack.Memory.valid_allocLayout (hfit.of_le (by omega)) + have hd : Disjoint (frontLayout s C).footprint (rearLayout s C).footprint := by + rw [Finset.disjoint_left] + intro a ha hb + simp only [Stack.Memory.Layout.footprint, Finset.mem_union, Block.mem_footprint, + Block.Owns] at ha hb + omega + -- The first stack, as allocated, before the second allocation. + obtain ⟨hi₁, ha₁⟩ := Stack.Memory.inv_allocLayout (C := C) s + -- The second stack, allocated from the state after the first. + obtain ⟨hi₂, ha₂⟩ := Stack.Memory.inv_allocLayout (C := C) + ⟨clear s.mem s.frontier 1, s.frontier + 1 + C⟩ + -- The second allocation leaves the first stack's cells alone. + have hframe : ∀ a ∈ (frontLayout s C).footprint, + (allocated s C).mem a = clear s.mem s.frontier 1 a := by + intro a ha + simp only [Stack.Memory.Layout.footprint, Finset.mem_union, Block.mem_footprint, + Block.Owns] at ha + exact clear_of_not_owns (by simp only [Block.Owns]; omega) + have hi₁' := hi₁.congr h₁ hframe (by simp only; omega) + have ha₁' := (Stack.Memory.abs_congr h₁ hi₁ hframe).trans ha₁ + refine ⟨h₁, h₂, hd, ⟨⟨hi₁', hi₂⟩, ?_⟩, ?_, ha₂⟩ + · change Inv w C (Queue.WordRAM.contents + (Stack.Memory.abs (frontLayout s C) (allocated s C), + Stack.Memory.abs (rearLayout s C) (allocated s C))) + rw [ha₁', ha₂] + exact ⟨by simp [Queue.WordRAM.contents], by simp [Queue.WordRAM.contents]⟩ + · change Queue.WordRAM.contents + (Stack.Memory.abs (frontLayout s C) (allocated s C), + Stack.Memory.abs (rearLayout s C) (allocated s C)) = [] + rw [ha₁', ha₂] + rfl + +/-- **Correctness.** From any memory with room for both stacks, an admissible queue client run +over two freshly allocated array stacks issues only admissible memory operations and returns the +specified answer. -/ +theorem run_correct {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Memory.State} + (hfit : Fits w (s.frontier + 2 * (C + 1))) + (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : + (Memory.spec w).ValidHistory s ((Memory.spec w).clientHistory s (run w C p)) ∧ + (Memory.spec w).clientResult s (run w C p) = (Queue.WordRAM.spec w C).clientResult [] p := by + obtain ⟨h₁, h₂, hd, hinv, habs, -⟩ := setup s hfit + obtain ⟨hvE, hres, -, -⟩ := + (memRefinement h₁ h₂ hd).simulates_client p hinv (by rw [habs]; exact hv) + rw [memRefinement_methods] at hvE hres + simp only [StateTransitionSpec.clientHistory, StateTransitionSpec.clientResult, + runClient_run] at hvE hres ⊢ + simp only [StateTransitionSpec.validHistory_cons, StateTransitionSpec.stepFn, step_alloc, + step_allocDirty, valid_alloc, valid_allocDirty] + refine ⟨⟨hfit.of_le (by omega), hfit.of_le (by omega), hfit.of_le (by omega), + hfit.of_le (by omega), hvE⟩, hres.trans (by rw [habs])⟩ + +/-- **Cost.** The same run costs at most `6 + 9 m` memory units for a client of `m` queue +operations: six for the two allocations and nine per operation, amortized. -/ +theorem run_cost {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Memory.State} + (hfit : Fits w (s.frontier + 2 * (C + 1))) + (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : + (Memory.spec w).totalCost charge s ((Memory.spec w).clientHistory s (run w C p)) ≤ + 6 + 9 * ((Queue.WordRAM.spec w C).clientHistory [] p).length := by + obtain ⟨h₁, h₂, hd, hinv, habs, hrear⟩ := setup s hfit + have h := (mem_amortized h₁ h₂ hd).client p hinv (by rw [habs]; exact hv) + rw [memRefinement_methods, habs, StateTransitionSpec.totalCost_const, hrear] at h + simp only [StateTransitionSpec.clientHistory, runClient_run, + StateTransitionSpec.totalCost_cons, charge_alloc, charge_allocDirty, + StateTransitionSpec.stepFn, step_alloc, step_allocDirty] at h ⊢ + simp only [List.length_nil, Nat.mul_zero, Nat.add_zero, allocated] at h ⊢ + omega + +/-- **The exported form.** Over admissible clients, the memory cost of a run is `O(m)` in the +number `m` of queue operations. -/ +theorem run_cost_isBigO (s : Memory.State) (hfit : Fits w (s.frontier + 2 * (C + 1))) : + CostIsBigO Filter.atTop (fun m : ℕ => (m : ℝ)) + (fun p : {p : (Queue.WordRAM.spec w C).Client Unit // + (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)} => + ((Queue.WordRAM.spec w C).clientHistory [] p.1).length) + (fun p => (Memory.spec w).totalCost charge s + ((Memory.spec w).clientHistory s (run w C p.1))) := + CostIsBigO.of_le_add_mul 6 9 fun p => run_cost p.1 hfit p.2 + end Cslib.DataStructure.Queue.Memory diff --git a/Complexity/DataStructure/Queue/Spec.lean b/Complexity/DataStructure/Queue/Spec.lean index a420660819..0986e07b05 100644 --- a/Complexity/DataStructure/Queue/Spec.lean +++ b/Complexity/DataStructure/Queue/Spec.lean @@ -155,36 +155,6 @@ theorem stepFn_methods (front back : List α) (o : QueueOp α) : rw [runClient_dequeue_nil] simp -/-- A dequeue from an empty front stack asks for the rear size, transfers the rear, and pops the -front again. -/ -theorem runClient_dequeue_nil (back : List α) : - (stackPair α).runClient ([], back) (methods .dequeue) = - (back.reverse.head?, (back.reverse.tail, []), - .inl .pop :: .inr .size :: (transferTrace back ++ [.inl .pop])) := by - simp only [methods, bind, StateTransitionSpec.runClient_liftBind, stackPair, stack, - List.tail_nil, List.head?_nil] - rw [StateTransitionSpec.runClient_bind, transfer_run] - simp - -/-- Each method returns the FIFO answer, and the stacks then represent the FIFO successor of the -represented list. -/ -theorem stepFn_methods (front back : List α) (o : QueueOp α) : - (queue α).stepFn (front ++ back.reverse) o = - (((stackPair α).clientState (front, back) (methods o)).1 ++ - ((stackPair α).clientState (front, back) (methods o)).2.reverse, - (stackPair α).clientResult (front, back) (methods o)) := by - cases o with - | enqueue n => - change ((front ++ back.reverse) ++ [n], ()) = (front ++ (n :: back).reverse, ()) - simp [List.reverse_cons, List.append_assoc] - | dequeue => - cases front with - | cons a front => rfl - | nil => - simp only [StateTransitionSpec.clientState, StateTransitionSpec.clientResult] - rw [runClient_dequeue_nil] - simp - /-- Representation and behavior are proved once, before selecting a backend or cost model. -/ def implementation : Module (queue α) (stackPair α) := Module.ofFn diff --git a/Complexity/DataStructure/Stack/Memory.lean b/Complexity/DataStructure/Stack/Memory.lean index 8f5069fe44..cf79d42258 100644 --- a/Complexity/DataStructure/Stack/Memory.lean +++ b/Complexity/DataStructure/Stack/Memory.lean @@ -17,9 +17,9 @@ public import Complexity.DataStructure.Stack.WordRAM.Basic The bounded stack `Stack.WordRAM.spec w C` is implemented by three short programs over `Memory.spec w`. The storage is a `Layout` with two named fields: a one-cell `count` and an `items` array of `C` cells, bottom first. The abstraction function reads the first `count` items -and reverses them, so the top comes first. Each operation is one read, two writes at most, and a -single branch; its evaluation is definitional (`runClient_push`, `runClient_pop_of_pos`, …), and -the refinement obligations follow from those equations and the list view of the array. +and reverses them, so the top comes first. Each operation is at most three memory operations and +one branch. Its evaluation is definitional (`runClient_push`, `runClient_pop_of_pos`, …), and the +refinement obligations follow from those equations and the list view of the array. Every operation costs at most three memory units, with no potential (`amortized`). The methods write only the layout's cells and never allocate (`modifies_methods`), and the abstraction and @@ -27,8 +27,8 @@ invariant depend only on those cells (`abs_congr`, `Inv.congr`). Two stacks with layouts therefore implement the product of two stacks (`pairRefinement`). This is the same specification that `Stack.WordRAM` implements by a hand-stepped nine-register -dispatcher. Here no register, instruction count or word-fit chain appears; fits enter only -through the admissibility of `Memory.spec w`. +dispatcher. Here no register or instruction count appears. Word fit enters only through the +admissibility of `Memory.spec w` and one static fact of the layout, that the capacity fits. ## Main definitions @@ -46,9 +46,11 @@ through the admissibility of `Memory.spec w`. * `amortized`: every operation costs at most three memory units. * `pair_amortized`: so does every operation of the pair. * `pairRefinement_methods`: the pair's methods do not depend on the proofs about its layouts. -* `modifies_methods`, `abs_congr`, `Inv.congr`: the frame of a stack. -* `runClient_allocLayout_bind`, `inv_allocLayout`, `abs_allocLayout`: allocation yields an empty - stack in three memory units. +* `runClient_push`, `runClient_pop_of_eq_zero`, `runClient_pop_of_pos`, `runClient_size`: the + operations evaluate definitionally. +* `modifies_methods`, `abs_congr`, `Inv.congr`, `frame`: the frame of a stack. +* `runClient_allocLayout_bind`, `valid_allocLayout`, `inv_allocLayout`: allocation yields a + well-formed empty stack in three memory units. ## References diff --git a/ComplexityTests/DataStructureQueueMemory.lean b/ComplexityTests/DataStructureQueueMemory.lean new file mode 100644 index 0000000000..93a8793360 --- /dev/null +++ b/ComplexityTests/DataStructureQueueMemory.lean @@ -0,0 +1,79 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: local-only (test driver) + +public import Complexity.DataStructure.Queue.Memory + +/-! +# A queue over two array stacks, run on concrete memory + +A client enqueues three words and dequeues four times; the third dequeue empties the rear stack +into the front. The run allocates both stacks in empty memory at word size `8` with capacity `3`. +The tests check its answers against the queue specification, its exact memory cost against the +proved bound `6 + 9 m`, the admissibility of every memory operation, and that the general +theorems apply: their room and admissibility hypotheses hold here. +-/ + +@[expose] public section + +namespace ComplexityTests.DataStructureQueueMemory + +open Cslib.WordRAM Cslib.DataStructure Cslib.DataStructure.Memory Cslib.DataStructure.Queue + Cslib.DataStructure.Queue.Memory + +/-- Enqueue `1, 2`, dequeue, enqueue `3`, then dequeue three times. -/ +def client : (Queue.WordRAM.spec 8 3).Client (List (Option ℕ)) := do + (Queue.WordRAM.spec 8 3).call (.enqueue 1) + (Queue.WordRAM.spec 8 3).call (.enqueue 2) + let a ← (Queue.WordRAM.spec 8 3).call .dequeue + (Queue.WordRAM.spec 8 3).call (.enqueue 3) + let b ← (Queue.WordRAM.spec 8 3).call .dequeue + let c ← (Queue.WordRAM.spec 8 3).call .dequeue + let d ← (Queue.WordRAM.spec 8 3).call .dequeue + pure [a, b, c, d] + +/-- Empty memory with nothing allocated. -/ +def empty : State := ⟨fun _ => 0, 0⟩ + +/-- FIFO answers, and an empty queue at the end. -/ +example : (Queue.WordRAM.spec 8 3).clientResult [] client = [some 1, some 2, some 3, none] := by + decide + +/-- The run over memory gives the same answers. -/ +example : (spec 8).clientResult empty (run 8 3 client) = [some 1, some 2, some 3, none] := by + decide + +/-- Its exact memory cost: 6 for the allocations and 43 for the seven queue operations, against +the proved bound `6 + 9 · 7 = 69`. The dequeue that moves two elements costs 17 alone. -/ +example : (spec 8).totalCost charge empty ((spec 8).clientHistory empty (run 8 3 client)) = 49 := by + decide + +/-- Every memory operation of the run is admissible. -/ +example : (spec 8).ValidHistory empty ((spec 8).clientHistory empty (run 8 3 client)) := by + decide + +/-- The client is admissible for the bounded queue. -/ +theorem client_valid : + (Queue.WordRAM.spec 8 3).ValidHistory [] ((Queue.WordRAM.spec 8 3).clientHistory [] client) := + ⟨⟨by decide, by decide⟩, ⟨by decide, by decide⟩, trivial, ⟨by decide, by decide⟩, trivial, + trivial, trivial, trivial⟩ + +/-- The general theorems apply: empty memory has room for both stacks at this word size. -/ +theorem room : Fits 8 (empty.frontier + 2 * (3 + 1)) := by decide + +example : (spec 8).clientResult empty (run 8 3 client) = + (Queue.WordRAM.spec 8 3).clientResult [] client := + (run_correct client room client_valid).2 + +example : (spec 8).totalCost charge empty ((spec 8).clientHistory empty (run 8 3 client)) ≤ 69 := + run_cost client room client_valid + +/-- A memory with too little room: the second stack would end past the 4-bit word range. -/ +example : ¬Fits 4 (empty.frontier + 2 * (7 + 1)) := by decide + +end ComplexityTests.DataStructureQueueMemory From f40fe1c5d742951bad6e0b74a4fed12607676ee8 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 18:48:07 -0500 Subject: [PATCH 12/18] feat(DataStructure): products over one memory, and glue charges for hop 3 Products over a shared memory (#2205 hop 3): - `Memory.Framed`: a refinement over `Memory.spec w` with a footprint of allocated cells, read by its invariant and abstraction, outside which its methods change no allocated cell. `Framed.ofStatic` for fixed layouts. - `Framed.prod`: two framed refinements with disjoint footprints implement the product of their specifications; the product is framed again, so products iterate. `Framed.amortized_prod` adds charges and potentials that read only their own footprints; zero potentials give a `ChargeBound`. - `Framed.of_fresh`, `Framed.prod_of_fresh`: allocation preserves structures and sets up products. - `Refinement.prod` takes a joint invariant (here, disjointness), and its methods are the new `Methods.prod`. The array stack is now `Stack.Memory.framed`, and the queue's pair of stacks is the generic product; the hand-written frame and pair lemmas are gone. Glue charges: `Refinement.Amortized.add`, `Amortized.glue` (glue at most `K` times a call's charge plus `K`), `Amortized.glue_of_one_le`, `StateTransitionSpec.totalCost_glue_le` and `Memory.one_le_charge`. `Queue.Memory.run_cost_glue` bounds memory charges and glue together by `(2K + 1)(6 + 9m)`. `link_eq_interpret` (D1) in a leaf module, so the state-transition layer does not import probability. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity.lean | 2 + Complexity/DataStructure/Memory/Frame.lean | 334 ++++++++++++++++++ Complexity/DataStructure/Memory/Spec.lean | 6 + Complexity/DataStructure/Queue/Memory.lean | 112 +++--- Complexity/DataStructure/Stack/Memory.lean | 114 +++--- .../DataStructure/StateTransition/Oracle.lean | 44 +++ .../StateTransition/Refinement.lean | 176 +++++++-- .../DataStructureMemoryProduct.lean | 110 ++++++ ComplexityTests/DataStructureQueueMemory.lean | 12 + 9 files changed, 752 insertions(+), 158 deletions(-) create mode 100644 Complexity/DataStructure/Memory/Frame.lean create mode 100644 Complexity/DataStructure/StateTransition/Oracle.lean create mode 100644 ComplexityTests/DataStructureMemoryProduct.lean diff --git a/Complexity.lean b/Complexity.lean index 8599d2709f..5f2e2464a6 100644 --- a/Complexity.lean +++ b/Complexity.lean @@ -2874,6 +2874,7 @@ public import Complexity.DataStructure.HashFamily.WordRAM.KWise public import Complexity.DataStructure.HashFamily.WordRAM.MultiplyShift public import Complexity.DataStructure.HashFamily.WordRAM.Tabulation public import Complexity.DataStructure.Memory.Field +public import Complexity.DataStructure.Memory.Frame public import Complexity.DataStructure.Memory.Spec public import Complexity.DataStructure.ObliviousSequence public import Complexity.DataStructure.Online.Basic @@ -2925,6 +2926,7 @@ public import Complexity.DataStructure.StateTransition.CostHoare public import Complexity.DataStructure.StateTransition.Hoare public import Complexity.DataStructure.StateTransition.Impl public import Complexity.DataStructure.StateTransition.Module +public import Complexity.DataStructure.StateTransition.Oracle public import Complexity.DataStructure.StateTransition.Refinement public import Complexity.DataStructure.StateTransition.Resource public import Complexity.DataStructure.VanEmdeBoas.Basic diff --git a/Complexity/DataStructure/Memory/Frame.lean b/Complexity/DataStructure/Memory/Frame.lean new file mode 100644 index 0000000000..9c41b35613 --- /dev/null +++ b/Complexity/DataStructure/Memory/Frame.lean @@ -0,0 +1,334 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.DataStructure.Memory.Spec +public import Complexity.DataStructure.StateTransition.Refinement + +/-! +# Footprints, and products of data structures sharing one memory + +A `Memory.Framed D w` is a refinement of `D` over `Memory.spec w` together with a footprint: the +finite set of allocated cells that holds the representation of each memory state. Three +obligations make the footprint honest. + +* `congr`: the invariant, the abstraction and the footprint itself read only the footprint's + cells, and survive a growing frontier. +* `modifies`: a method run changes no allocated cell outside the footprint, and the footprint + grows only into cells that the run allocated. +* `lt_frontier`: the footprint is allocated. + +Footprints may change from state to state, so that a structure can grow into cells it allocates +inside its operations. A structure laid out in fixed blocks has a constant footprint, and +`Framed.ofStatic` asks for less. Allocating inside an operation also needs room, since +`Memory.valid` asks the new frontier to fit a word. Neither the abstract state nor the structure's +own cells determine the frontier, and the other side of a product moves it, so such a structure +needs a shared budget of room, which this file does not provide. The structures built so far +allocate their storage once, up front. + +Two framed refinements implement the product of their specifications over one shared memory, +as long as their footprints are disjoint (`Framed.prod`). Disjointness is part of the product's +invariant. It holds after each method run because a run of one side writes only its own cells +and fresh ones, and the other side's footprint is allocated, hence not fresh. The product is +framed again, with the union of the footprints, so products iterate: +`(M₁.prod M₂).prod M₃` implements three structures in one memory. A potential that reads only the +footprint (`Framed.Local`) is left unchanged by the other side's methods, so amortized +certificates add across the product (`Framed.amortized_prod`), and so do worst-case +`Module.ChargeBound`s, as the case of zero potentials. + +Allocation is a memory program like any other. A run that leaves every allocated cell unchanged, +such as the allocation of a further structure, preserves a framed refinement (`Framed.of_fresh`), +and `Framed.prod_of_fresh` sets up a product from structures allocated one after the other. + +## Main definitions + +* `Memory.Framed`: a refinement over memory with a footprint. +* `Memory.Framed.ofStatic`: the constructor for a constant footprint. +* `Memory.Framed.Local`: a function of memory that reads only the footprint. +* `Memory.Framed.prod`: two framed refinements with disjoint footprints implement the product. + +## Main results + +* `Memory.frontier_le_of_clientRun`: a run never lowers the frontier. +* `Memory.mem_run_of_lt_frontier`, `Memory.mem_clientState_of_lt_frontier`: a run changes an + allocated cell only by writing it. +* `Memory.Framed.frame`: a method run of one structure leaves a structure with a disjoint + footprint unchanged. +* `Memory.Framed.prod_methods`: the product's methods are the two sides' methods. +* `Memory.Framed.amortized_prod`: potentials and charges add across the product. +* `Memory.Framed.of_fresh`, `Memory.Framed.prod_of_fresh`: allocation preserves structures and + sets up products. + +## References + +The footprint and frame discipline is the locality principle of separation logic, here with +explicit finite footprints as in `Cslib.WordRAM.HeapAssertion`; see +[J. C. Reynolds, *Separation Logic: A Logic for Shared Mutable Data Structures*][Reynolds2002]. +-/ + +@[expose] public section + +namespace Cslib.DataStructure.Memory + +variable {w : ℕ} + +/-! ## Runs and allocated cells -/ + +/-- No run of a memory program lowers the frontier. -/ +theorem frontier_le_of_clientRun {α : Type} {s s' : State} {p : Prog α} {a : α} + {tr : (spec w).Transcript} (h : (spec w).ClientRun s p a s' tr) : + s.frontier ≤ s'.frontier := by + obtain ⟨_, _, rfl, _⟩ := ((spec w).clientRun_iff_runClient _ _ _ _ _).mp h + rw [← (spec w).run_runClient] + exact frontier_le_run s _ + +/-- A sequence of operations changes an allocated cell only by writing it. -/ +theorem mem_run_of_lt_frontier (s : State) (os : List Op) {a : ℕ} (ha : a < s.frontier) + (hw : ∀ v, Op.write a v ∉ os) : ((spec w).run s os).1.mem a = s.mem a := by + induction os generalizing s with + | nil => rfl + | cons o os ih => + have hw' : ∀ v, Op.write a v ∉ os := fun v hv => hw v (List.mem_cons_of_mem o hv) + change ((spec w).run (step s o).1 os).1.mem a = s.mem a + rw [ih _ (ha.trans_le (frontier_le_step s o)) hw'] + exact mem_step_of_lt_frontier s o ha fun v h => hw v (h ▸ List.mem_cons_self) + +/-- A memory program changes an allocated cell only by writing it. -/ +theorem mem_clientState_of_lt_frontier {α : Type} (s : State) (p : Prog α) {a : ℕ} + (ha : a < s.frontier) (hw : ∀ v, Op.write a v ∉ (spec w).clientHistory s p) : + ((spec w).clientState s p).mem a = s.mem a := by + rw [← (spec w).run_runClient] + exact mem_run_of_lt_frontier s _ ha hw + +/-! ## Framed refinements -/ + +/-- A refinement of `D` over memory whose representation lives in a footprint of allocated +cells. The invariant, the abstraction and the footprint read only the footprint's cells; a +method run changes no allocated cell outside the footprint; and the footprint grows only into +cells at or above the old frontier, which the run allocated. -/ +structure Framed (D : StateTransitionSpec.{0, 0, 0}) (w : ℕ) extends Refinement D (spec w) where + /-- The cells holding the representation. -/ + footprint : State → Finset ℕ + /-- The footprint is allocated. -/ + lt_frontier : ∀ {s a}, inv s → a ∈ footprint s → a < s.frontier + /-- The invariant, the abstraction and the footprint read only the footprint's cells, and + survive a growing frontier. -/ + congr : ∀ {s s'}, inv s → (∀ a ∈ footprint s, s'.mem a = s.mem a) → + s.frontier ≤ s'.frontier → inv s' ∧ abs s' = abs s ∧ footprint s' = footprint s + /-- A method run changes no allocated cell outside the footprint, and the footprint grows + only into fresh cells. -/ + modifies : ∀ {s} o {a s' tr}, inv s → D.valid (abs s) o → + (spec w).ClientRun s (methods o) a s' tr → + (∀ b < s.frontier, b ∉ footprint s → s'.mem b = s.mem b) ∧ + ∀ b ∈ footprint s', b ∈ footprint s ∨ s.frontier ≤ b + +namespace Framed + +variable {D D₁ D₂ : StateTransitionSpec.{0, 0, 0}} + +/-- The constructor for a constant footprint `fp`: the invariant and abstraction read only `fp`, +and each method's final state agrees with its initial state on the allocated cells outside +`fp`. -/ +def ofStatic (M : Refinement D (spec w)) (fp : Finset ℕ) + (lt_frontier : ∀ {s a}, M.inv s → a ∈ fp → a < s.frontier) + (congr : ∀ {s s'}, M.inv s → (∀ a ∈ fp, s'.mem a = s.mem a) → + s.frontier ≤ s'.frontier → M.inv s' ∧ M.abs s' = M.abs s) + (modifies : ∀ {s} o, M.inv s → D.valid (M.abs s) o → ∀ b < s.frontier, b ∉ fp → + ((spec w).clientState s (M.methods o)).mem b = s.mem b) : + Framed D w where + toRefinement := M + footprint _ := fp + lt_frontier := lt_frontier + congr hs h hf := ⟨(congr hs h hf).1, (congr hs h hf).2, rfl⟩ + modifies {s} o {a s' tr} hs hv hrun := by + obtain ⟨_, _, rfl, _⟩ := ((spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun + exact ⟨modifies o hs hv, fun b hb => .inl hb⟩ + +variable (M : Framed D w) + +/-- A function of memory reads only the footprint: it is unchanged when the footprint's cells +are and the frontier grows. The abstraction is one (`Local.abs`); so are potentials that a +product must keep apart. -/ +def Local {β : Type*} (f : State → β) : Prop := + ∀ {s s'}, M.inv s → (∀ a ∈ M.footprint s, s'.mem a = s.mem a) → + s.frontier ≤ s'.frontier → f s' = f s + +/-- The abstraction reads only the footprint. -/ +theorem Local.abs : M.Local M.abs := fun hs h hf => (M.congr hs h hf).2.1 + +/-- A constant reads no cell. -/ +theorem Local.const {β : Type*} (b : β) : M.Local fun _ => b := fun _ _ _ => rfl + +/-- Sums of local functions are local. -/ +theorem Local.add {f g : State → ℕ} (hf : M.Local f) (hg : M.Local g) : + M.Local fun s => f s + g s := fun hs h hfr => + congrArg₂ (· + ·) (hf hs h hfr) (hg hs h hfr) + +/-- A run that leaves every allocated cell unchanged preserves a framed refinement. Allocating +another structure is such a run. -/ +theorem of_fresh {s s' : State} (hs : M.inv s) + (hmem : ∀ a < s.frontier, s'.mem a = s.mem a) (hf : s.frontier ≤ s'.frontier) : + M.inv s' ∧ M.abs s' = M.abs s ∧ M.footprint s' = M.footprint s := + M.congr hs (fun a ha => hmem a (M.lt_frontier hs ha)) hf + +variable {M} (N : Framed D₂ w) + +/-- A method run of `M` keeps every cell of a disjoint, allocated footprint. -/ +theorem agree_of_run {s : State} (o : D.Op) {a : D.Result o} {s' : State} + {tr : (spec w).Transcript} (hM : M.inv s) (hN : N.inv s) + (hd : Disjoint (M.footprint s) (N.footprint s)) (hv : D.valid (M.abs s) o) + (hrun : (spec w).ClientRun s (M.methods o) a s' tr) : + ∀ b ∈ N.footprint s, s'.mem b = s.mem b := fun b hb => + (M.modifies o hM hv hrun).1 b (N.lt_frontier hN hb) (Finset.disjoint_right.mp hd hb) + +/-- **The frame of a framed refinement.** A method run of `M` leaves a framed refinement with a +disjoint footprint unchanged, and the footprints stay disjoint. -/ +theorem frame {s : State} (o : D.Op) {a : D.Result o} {s' : State} + {tr : (spec w).Transcript} (hM : M.inv s) (hN : N.inv s) + (hd : Disjoint (M.footprint s) (N.footprint s)) (hv : D.valid (M.abs s) o) + (hrun : (spec w).ClientRun s (M.methods o) a s' tr) : + N.abs s' = N.abs s ∧ N.inv s' ∧ N.footprint s' = N.footprint s ∧ + Disjoint (M.footprint s') (N.footprint s') := by + have hf := frontier_le_of_clientRun hrun + obtain ⟨hN', habs, hfp⟩ := N.congr hN (agree_of_run N o hM hN hd hv hrun) hf + refine ⟨habs, hN', hfp, Finset.disjoint_left.mpr fun b hb hb' => ?_⟩ + rw [hfp] at hb' + rcases (M.modifies o hM hv hrun).2 b hb with hb | hb + · exact Finset.disjoint_left.mp hd hb hb' + · exact absurd (N.lt_frontier hN hb') (Nat.not_lt.mpr hb) + +/-- A method run of `M` leaves unchanged every function that reads only a disjoint footprint. -/ +theorem Local.frame {β : Type*} {f : State → β} (hf : N.Local f) {s : State} (o : D.Op) + {a : D.Result o} {s' : State} {tr : (spec w).Transcript} (hM : M.inv s) (hN : N.inv s) + (hd : Disjoint (M.footprint s) (N.footprint s)) (hv : D.valid (M.abs s) o) + (hrun : (spec w).ClientRun s (M.methods o) a s' tr) : f s' = f s := + hf hN (agree_of_run N o hM hN hd hv hrun) (frontier_le_of_clientRun hrun) + +/-! ## Products -/ + +variable (M₁ : Framed D₁ w) (M₂ : Framed D₂ w) + +/-- A method run of the left side keeps the right side and the disjointness of the footprints. -/ +theorem prod_frame_left {s : State} (o : D₁.Op) {a : D₁.Result o} {s' : State} + {tr : (spec w).Transcript} (h₁ : M₁.inv s) (h₂ : M₂.inv s) + (hd : Disjoint (M₁.footprint s) (M₂.footprint s)) (hv : D₁.valid (M₁.abs s) o) + (hrun : (spec w).ClientRun s (M₁.methods o) a s' tr) : + M₂.abs s' = M₂.abs s ∧ M₂.inv s' ∧ Disjoint (M₁.footprint s') (M₂.footprint s') := + have h := frame M₂ o h₁ h₂ hd hv hrun + ⟨h.1, h.2.1, h.2.2.2⟩ + +/-- A method run of the right side keeps the left side and the disjointness of the footprints. -/ +theorem prod_frame_right {s : State} (o : D₂.Op) {a : D₂.Result o} {s' : State} + {tr : (spec w).Transcript} (h₁ : M₁.inv s) (h₂ : M₂.inv s) + (hd : Disjoint (M₁.footprint s) (M₂.footprint s)) (hv : D₂.valid (M₂.abs s) o) + (hrun : (spec w).ClientRun s (M₂.methods o) a s' tr) : + M₁.abs s' = M₁.abs s ∧ M₁.inv s' ∧ Disjoint (M₁.footprint s') (M₂.footprint s') := + have h := frame M₁ o h₂ h₁ hd.symm hv hrun + ⟨h.1, h.2.1, h.2.2.2.symm⟩ + +/-- **Products over one memory.** Two framed refinements implement the product of their +specifications over one memory, with the union of their footprints, while the footprints are +disjoint. Disjointness is part of the product's invariant. -/ +def prod : Framed (product D₁ D₂) w where + toRefinement := M₁.toRefinement.prod M₂.toRefinement + (fun s => Disjoint (M₁.footprint s) (M₂.footprint s)) + (prod_frame_left M₁ M₂) (prod_frame_right M₁ M₂) + footprint s := M₁.footprint s ∪ M₂.footprint s + lt_frontier {_ _} hs ha := by + rcases Finset.mem_union.mp ha with ha | ha + · exact M₁.lt_frontier hs.1 ha + · exact M₂.lt_frontier hs.2.1 ha + congr {_ _} hs h hf := by + obtain ⟨h₁, h₂, hd⟩ := hs + obtain ⟨h₁', habs₁, hfp₁⟩ := M₁.congr h₁ (fun a ha => h a (Finset.mem_union_left _ ha)) hf + obtain ⟨h₂', habs₂, hfp₂⟩ := M₂.congr h₂ (fun a ha => h a (Finset.mem_union_right _ ha)) hf + refine ⟨⟨h₁', h₂', ?_⟩, ?_, ?_⟩ + · change Disjoint (M₁.footprint _) (M₂.footprint _) + rwa [hfp₁, hfp₂] + · change (M₁.abs _, M₂.abs _) = (M₁.abs _, M₂.abs _) + rw [habs₁, habs₂] + · rw [hfp₁, hfp₂] + modifies {s} o {a s' tr} hs hv hrun := by + obtain ⟨h₁, h₂, hd⟩ := hs + cases o with + | inl o => + obtain ⟨hmod, hgrow⟩ := M₁.modifies o h₁ hv hrun + obtain ⟨-, -, hfp₂, -⟩ := frame M₂ o h₁ h₂ hd hv hrun + refine ⟨fun b hb hb' => hmod b hb fun h => hb' (Finset.mem_union_left _ h), + fun b hb => ?_⟩ + change b ∈ M₁.footprint s' ∪ M₂.footprint s' at hb + rw [hfp₂] at hb + rcases Finset.mem_union.mp hb with hb | hb + · exact (hgrow b hb).imp_left (Finset.mem_union_left _) + · exact .inl (Finset.mem_union_right _ hb) + | inr o => + obtain ⟨hmod, hgrow⟩ := M₂.modifies o h₂ hv hrun + obtain ⟨-, -, hfp₁, -⟩ := frame M₁ o h₂ h₁ hd.symm hv hrun + refine ⟨fun b hb hb' => hmod b hb fun h => hb' (Finset.mem_union_right _ h), + fun b hb => ?_⟩ + change b ∈ M₁.footprint s' ∪ M₂.footprint s' at hb + rw [hfp₁] at hb + rcases Finset.mem_union.mp hb with hb | hb + · exact .inl (Finset.mem_union_left _ hb) + · exact (hgrow b hb).imp_left (Finset.mem_union_right _) + +/-- The product's methods run each side's methods. They do not depend on the proofs. -/ +theorem prod_methods : (M₁.prod M₂).methods = Methods.prod M₁.methods M₂.methods := rfl + +/-- The product represents each side by its own abstraction. -/ +@[simp] theorem prod_abs (s : State) : (M₁.prod M₂).abs s = (M₁.abs s, M₂.abs s) := rfl + +/-- The product's invariant: both sides' invariants, and disjoint footprints. -/ +theorem prod_inv_iff (s : State) : + (M₁.prod M₂).inv s ↔ + M₁.inv s ∧ M₂.inv s ∧ Disjoint (M₁.footprint s) (M₂.footprint s) := Iff.rfl + +/-- The product's footprint is the union of the footprints. -/ +@[simp] theorem prod_footprint (s : State) : + (M₁.prod M₂).footprint s = M₁.footprint s ∪ M₂.footprint s := rfl + +variable {M₁ M₂} + +/-- A function local to the left side is local to the product. -/ +theorem Local.prod_left {β : Type*} {f : State → β} (hf : M₁.Local f) : + (M₁.prod M₂).Local f := fun hs h hfr => + hf hs.1 (fun a ha => h a (Finset.mem_union_left _ ha)) hfr + +/-- A function local to the right side is local to the product. -/ +theorem Local.prod_right {β : Type*} {f : State → β} (hf : M₂.Local f) : + (M₁.prod M₂).Local f := fun hs h hfr => + hf hs.2.1 (fun a ha => h a (Finset.mem_union_right _ ha)) hfr + +/-- **Potentials add across a product over one memory.** Certificates of the two sides against +one lower charge, with potentials that read only their own footprints, give a certificate of +the product with componentwise upper charges and the sum of the potentials. -/ +theorem amortized_prod {lower : State → Op → ℕ} {upper₁ : D₁.State → D₁.Op → ℕ} + {upper₂ : D₂.State → D₂.Op → ℕ} {Φ₁ Φ₂ : State → ℕ} + (A₁ : M₁.Amortized lower upper₁ Φ₁) (A₂ : M₂.Amortized lower upper₂ Φ₂) + (hΦ₁ : M₁.Local Φ₁) (hΦ₂ : M₂.Local Φ₂) : + (M₁.prod M₂).Amortized lower (productCharge upper₁ upper₂) fun s => Φ₁ s + Φ₂ s := + Refinement.Amortized.prod (frame₁ := prod_frame_left M₁ M₂) (frame₂ := prod_frame_right M₁ M₂) + A₁ A₂ (fun {_} o {_ _ _} h₁ h₂ hd hv hrun => hΦ₂.frame M₂ o h₁ h₂ hd hv hrun) + (fun {_} o {_ _ _} h₁ h₂ hd hv hrun => hΦ₁.frame M₁ o h₂ h₁ hd.symm hv hrun) + +/-- Setting up a product from structures allocated one after the other: `M₁` holds at `s`; a +run from `s` to `s'` leaves the cells below `s.frontier` unchanged; and `M₂` holds at `s'` on a +footprint at or above `s.frontier`. Then the product holds at `s'`, with `M₁`'s abstraction +unchanged. -/ +theorem prod_of_fresh {s s' : State} (h₁ : M₁.inv s) + (hmem : ∀ a < s.frontier, s'.mem a = s.mem a) (hf : s.frontier ≤ s'.frontier) + (h₂ : M₂.inv s') (hfp : ∀ a ∈ M₂.footprint s', s.frontier ≤ a) : + (M₁.prod M₂).inv s' ∧ (M₁.prod M₂).abs s' = (M₁.abs s, M₂.abs s') := by + obtain ⟨h₁', habs, hfp₁⟩ := M₁.of_fresh h₁ hmem hf + refine ⟨⟨h₁', h₂, Finset.disjoint_left.mpr fun a ha ha' => ?_⟩, by rw [prod_abs, habs]⟩ + rw [hfp₁] at ha + exact absurd (M₁.lt_frontier h₁ ha) (Nat.not_lt.mpr (hfp a ha')) + +end Framed + +end Cslib.DataStructure.Memory diff --git a/Complexity/DataStructure/Memory/Spec.lean b/Complexity/DataStructure/Memory/Spec.lean index 3507072aee..f61e0fda4e 100644 --- a/Complexity/DataStructure/Memory/Spec.lean +++ b/Complexity/DataStructure/Memory/Spec.lean @@ -48,6 +48,7 @@ separate step that exchanges it into instructions. Allocation never reuses addre * `Memory.WellFormed.step`, `Memory.WellFormed.run`: admissible operations preserve well-formedness. * `Memory.WellFormed.fits_of_lt_frontier`: allocated addresses fit a word. +* `Memory.one_le_charge`: every operation costs at least one unit. * `Memory.frontier_le_step`, `Memory.frontier_le_run`: the frontier never decreases. * `Memory.mem_step_of_lt_frontier`: an operation changes an allocated cell only by writing it. * `Memory.wellFormed_init`: the input convention is well formed. @@ -162,6 +163,11 @@ abbrev allocDirty (k : ℕ) : Prog ℕ := @[simp] theorem charge_allocDirty (s : State) (k : ℕ) : charge s (.allocDirty k) = 1 := rfl +/-- Every memory operation costs at least one unit, so the number of operations, hence the +glue run after them, is paid for by their charges. -/ +theorem one_le_charge (s : State) (o : Op) : 1 ≤ charge s o := by + cases o <;> simp + /-- Admissibility of a concrete history is decidable. -/ instance decidableValidHistory (w : ℕ) (s : State) (os : List Op) : Decidable ((spec w).ValidHistory s os) := by diff --git a/Complexity/DataStructure/Queue/Memory.lean b/Complexity/DataStructure/Queue/Memory.lean index d3fa6805cf..89acafde8b 100644 --- a/Complexity/DataStructure/Queue/Memory.lean +++ b/Complexity/DataStructure/Queue/Memory.lean @@ -22,8 +22,9 @@ proved once: (`Queue.potential_step`); only the capacity and word-fit admissibility of the bounded stacks is new. Each queue operation costs at most three stack calls, amortized, with two credits per rear element. -2. `Stack.Memory.pairRefinement`: two array stacks with disjoint layouts over `Memory.spec w`, - each stack call costing at most three memory units. +2. `pair`: two array stacks with disjoint layouts in one memory, as the generic product + `Memory.Framed.prod` of two framed stacks, each stack call costing at most three memory + units. `memRefinement` composes them, and `mem_amortized` stacks the potentials across the composition (`Refinement.Amortized.comp`): every queue operation costs at most nine memory units, amortized, @@ -42,6 +43,7 @@ provide. * `boundedStacks`: the pair of bounded stacks. * `Inv`: a FIFO list within capacity holding words. * `refinement`: the two-stack queue over the pair of bounded stacks. +* `pair`: the two stacks in one memory. * `memRefinement`, `memMethods`: the queue over two array stacks over memory, and its methods as plain memory programs. * `run`: allocate the stacks, then run a queue client. @@ -53,6 +55,7 @@ provide. * `mem_amortized`: nine memory units per queue operation, with six credits per rear element. * `run_correct`, `run_cost`: the allocated queue answers like the specification and costs at most `6 + 9 m` memory units for `m` queue operations. +* `run_cost_glue`: with glue charged per memory operation, at most `(2 K + 1) (6 + 9 m)`. * `run_cost_isBigO`: the same cost as `O(m)`. ## References @@ -188,22 +191,28 @@ theorem amortized : /-! ## Over memory -/ +/-- The two stacks over one memory, in disjoint layouts. -/ +abbrev pair {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) : + Memory.Framed (boundedStacks w C) w := + (Stack.Memory.framed h₁).prod (Stack.Memory.framed h₂) + /-- The queue over two array stacks over memory. -/ -def memRefinement {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) - (hd : Disjoint L₁.footprint L₂.footprint) : +def memRefinement {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) : Refinement (Queue.WordRAM.spec w C) (Memory.spec w) := - (refinement w C).comp (Stack.Memory.pairRefinement h₁ h₂ hd) + (refinement w C).comp (pair h₁ h₂).toRefinement /-- **Stacked potentials.** Every queue operation costs at most nine memory units, amortized, with six credits per element of the rear stack: the queue's potential, at three memory units per stack call, read through the stacks' abstraction. -/ -theorem mem_amortized {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) - (hd : Disjoint L₁.footprint L₂.footprint) : - (memRefinement h₁ h₂ hd).Amortized charge (fun _ _ => 9) +theorem mem_amortized {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) : + (memRefinement h₁ h₂).Amortized charge (fun _ _ => 9) fun s => 6 * (Stack.Memory.abs L₂ s).length := by - have h := (amortized.mul_left 3).comp (Stack.Memory.pair_amortized h₁ h₂ hd) + have hpair := Memory.Framed.amortized_prod (Stack.Memory.amortized h₁) + (Stack.Memory.amortized h₂) (Memory.Framed.Local.const _ 0) (Memory.Framed.Local.const _ 0) + rw [productCharge_const] at hpair + have h := (amortized.mul_left 3).comp hpair have hΦ : (fun s : Memory.State => - 3 * (2 * (Stack.Memory.abs L₂ s).length) + 0) = + 3 * (2 * (Stack.Memory.abs L₂ s).length) + (0 + 0)) = fun s => 6 * (Stack.Memory.abs L₂ s).length := by funext s omega @@ -214,16 +223,12 @@ theorem mem_amortized {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) /-- The queue methods over two array stacks, as plain memory programs. -/ def memMethods (L₁ L₂ : Stack.Memory.Layout) (w C : ℕ) : Methods (Queue.WordRAM.spec w C) (Memory.spec w) := - fun o => link (Stack.Memory.pairMethods L₁ L₂ w C) (methods o) + fun o => link (Methods.prod (Stack.Memory.methods L₁ w C) (Stack.Memory.methods L₂ w C)) + (methods o) /-- The composite's methods are `memMethods`, whatever the proofs about the layouts. -/ theorem memRefinement_methods {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) - (h₂ : L₂.Valid w C) (hd : Disjoint L₁.footprint L₂.footprint) : - (memRefinement h₁ h₂ hd).methods = memMethods L₁ L₂ w C := by - funext o - change link (Stack.Memory.pairRefinement h₁ h₂ hd).methods (methods o) = _ - rw [Stack.Memory.pairRefinement_methods] - rfl + (h₂ : L₂.Valid w C) : (memRefinement h₁ h₂).methods = memMethods L₁ L₂ w C := rfl /-- Allocate two empty stacks of capacity `C`, then run a queue client on them. -/ def run (w C : ℕ) {β : Type} (p : (Queue.WordRAM.spec w C).Client β) : Prog β := do @@ -252,43 +257,33 @@ theorem runClient_run {β : Type} (p : (Queue.WordRAM.spec w C).Client β) (s : /-- The facts that start the refinement after allocation. -/ private theorem setup (s : Memory.State) (hfit : Fits w (s.frontier + 2 * (C + 1))) : - ∃ (h₁ : (frontLayout s C).Valid w C) (h₂ : (rearLayout s C).Valid w C) - (hd : Disjoint (frontLayout s C).footprint (rearLayout s C).footprint), - (memRefinement h₁ h₂ hd).inv (allocated s C) ∧ - (memRefinement h₁ h₂ hd).abs (allocated s C) = [] ∧ + ∃ (h₁ : (frontLayout s C).Valid w C) (h₂ : (rearLayout s C).Valid w C), + (memRefinement h₁ h₂).inv (allocated s C) ∧ + (memRefinement h₁ h₂).abs (allocated s C) = [] ∧ Stack.Memory.abs (rearLayout s C) (allocated s C) = [] := by have h₁ : (frontLayout s C).Valid w C := Stack.Memory.valid_allocLayout (hfit.of_le (by omega)) have h₂ : (rearLayout s C).Valid w C := Stack.Memory.valid_allocLayout (hfit.of_le (by omega)) - have hd : Disjoint (frontLayout s C).footprint (rearLayout s C).footprint := by - rw [Finset.disjoint_left] - intro a ha hb - simp only [Stack.Memory.Layout.footprint, Finset.mem_union, Block.mem_footprint, - Block.Owns] at ha hb - omega - -- The first stack, as allocated, before the second allocation. + -- Each stack is empty once allocated, the second one from the state after the first. obtain ⟨hi₁, ha₁⟩ := Stack.Memory.inv_allocLayout (C := C) s - -- The second stack, allocated from the state after the first. obtain ⟨hi₂, ha₂⟩ := Stack.Memory.inv_allocLayout (C := C) ⟨clear s.mem s.frontier 1, s.frontier + 1 + C⟩ - -- The second allocation leaves the first stack's cells alone. - have hframe : ∀ a ∈ (frontLayout s C).footprint, - (allocated s C).mem a = clear s.mem s.frontier 1 a := by - intro a ha - simp only [Stack.Memory.Layout.footprint, Finset.mem_union, Block.mem_footprint, - Block.Owns] at ha - exact clear_of_not_owns (by simp only [Block.Owns]; omega) - have hi₁' := hi₁.congr h₁ hframe (by simp only; omega) - have ha₁' := (Stack.Memory.abs_congr h₁ hi₁ hframe).trans ha₁ - refine ⟨h₁, h₂, hd, ⟨⟨hi₁', hi₂⟩, ?_⟩, ?_, ha₂⟩ - · change Inv w C (Queue.WordRAM.contents - (Stack.Memory.abs (frontLayout s C) (allocated s C), - Stack.Memory.abs (rearLayout s C) (allocated s C))) - rw [ha₁', ha₂] + -- The second allocation is fresh, so the stacks form a product. + obtain ⟨hinv, habs⟩ := Memory.Framed.prod_of_fresh (M₁ := Stack.Memory.framed h₁) + (M₂ := Stack.Memory.framed h₂) hi₁ + (fun a ha => clear_of_not_owns (by simp only [Block.Owns] at ha ⊢; omega)) (by simp only; omega) + hi₂ + (fun a ha => by + simp only [Stack.Memory.framed_footprint, Stack.Memory.Layout.footprint, Finset.mem_union, + Block.mem_footprint, Block.Owns] at ha + simp only + omega) + have hempty : (pair h₁ h₂).abs (allocated s C) = ([], []) := habs.trans (Prod.ext ha₁ ha₂) + refine ⟨h₁, h₂, ⟨hinv, ?_⟩, ?_, ha₂⟩ + · change Inv w C (Queue.WordRAM.contents ((pair h₁ h₂).abs (allocated s C))) + rw [hempty] exact ⟨by simp [Queue.WordRAM.contents], by simp [Queue.WordRAM.contents]⟩ - · change Queue.WordRAM.contents - (Stack.Memory.abs (frontLayout s C) (allocated s C), - Stack.Memory.abs (rearLayout s C) (allocated s C)) = [] - rw [ha₁', ha₂] + · change Queue.WordRAM.contents ((pair h₁ h₂).abs (allocated s C)) = [] + rw [hempty] rfl /-- **Correctness.** From any memory with room for both stacks, an admissible queue client run @@ -299,9 +294,9 @@ theorem run_correct {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Me (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : (Memory.spec w).ValidHistory s ((Memory.spec w).clientHistory s (run w C p)) ∧ (Memory.spec w).clientResult s (run w C p) = (Queue.WordRAM.spec w C).clientResult [] p := by - obtain ⟨h₁, h₂, hd, hinv, habs, -⟩ := setup s hfit + obtain ⟨h₁, h₂, hinv, habs, -⟩ := setup s hfit obtain ⟨hvE, hres, -, -⟩ := - (memRefinement h₁ h₂ hd).simulates_client p hinv (by rw [habs]; exact hv) + (memRefinement h₁ h₂).simulates_client p hinv (by rw [habs]; exact hv) rw [memRefinement_methods] at hvE hres simp only [StateTransitionSpec.clientHistory, StateTransitionSpec.clientResult, runClient_run] at hvE hres ⊢ @@ -317,8 +312,8 @@ theorem run_cost {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Memor (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : (Memory.spec w).totalCost charge s ((Memory.spec w).clientHistory s (run w C p)) ≤ 6 + 9 * ((Queue.WordRAM.spec w C).clientHistory [] p).length := by - obtain ⟨h₁, h₂, hd, hinv, habs, hrear⟩ := setup s hfit - have h := (mem_amortized h₁ h₂ hd).client p hinv (by rw [habs]; exact hv) + obtain ⟨h₁, h₂, hinv, habs, hrear⟩ := setup s hfit + have h := (mem_amortized h₁ h₂).client p hinv (by rw [habs]; exact hv) rw [memRefinement_methods, habs, StateTransitionSpec.totalCost_const, hrear] at h simp only [StateTransitionSpec.clientHistory, runClient_run, StateTransitionSpec.totalCost_cons, charge_alloc, charge_allocDirty, @@ -326,6 +321,21 @@ theorem run_cost {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Memor simp only [List.length_nil, Nat.mul_zero, Nat.add_zero, allocated] at h ⊢ omega +/-- **Cost with glue.** A realization also charges the glue that runs after each memory operation. +If that glue costs at most `K` times the operation's charge plus `K`, the run costs at most +`(2 K + 1) (6 + 9 m)` in memory charges and glue together. The queue's and the stacks' methods +are straight-line code between memory operations, so their glue is of this form. -/ +theorem run_cost_glue {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Memory.State} + (hfit : Fits w (s.frontier + 2 * (C + 1))) + (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) + {glue : Memory.State → Memory.Op → ℕ} {K : ℕ} + (hglue : ∀ s o, glue s o ≤ K * (charge s o + 1)) : + (Memory.spec w).totalCost (fun s o => charge s o + glue s o) s + ((Memory.spec w).clientHistory s (run w C p)) ≤ + (2 * K + 1) * (6 + 9 * ((Queue.WordRAM.spec w C).clientHistory [] p).length) := + ((Memory.spec w).totalCost_glue_le one_le_charge hglue s _).trans + (Nat.mul_le_mul_left _ (run_cost p hfit hv)) + /-- **The exported form.** Over admissible clients, the memory cost of a run is `O(m)` in the number `m` of queue operations. -/ theorem run_cost_isBigO (s : Memory.State) (hfit : Fits w (s.frontier + 2 * (C + 1))) : diff --git a/Complexity/DataStructure/Stack/Memory.lean b/Complexity/DataStructure/Stack/Memory.lean index cf79d42258..5fb09e09e6 100644 --- a/Complexity/DataStructure/Stack/Memory.lean +++ b/Complexity/DataStructure/Stack/Memory.lean @@ -8,7 +8,7 @@ module -- upstream: candidate public import Complexity.DataStructure.Memory.Field -public import Complexity.DataStructure.StateTransition.Refinement +public import Complexity.DataStructure.Memory.Frame public import Complexity.DataStructure.Stack.WordRAM.Basic /-! @@ -23,8 +23,9 @@ refinement obligations follow from those equations and the list view of the arra Every operation costs at most three memory units, with no potential (`amortized`). The methods write only the layout's cells and never allocate (`modifies_methods`), and the abstraction and -invariant depend only on those cells (`abs_congr`, `Inv.congr`). Two stacks with disjoint -layouts therefore implement the product of two stacks (`pairRefinement`). +invariant depend only on those cells (`abs_congr`, `Inv.congr`). So the stack is framed by its +layout (`framed`), and stacks with disjoint layouts share one memory through the generic product +`Memory.Framed.prod`. This is the same specification that `Stack.WordRAM` implements by a hand-stepped nine-register dispatcher. Here no register or instruction count appears. Word fit enters only through the @@ -37,18 +38,15 @@ admissibility of `Memory.spec w` and one static fact of the layout, that the cap * `push`, `pop`, `size`, `methods`: the operations as memory programs. * `abs`, `Inv`: the abstraction function and the implementation invariant. * `refinement`: the array stack as a `Refinement` of the bounded stack. -* `pairMethods`, `pairRefinement`: two array stacks with disjoint layouts implement the pair of - stacks. +* `framed`: the same refinement, framed by the layout's cells. * `allocLayout`: allocate an empty stack. ## Main results * `amortized`: every operation costs at most three memory units. -* `pair_amortized`: so does every operation of the pair. -* `pairRefinement_methods`: the pair's methods do not depend on the proofs about its layouts. * `runClient_push`, `runClient_pop_of_eq_zero`, `runClient_pop_of_pos`, `runClient_size`: the operations evaluate definitionally. -* `modifies_methods`, `abs_congr`, `Inv.congr`, `frame`: the frame of a stack. +* `modifies_methods`, `abs_congr`, `Inv.congr`, `Inv.lt_frontier`: the frame of a stack. * `runClient_allocLayout_bind`, `valid_allocLayout`, `inv_allocLayout`: allocation yields a well-formed empty stack in three memory units. @@ -278,20 +276,6 @@ def refinement (hL : L.Valid w C) : Refinement (Stack.WordRAM.spec w C) (Memory. runClient_size] exact ⟨by simp, hs⟩) -/-- Every operation costs at most three memory units, so the stack needs no potential. -/ -theorem amortized (hL : L.Valid w C) : - (refinement hL).Amortized charge (fun _ _ => 3) fun _ => 0 := by - apply Refinement.Amortized.ofFn - intro s o _ _ - cases o with - | push v => simp [refinement, methods, StateTransitionSpec.clientHistory, runClient_push] - | pop => - by_cases h0 : s.mem L.count.base = 0 - · simp [refinement, methods, StateTransitionSpec.clientHistory, - runClient_pop_of_eq_zero L s h0] - · simp [refinement, methods, StateTransitionSpec.clientHistory, runClient_pop_of_pos L s h0] - | size => simp [refinement, methods, StateTransitionSpec.clientHistory, runClient_size] - /-! ## The frame of a stack -/ /-- Each admissible operation writes only the layout's cells and never allocates. -/ @@ -319,54 +303,44 @@ theorem modifies_methods (hL : L.Valid w C) {s : Memory.State} (o : StackOp ℕ) exact ⟨fun a ha => Function.update_of_ne (ne_of_mem_of_not_mem hc ha).symm _ _, trivial⟩ | size => exact ⟨fun _ _ => rfl, rfl⟩ -/-- A stack's operations leave a stack with a disjoint layout unchanged. -/ -theorem frame {L' : Layout} (hL : L.Valid w C) (hL' : L'.Valid w C) - (hd : Disjoint L.footprint L'.footprint) {s : Memory.State} (hs' : Inv L' s) - (o : StackOp ℕ) (hv : (Stack.WordRAM.spec w C).valid (abs L s) o) : - abs L' ((Memory.spec w).clientState s (methods L w C o)) = abs L' s ∧ - Inv L' ((Memory.spec w).clientState s (methods L w C o)) := by - obtain ⟨hm, hf⟩ := modifies_methods hL o hv - have h : ∀ a ∈ L'.footprint, - ((Memory.spec w).clientState s (methods L w C o)).mem a = s.mem a := - fun a ha => hm a (Finset.disjoint_right.mp hd ha) - exact ⟨abs_congr hL' hs' h, hs'.congr hL' h hf.ge⟩ - -/-! ## Two stacks in one memory -/ - -/-- The operations of two stacks, each on its own layout. -/ -def pairMethods (L₁ L₂ : Layout) (w C : ℕ) : - Methods (product (Stack.WordRAM.spec w C) (Stack.WordRAM.spec w C)) (Memory.spec w) - | .inl o => methods L₁ w C o - | .inr o => methods L₂ w C o - -/-- Two array stacks with disjoint layouts implement a pair of bounded stacks. -/ -def pairRefinement {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) - (hd : Disjoint L₁.footprint L₂.footprint) : - Refinement (product (Stack.WordRAM.spec w C) (Stack.WordRAM.spec w C)) (Memory.spec w) := - (refinement h₁).prod (refinement h₂) - (fun {s} o {a s' tr} hs₁ hs₂ hv hrun => by - obtain ⟨_, _, rfl, _⟩ := ((Memory.spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun - exact frame h₁ h₂ hd hs₂ o hv) - (fun {s} o {a s' tr} hs₁ hs₂ hv hrun => by - obtain ⟨_, _, rfl, _⟩ := ((Memory.spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun - exact frame h₂ h₁ hd.symm hs₁ o hv) - -/-- The pair's methods are `pairMethods`, whatever the proofs about the layouts. -/ -theorem pairRefinement_methods {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) - (hd : Disjoint L₁.footprint L₂.footprint) : - (pairRefinement h₁ h₂ hd).methods = pairMethods L₁ L₂ w C := by - funext o - cases o <;> rfl - -/-- Every operation of the pair costs at most three memory units. -/ -theorem pair_amortized {L₁ L₂ : Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) - (hd : Disjoint L₁.footprint L₂.footprint) : - (pairRefinement h₁ h₂ hd).Amortized charge (fun _ _ => 3) fun _ => 0 := by - have h : (pairRefinement h₁ h₂ hd).Amortized charge - (productCharge (fun _ _ => 3) (fun _ _ => 3)) fun _ => 0 := - (amortized h₁).prod (amortized h₂) (fun _ _ _ _ _ _ _ _ => rfl) - (fun _ _ _ _ _ _ _ _ => rfl) - rwa [productCharge_const] at h +/-- The layout's cells are allocated. -/ +theorem Inv.lt_frontier {s : Memory.State} (hs : Inv L s) {a : ℕ} (ha : a ∈ L.footprint) : + a < s.frontier := by + have h₁ := hs.count_allocated + have h₂ := hs.items_allocated + simp only [Layout.footprint, Finset.mem_union, Block.mem_footprint, Block.Owns] at ha + unfold Block.Allocated at h₁ h₂ + omega + +/-- The array stack, with its layout's cells as a constant footprint. Stacks with disjoint +layouts therefore share one memory as a product of stacks (`Memory.Framed.prod`). -/ +def framed (hL : L.Valid w C) : Memory.Framed (Stack.WordRAM.spec w C) w := + .ofStatic (refinement hL) L.footprint (fun hs ha => Inv.lt_frontier hs ha) + (fun hs h hf => ⟨hs.congr hL h hf, abs_congr hL hs h⟩) + (fun o _ hv b _ hb => (modifies_methods hL o hv).1 b hb) + +/-- Every operation costs at most three memory units, so the stack needs no potential. -/ +theorem amortized (hL : L.Valid w C) : + (framed hL).Amortized charge (fun _ _ => 3) fun _ => 0 := by + apply Refinement.Amortized.ofFn + intro s o _ _ + cases o with + | push v => + simp [framed, Memory.Framed.ofStatic, refinement, methods, + StateTransitionSpec.clientHistory, runClient_push] + | pop => + by_cases h0 : s.mem L.count.base = 0 + · simp [framed, Memory.Framed.ofStatic, refinement, methods, + StateTransitionSpec.clientHistory, runClient_pop_of_eq_zero L s h0] + · simp [framed, Memory.Framed.ofStatic, refinement, methods, + StateTransitionSpec.clientHistory, runClient_pop_of_pos L s h0] + | size => + simp [framed, Memory.Framed.ofStatic, refinement, methods, + StateTransitionSpec.clientHistory, runClient_size] + +/-- The footprint of a stack is its layout's cells. -/ +@[simp] theorem framed_footprint (hL : L.Valid w C) (s : Memory.State) : + (framed hL).footprint s = L.footprint := rfl /-! ## Allocation -/ diff --git a/Complexity/DataStructure/StateTransition/Oracle.lean b/Complexity/DataStructure/StateTransition/Oracle.lean new file mode 100644 index 0000000000..5cb3deff99 --- /dev/null +++ b/Complexity/DataStructure/StateTransition/Oracle.lean @@ -0,0 +1,44 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.DataStructure.StateTransition.Module +public import Complexity.Languages.Probabilistic.Basic + +/-! +# Linking is oracle interpretation + +A client of a state-transition specification is an oracle program over the specification's +signature: both are `PFunctor.FreeM` over the operations and their answer types. Linking a client +through methods over a lower interface is interpreting the program in the lower interface's client +monad, answering each call with its method (`link_eq_interpret`). The lemmas about +`OracleComp.interpret` (sequencing, composition of interpretations, interpretation into `PMF` +and state monads) therefore apply to linked clients, and refinement and cost results about `link` +apply to interpreted programs. + +This bridge is its own module so that the state-transition layer does not import probability. + +## Main results + +* `link_eq_interpret`: linking is interpretation by the methods. +-/ + +@[expose] public section + +namespace Cslib.DataStructure + +universe u v w + +variable {D E : StateTransitionSpec.{u, v, w}} {α : Type w} + +/-- Linking a client through methods is interpreting it as an oracle program, answering each +call with its method. -/ +theorem link_eq_interpret (methods : Methods D E) (p : D.Client α) : + link methods p = OracleComp.interpret (spec := D.Result) (m := E.Client) methods p := rfl + +end Cslib.DataStructure diff --git a/Complexity/DataStructure/StateTransition/Refinement.lean b/Complexity/DataStructure/StateTransition/Refinement.lean index e6d94e5d59..794f512032 100644 --- a/Complexity/DataStructure/StateTransition/Refinement.lean +++ b/Complexity/DataStructure/StateTransition/Refinement.lean @@ -30,7 +30,16 @@ representation is functional yields an `Amortized` certificate of the realized m (`Refinement.Amortized.realize`). Products of refinements over one lower interface (`Refinement.prod`) need non-interference: the -methods of each side leave the other side's abstraction, invariant and potential unchanged. +methods of each side leave the other side's abstraction, invariant and potential unchanged. A +joint invariant, such as the disjointness of the memory the two sides use, may be part of the +product's invariant. `Memory.Framed.prod` discharges these obligations from footprints. + +**Glue.** A realization charges, besides each lower call, the glue code that consumes its answer. +In a free-monad program every bind follows exactly one call, so glue charged per bind is a +further charge per lower call, and `Refinement.Amortized.glue` absorbs it at a constant rate when +it is at most proportional to the call's charge plus one. Work not attached to a lower call, such +as a loop iteration that makes no call, is charged per call one level up and added with +`Refinement.Amortized.add`. ## Main definitions @@ -40,12 +49,13 @@ methods of each side leave the other side's abstraction, invariant and potential * `Refinement.toModule`: the underlying relational module. * `Refinement.id`, `Refinement.comp`, `Refinement.prod`: identity, composition, and products under non-interference. +* `Methods.prod`: the methods of both sides of a product. * `Refinement.Amortized`: per-method amortized cost refinement with a potential on lower states. * `productCharge`: componentwise charges of a product specification. -The two transcript-cost lemmas `StateTransitionSpec.totalCostTranscript_mono` and -`StateTransitionSpec.totalCostTranscript_mul_left` sit here, beside their only clients, rather -than in the widely imported `StateTransition` module. +The transcript-cost lemmas `StateTransitionSpec.totalCostTranscript_mono`, +`StateTransitionSpec.totalCostTranscript_mul_left` and `StateTransitionSpec.totalCostTranscript_add` +sit here, beside their only clients, rather than in the widely imported `StateTransition` module. ## Main results @@ -58,12 +68,15 @@ than in the widely imported `StateTransition` module. * `Refinement.Amortized.prod`: potentials add across non-interfering products. * `Refinement.Amortized.realize`: an amortized certificate for the realized module. * `Refinement.Amortized.id`: the identity refinement passes charges through. -* `Refinement.Amortized.mono`, `Refinement.Amortized.mul_left`: weaken charges, and exchange - both currencies at a constant rate. +* `Refinement.Amortized.mono`, `Refinement.Amortized.mul_left`, `Refinement.Amortized.add`: + weaken charges, exchange both currencies at a constant rate, and add certificates. +* `Refinement.Amortized.glue`, `Refinement.Amortized.glue_of_one_le`: absorb glue charged per + lower call. +* `StateTransitionSpec.totalCost_glue_le`: the same on a history. * `productCharge_const`: equal constant charges on a product. * `StateTransitionSpec.totalCostTranscript_mono`, - `StateTransitionSpec.totalCostTranscript_mul_left`: monotonicity and scaling of transcript - costs. + `StateTransitionSpec.totalCostTranscript_mul_left`, `StateTransitionSpec.totalCostTranscript_add`: + monotonicity, scaling and additivity of transcript costs. * `Refinement.Amortized.chargeBound`, `Refinement.Amortized.of_chargeBound`: with zero potential, the existing worst-case `Module.ChargeBound`. @@ -96,6 +109,11 @@ structure Refinement (D E : StateTransitionSpec.{u, v, w}) where correct : ∀ {e} o {a e' tr}, inv e → D.valid (abs e) o → E.ClientRun e (methods o) a e' tr → D.step (abs e) o (abs e') a ∧ inv e' +/-- The methods of both sides of a product, over one lower interface. -/ +def Methods.prod (m₁ : Methods D₁ E) (m₂ : Methods D₂ E) : Methods (product D₁ D₂) E + | .inl o => m₁ o + | .inr o => m₂ o + /-- Componentwise charges of a product specification. -/ def productCharge (c₁ : D₁.State → D₁.Op → ℕ) (c₂ : D₂.State → D₂.Op → ℕ) : (product D₁ D₂).State → (product D₁ D₂).Op → ℕ @@ -127,6 +145,36 @@ theorem StateTransitionSpec.totalCostTranscript_mul_left (K : ℕ) (c : D.State | cons st tr ih => simp only [StateTransitionSpec.totalCostTranscript_cons, ih, Nat.mul_add] +/-- Charges add along a transcript. -/ +theorem StateTransitionSpec.totalCostTranscript_add (c c' : D.State → D.Op → ℕ) (s : D.State) + (tr : D.Transcript) : + D.totalCostTranscript (fun s o => c s o + c' s o) s tr = + D.totalCostTranscript c s tr + D.totalCostTranscript c' s tr := by + induction tr generalizing s with + | nil => simp + | cons st tr ih => + simp only [StateTransitionSpec.totalCostTranscript_cons, ih] + omega + +/-- **Glue on a history.** Glue charged with each operation at most `K` times its charge plus `K` +costs, together with the charges, at most `2 K + 1` times the charges, when every operation +costs at least one unit. -/ +theorem StateTransitionSpec.totalCost_glue_le + {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} [D.HasStepFn fD] + {lower glue : D.State → D.Op → ℕ} {K : ℕ} (hone : ∀ s o, 1 ≤ lower s o) + (hglue : ∀ s o, glue s o ≤ K * (lower s o + 1)) (s : D.State) (os : List D.Op) : + D.totalCost (fun s o => lower s o + glue s o) s os ≤ (2 * K + 1) * D.totalCost lower s os := by + induction os generalizing s with + | nil => simp + | cons o os ih => + simp only [StateTransitionSpec.totalCost_cons] + have h := ih (D.stepFn s o).1 + have hg := hglue s o + have h1 := Nat.mul_le_mul_left K (hone s o) + rw [Nat.mul_add] at hg ⊢ + rw [Nat.add_mul, Nat.mul_assoc, Nat.one_mul] at ⊢ + omega + namespace Refinement /-- The constructor for deterministic specifications: each method's history is admissible, and @@ -230,32 +278,33 @@ theorem link_comp (M : Refinement D E) (N : Refinement E F) (p : D.Client α) : (DataStructure.link_comp M.methods N.methods p).symm /-- Two refinements over one lower interface implement the product of their specifications, -provided the methods of each leave the other's abstraction and invariant unchanged. -/ -def prod (M₁ : Refinement D₁ E) (M₂ : Refinement D₂ E) - (frame₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₁.valid (M₁.abs e) o → - E.ClientRun e (M₁.methods o) a e' tr → M₂.abs e' = M₂.abs e ∧ M₂.inv e') - (frame₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₂.valid (M₂.abs e) o → - E.ClientRun e (M₂.methods o) a e' tr → M₁.abs e' = M₁.abs e ∧ M₁.inv e') : +provided the methods of each leave the other's abstraction and invariant unchanged. The joint +invariant `J` holds of the shared lower state and is preserved by both sides; it records what +keeps them apart, such as the disjointness of the memory each one uses. -/ +def prod (M₁ : Refinement D₁ E) (M₂ : Refinement D₂ E) (J : E.State → Prop) + (frame₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → J e → D₁.valid (M₁.abs e) o → + E.ClientRun e (M₁.methods o) a e' tr → M₂.abs e' = M₂.abs e ∧ M₂.inv e' ∧ J e') + (frame₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → J e → D₂.valid (M₂.abs e) o → + E.ClientRun e (M₂.methods o) a e' tr → M₁.abs e' = M₁.abs e ∧ M₁.inv e' ∧ J e') : Refinement (product D₁ D₂) E where - methods - | .inl o => M₁.methods o - | .inr o => M₂.methods o - inv e := M₁.inv e ∧ M₂.inv e + methods := Methods.prod M₁.methods M₂.methods + inv e := M₁.inv e ∧ M₂.inv e ∧ J e abs e := (M₁.abs e, M₂.abs e) valid {_} o hi hv := by cases o with | inl o => exact M₁.valid o hi.1 hv - | inr o => exact M₂.valid o hi.2 hv + | inr o => exact M₂.valid o hi.2.1 hv correct {_} o {_ _ _} hi hv hrun := by + obtain ⟨hi₁, hi₂, hJ⟩ := hi cases o with | inl o => - obtain ⟨hstep, hi₁⟩ := M₁.correct o hi.1 hv hrun - obtain ⟨habs, hi₂⟩ := frame₁ o hi.1 hi.2 hv hrun - exact ⟨⟨hstep, habs⟩, hi₁, hi₂⟩ + obtain ⟨hstep, hi₁'⟩ := M₁.correct o hi₁ hv hrun + obtain ⟨habs, hi₂', hJ'⟩ := frame₁ o hi₁ hi₂ hJ hv hrun + exact ⟨⟨hstep, habs⟩, hi₁', hi₂', hJ'⟩ | inr o => - obtain ⟨hstep, hi₂⟩ := M₂.correct o hi.2 hv hrun - obtain ⟨habs, hi₁⟩ := frame₂ o hi.1 hi.2 hv hrun - exact ⟨⟨habs, hstep⟩, hi₁, hi₂⟩ + obtain ⟨hstep, hi₂'⟩ := M₂.correct o hi₂ hv hrun + obtain ⟨habs, hi₁', hJ'⟩ := frame₂ o hi₁ hi₂ hJ hv hrun + exact ⟨⟨habs, hstep⟩, hi₁', hi₂', hJ'⟩ /-! ## Amortized costs -/ @@ -308,6 +357,58 @@ theorem Amortized.mul_left (A : M.Amortized lower upper potential) (K : ℕ) : rw [E.totalCostTranscript_mul_left, ← Nat.mul_add, ← Nat.mul_add] exact Nat.mul_le_mul_left K (A.step o hi hv hrun) +/-- Two certificates of one refinement add: their lower charges, upper charges and potentials. -/ +theorem Amortized.add (A : M.Amortized lower upper potential) + {lower' : E.State → E.Op → ℕ} {upper' : D.State → D.Op → ℕ} {potential' : E.State → ℕ} + (B : M.Amortized lower' upper' potential') : + M.Amortized (fun e o => lower e o + lower' e o) (fun s o => upper s o + upper' s o) + (fun e => potential e + potential' e) := by + refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ + have h := A.step o hi hv hrun + have h' := B.step o hi hv hrun + rw [E.totalCostTranscript_add] + omega + +/-- **Glue charges.** A realization of the lower interface charges, besides each lower call, the +glue that consumes its answer: in a free-monad program every bind follows exactly one call. If +the glue charged with a call is at most `K` times the call's charge plus `K`, a certificate for +the charges and one for the number of calls give a certificate for the combined charge. Work that +is not attached to a lower call, such as the iterations of a loop that makes no call, is a +further per-call charge of the upper interface, added with `Amortized.add` one level up. -/ +theorem Amortized.glue (A : M.Amortized lower upper potential) + {calls : D.State → D.Op → ℕ} {Ψ : E.State → ℕ} (B : M.Amortized (fun _ _ => 1) calls Ψ) + {glue : E.State → E.Op → ℕ} {K : ℕ} (hglue : ∀ e o, glue e o ≤ K * (lower e o + 1)) : + M.Amortized (fun e o => lower e o + glue e o) + (fun s o => (K + 1) * upper s o + K * calls s o) + (fun e => (K + 1) * potential e + K * Ψ e) := + ((A.mul_left (K + 1)).add (B.mul_left K)).mono + (fun e o => calc + lower e o + glue e o ≤ lower e o + K * (lower e o + 1) := Nat.add_le_add_left (hglue e o) _ + _ = (K + 1) * lower e o + K * 1 := by rw [Nat.mul_add, Nat.add_mul]; omega) + fun _ _ _ => le_rfl + +/-- **Glue charges, when every call costs at least one unit**, as every memory operation does: +then the calls are paid for by their charges, and the combined charge is amortized at `2 K + 1` +times the upper charge. -/ +theorem Amortized.glue_of_one_le (A : M.Amortized lower upper potential) + (hone : ∀ e o, 1 ≤ lower e o) {glue : E.State → E.Op → ℕ} {K : ℕ} + (hglue : ∀ e o, glue e o ≤ K * (lower e o + 1)) : + M.Amortized (fun e o => lower e o + glue e o) (fun s o => (2 * K + 1) * upper s o) + (fun e => (2 * K + 1) * potential e) := by + have h := A.glue (A.mono hone fun _ _ _ => le_rfl) hglue + have hu : (fun s o => (K + 1) * upper s o + K * upper s o) = + fun s o => (2 * K + 1) * upper s o := by + funext s o + rw [Nat.add_mul, Nat.add_mul, Nat.mul_assoc] + omega + have hp : (fun e => (K + 1) * potential e + K * potential e) = + fun e => (2 * K + 1) * potential e := by + funext e + rw [Nat.add_mul, Nat.add_mul, Nat.mul_assoc] + omega + rw [hu, hp] at h + exact h + /-- Potentials telescope along any run of a linked admissible client. -/ theorem Amortized.demonic (A : M.Amortized lower upper potential) (p : D.Client α) {e e' : E.State} {a : α} {tr_E : E.Transcript} (he : M.inv e) @@ -366,31 +467,32 @@ theorem Amortized.comp {N : Refinement E F} {lower : F.State → F.Op → ℕ} /-- **Potentials add across non-interfering products**, provided the methods of each side leave the other side's potential unchanged. -/ -theorem Amortized.prod {M₁ : Refinement D₁ E} {M₂ : Refinement D₂ E} - {frame₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₁.valid (M₁.abs e) o → - E.ClientRun e (M₁.methods o) a e' tr → M₂.abs e' = M₂.abs e ∧ M₂.inv e'} - {frame₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₂.valid (M₂.abs e) o → - E.ClientRun e (M₂.methods o) a e' tr → M₁.abs e' = M₁.abs e ∧ M₁.inv e'} +theorem Amortized.prod {M₁ : Refinement D₁ E} {M₂ : Refinement D₂ E} {J : E.State → Prop} + {frame₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → J e → D₁.valid (M₁.abs e) o → + E.ClientRun e (M₁.methods o) a e' tr → M₂.abs e' = M₂.abs e ∧ M₂.inv e' ∧ J e'} + {frame₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → J e → D₂.valid (M₂.abs e) o → + E.ClientRun e (M₂.methods o) a e' tr → M₁.abs e' = M₁.abs e ∧ M₁.inv e' ∧ J e'} {upper₁ : D₁.State → D₁.Op → ℕ} {upper₂ : D₂.State → D₂.Op → ℕ} {Φ₁ Φ₂ : E.State → ℕ} (A₁ : M₁.Amortized lower upper₁ Φ₁) (A₂ : M₂.Amortized lower upper₂ Φ₂) - (potential₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₁.valid (M₁.abs e) o → + (potential₁ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → J e → D₁.valid (M₁.abs e) o → E.ClientRun e (M₁.methods o) a e' tr → Φ₂ e' = Φ₂ e) - (potential₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → D₂.valid (M₂.abs e) o → + (potential₂ : ∀ {e} o {a e' tr}, M₁.inv e → M₂.inv e → J e → D₂.valid (M₂.abs e) o → E.ClientRun e (M₂.methods o) a e' tr → Φ₁ e' = Φ₁ e) : - (M₁.prod M₂ frame₁ frame₂).Amortized lower (productCharge upper₁ upper₂) + (M₁.prod M₂ J frame₁ frame₂).Amortized lower (productCharge upper₁ upper₂) (fun e => Φ₁ e + Φ₂ e) := by refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ + obtain ⟨hi₁, hi₂, hJ⟩ := hi cases o with | inl o => - have h := A₁.step o hi.1 hv hrun - have hΦ := potential₁ o hi.1 hi.2 hv hrun + have h := A₁.step o hi₁ hv hrun + have hΦ := potential₁ o hi₁ hi₂ hJ hv hrun change E.totalCostTranscript lower e tr + (Φ₁ e' + Φ₂ e') ≤ upper₁ (M₁.abs e) o + (Φ₁ e + Φ₂ e) omega | inr o => - have h := A₂.step o hi.2 hv hrun - have hΦ := potential₂ o hi.1 hi.2 hv hrun + have h := A₂.step o hi₂ hv hrun + have hΦ := potential₂ o hi₁ hi₂ hJ hv hrun change E.totalCostTranscript lower e tr + (Φ₁ e' + Φ₂ e') ≤ upper₂ (M₂.abs e) o + (Φ₁ e + Φ₂ e) omega diff --git a/ComplexityTests/DataStructureMemoryProduct.lean b/ComplexityTests/DataStructureMemoryProduct.lean new file mode 100644 index 0000000000..22244481b9 --- /dev/null +++ b/ComplexityTests/DataStructureMemoryProduct.lean @@ -0,0 +1,110 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: local-only (test driver) + +public import Complexity.DataStructure.Stack.Memory + +/-! +# Three stacks in one memory + +Three array stacks of capacity `2` sit side by side in one memory at word size `8`. The iterated +product `(S₀.prod S₁).prod S₂` of their framed refinements implements the product of three stack +specifications. The tests run one linked client against concrete memory, transport the +specification's answer to it with `Refinement.simulates_client`, and check that the amortized +certificates add across both products to three memory units per operation, which also gives a +worst-case `Module.ChargeBound`. +-/ + +@[expose] public section + +namespace ComplexityTests.DataStructureMemoryProduct + +open Cslib.WordRAM Cslib.DataStructure Cslib.DataStructure.Memory Cslib.DataStructure.Queue + +/-- A bounded stack of capacity `2` at word size `8`. -/ +abbrev S : StateTransitionSpec := Stack.WordRAM.spec 8 2 + +/-- The layout of stack `i`: its count cell at `3 i`, its items at `3 i + 1` and `3 i + 2`. -/ +def layout (i : ℕ) : Stack.Memory.Layout := ⟨⟨3 * i, 1⟩, ⟨3 * i + 1, 2⟩⟩ + +/-- The three layouts are well formed. -/ +theorem valid (i : ℕ) (hi : i < 3) : (layout i).Valid 8 2 := + Stack.Memory.valid_allocLayout (f := 3 * i) (Fits.of_le (show Fits 8 9 by decide) (by omega)) + +/-- Nested products need their transition function named. -/ +instance : (product (product S S) S).HasStepFn + (productStepFn _ _ (productStepFn S S (stack ℕ).stepFn (stack ℕ).stepFn) (stack ℕ).stepFn) := + instHasStepFnProduct _ _ + +/-- Stack `i`, framed by its layout. -/ +abbrev stackAt (i : ℕ) (hi : i < 3) : Framed S 8 := Stack.Memory.framed (valid i hi) + +/-- The three stacks in one memory. -/ +abbrev triple : Framed (product (product S S) S) 8 := + ((stackAt 0 (by decide)).prod (stackAt 1 (by decide))).prod (stackAt 2 (by decide)) + +/-- Push `1` and `2` on the first stack and `3` on the third, then pop the first stack twice and +read the sizes. -/ +def client : (product (product S S) S).Client (List (Option ℕ) × List ℕ) := do + let _ ← (product (product S S) S).call (.inl (.inl (.push 1))) + let _ ← (product (product S S) S).call (.inl (.inl (.push 2))) + let _ ← (product (product S S) S).call (.inr (.push 3)) + let a ← (product (product S S) S).call (.inl (.inl .pop)) + let b ← (product (product S S) S).call (.inl (.inl .pop)) + let m ← (product (product S S) S).call (.inl (.inr .size)) + let n ← (product (product S S) S).call (.inr .size) + pure ([a, b], [m, n]) + +/-- Nine allocated zero cells: three empty stacks. -/ +def empty : State := ⟨fun _ => 0, 9⟩ + +/-- The linked client answers last in, first out, on each stack separately. -/ +example : (spec 8).clientResult empty (link triple.methods client) = + ([some 2, some 1], [0, 1]) := by + decide + +/-- It issues `4 · 3 + 1 + 1` memory operations, three per push and per nonempty pop, one per +size. -/ +example : (spec 8).totalCost charge empty ((spec 8).clientHistory empty + (link triple.methods client)) = 17 := by + decide + +/-- Each stack holds in the empty memory. -/ +theorem inv_layout (i : ℕ) (hi : i < 3) : Stack.Memory.Inv (layout i) empty where + count_allocated := by simp only [Block.Allocated, layout, empty]; omega + items_allocated := by simp only [Block.Allocated, layout, empty]; omega + count_le := by simp [layout, empty] + +/-- The three stacks hold in the empty memory, with disjoint footprints. -/ +theorem inv_empty : triple.inv empty := + ⟨⟨inv_layout 0 (by decide), inv_layout 1 (by decide), by decide⟩, inv_layout 2 (by decide), + by decide⟩ + +/-- The specification's answer, transported to the memory run by the iterated product. -/ +example : (spec 8).clientResult empty (link triple.methods client) = + (product (product S S) S).clientResult (triple.abs empty) client := + (triple.simulates_client client inv_empty + ⟨⟨by decide, by decide⟩, ⟨by decide, by decide⟩, ⟨by decide, by decide⟩, trivial, trivial, + trivial, trivial, trivial⟩).2.1 + +/-- Every operation of the three stacks costs at most three memory units, amortized with zero +potential: the stacks' certificates add across both products. -/ +theorem amortized : triple.Amortized charge (fun _ _ => 3) fun _ => 0 := by + have h₀₁ := Framed.amortized_prod (Stack.Memory.amortized (valid 0 (by decide))) + (Stack.Memory.amortized (valid 1 (by decide))) (Framed.Local.const _ 0) + (Framed.Local.const _ 0) + rw [productCharge_const] at h₀₁ + have h := Framed.amortized_prod h₀₁ (Stack.Memory.amortized (valid 2 (by decide))) + (fun _ _ _ => rfl) (Framed.Local.const _ 0) + rwa [productCharge_const] at h + +/-- With zero potential, the same certificate is a worst-case charge bound. -/ +example : triple.toModule.ChargeBound charge fun _ _ => 3 := + amortized.chargeBound + +end ComplexityTests.DataStructureMemoryProduct diff --git a/ComplexityTests/DataStructureQueueMemory.lean b/ComplexityTests/DataStructureQueueMemory.lean index 93a8793360..78d4631f2f 100644 --- a/ComplexityTests/DataStructureQueueMemory.lean +++ b/ComplexityTests/DataStructureQueueMemory.lean @@ -73,6 +73,18 @@ example : (spec 8).clientResult empty (run 8 3 client) = example : (spec 8).totalCost charge empty ((spec 8).clientHistory empty (run 8 3 client)) ≤ 69 := run_cost client room client_valid +/-- Glue of two units after each memory operation: the 47 operations add 94 units to the 49 +memory units, within the bound `(2 · 1 + 1) · 69` for `K = 1`. -/ +example : (spec 8).totalCost (fun s o => charge s o + 2) empty + ((spec 8).clientHistory empty (run 8 3 client)) = 143 := by + decide + +example : (spec 8).totalCost (fun s o => charge s o + 2) empty + ((spec 8).clientHistory empty (run 8 3 client)) ≤ 3 * 69 := + run_cost_glue client room client_valid (K := 1) fun s o => by + have := one_le_charge s o + omega + /-- A memory with too little room: the second stack would end past the 4-bit word range. -/ example : ¬Fits 4 (empty.frontier + 2 * (7 + 1)) := by decide From 0e316951149219d8e8b47bc38c35c9af4ac4c293 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 19:04:02 -0500 Subject: [PATCH 13/18] feat(DataStructure/StateTransition): instance search for nested products The product instances index their specification with `no_index`, so `product (product D E) F` and deeper products find `HasStepFn`, `Total` and `Deterministic` without a named instance. The test of three stacks in one memory drops its named instance. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity/DataStructure/StateTransition.lean | 18 ++++++++++--- .../DataStructureMemoryProduct.lean | 25 ++++++++++++++++--- 2 files changed, 36 insertions(+), 7 deletions(-) diff --git a/Complexity/DataStructure/StateTransition.lean b/Complexity/DataStructure/StateTransition.lean index 8c0ae6041b..623ad75564 100644 --- a/Complexity/DataStructure/StateTransition.lean +++ b/Complexity/DataStructure/StateTransition.lean @@ -1136,9 +1136,17 @@ state. -/ (s : D.State × E.State) (o : E.Op) : (product D E).valid s (.inr o) ↔ E.valid s.2 o := Iff.rfl +/-! ### Instances on products + +`product` is an `abbrev`, so an instance stated on `product D E` would be indexed by the unfolded +structure, whose state type is the projection `D.State × E.State`. A nested product such as +`product (product D E) F` has state type `(D.State × E.State) × F.State` after unfolding, which +that index does not match. The product instances therefore index their specification with +`no_index`, and products of any depth find them by unification. -/ + /-- Product of two total specifications is total. -/ instance instTotalProduct (D E : StateTransitionSpec.{u, v, w}) [D.Total] [E.Total] : - (product D E).Total where + (no_index (product D E)).Total where step_total | s, .inl o, hv => by obtain ⟨s₁', a, hstep⟩ := D.exists_step hv @@ -1150,7 +1158,7 @@ instance instTotalProduct (D E : StateTransitionSpec.{u, v, w}) [D.Total] [E.Tot /-- Product of two deterministic specifications is deterministic. -/ instance instDeterministicProduct (D E : StateTransitionSpec.{u, v, w}) [D.Deterministic] [E.Deterministic] : - (product D E).Deterministic where + (no_index (product D E)).Deterministic where step_deterministic {_ o _ _ _ _} h₁ h₂ := by cases o with | inl o => @@ -1173,11 +1181,13 @@ abbrev productStepFn (D E : StateTransitionSpec.{u, v, w}) | .inl o => let r := fD s.1 o; ((r.1, s.2), r.2) | .inr o => let r := fE s.2 o; ((s.1, r.1), r.2) +/-- A product of deterministic specifications is deterministic with the componentwise transition +function, at any nesting depth. -/ instance instHasStepFnProduct (D E : StateTransitionSpec.{u, v, w}) {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} {fE : (s : E.State) → (o : E.Op) → E.State × E.Result o} [D.HasStepFn fD] [E.HasStepFn fE] : - (product D E).HasStepFn (productStepFn D E fD fE) where + (no_index (product D E)).HasStepFn (productStepFn D E fD fE) where step_iff | ⟨s₁, s₂⟩, .inl o, ⟨s'₁, s'₂⟩, (a : D.Result o) => by simp only [productStepFn, StateTransitionSpec.HasStepFn.step_iff, Prod.ext_iff, and_assoc] @@ -1185,6 +1195,8 @@ instance instHasStepFnProduct (D E : StateTransitionSpec.{u, v, w}) | ⟨s₁, s₂⟩, .inr o, ⟨s'₁, s'₂⟩, (a : E.Result o) => by simp [productStepFn, StateTransitionSpec.HasStepFn.step_iff, Prod.ext_iff, and_assoc] +/-- The product of two `ofFn` specifications, with their transition functions named. It is tried +before `instHasStepFnProduct`, so such products keep this form of the transition function. -/ instance instHasStepFnProductOfFn {State₁ : Type u} {Op₁ : Type v} {Result₁ : Op₁ → Type w} {State₂ : Type u} {Op₂ : Type v} {Result₂ : Op₂ → Type w} diff --git a/ComplexityTests/DataStructureMemoryProduct.lean b/ComplexityTests/DataStructureMemoryProduct.lean index 22244481b9..52d5e75184 100644 --- a/ComplexityTests/DataStructureMemoryProduct.lean +++ b/ComplexityTests/DataStructureMemoryProduct.lean @@ -36,10 +36,27 @@ def layout (i : ℕ) : Stack.Memory.Layout := ⟨⟨3 * i, 1⟩, ⟨3 * i + 1, 2 theorem valid (i : ℕ) (hi : i < 3) : (layout i).Valid 8 2 := Stack.Memory.valid_allocLayout (f := 3 * i) (Fits.of_le (show Fits 8 9 by decide) (by omega)) -/-- Nested products need their transition function named. -/ -instance : (product (product S S) S).HasStepFn - (productStepFn _ _ (productStepFn S S (stack ℕ).stepFn (stack ℕ).stepFn) (stack ℕ).stepFn) := - instHasStepFnProduct _ _ +/-- Nested products find their transition function by instance search, at any depth. -/ +example : (product (product S S) (product S (product S S))).HasStepFn + (productStepFn _ _ (productStepFn S S (stack ℕ).stepFn (stack ℕ).stepFn) + (productStepFn _ _ (stack ℕ).stepFn + (productStepFn S S (stack ℕ).stepFn (stack ℕ).stepFn))) := + inferInstance + +/-- So do nested products of arbitrary deterministic specifications. -/ +example (D E F : StateTransitionSpec) + {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} + {fE : (s : E.State) → (o : E.Op) → E.State × E.Result o} + {fF : (s : F.State) → (o : F.Op) → F.State × F.Result o} + [D.HasStepFn fD] [E.HasStepFn fE] [F.HasStepFn fF] : + (product D (product E F)).HasStepFn (productStepFn _ _ fD (productStepFn _ _ fE fF)) := + inferInstance + +/-- Totality and determinism of nested products of relational specifications. -/ +example (D E F : StateTransitionSpec) [D.Total] [E.Total] [F.Total] [D.Deterministic] + [E.Deterministic] [F.Deterministic] : + (product (product D E) F).Total ∧ (product (product D E) F).Deterministic := + ⟨inferInstance, inferInstance⟩ /-- Stack `i`, framed by its layout. -/ abbrev stackAt (i : ℕ) (hi : i < 3) : Framed S 8 := Stack.Memory.framed (valid i hi) From 5a872f3b6b3d420e650fa8da30c04c4404ae2592 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 19:23:43 -0500 Subject: [PATCH 14/18] feat(DataStructure/Memory): allocators, and a shared room budget for growing structures `Memory.Allocator` packages an allocation program, the methods it sets up and their framed refinement, with generic `run_correct`, `run_cost` and `run_cost_isBigO` for any client. `Allocator.prod` allocates structures one after the other and `Allocator.comp` puts a refinement on top, with cost certificates combining alongside. `Memory.Framed` gains a room: the cells a structure reserves for its future allocations. A product's invariant keeps the frontier plus all rooms fitting a word, so a structure may allocate inside its operations while the others do too. `Framed.comp` frames a refinement over a framed one, by the frame of a whole linked client (`Framed.modifies_demonic`). The queue over two stacks is restated through the allocator, with no setup proof of its own. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity.lean | 1 + Complexity/DataStructure/Memory/Alloc.lean | 423 +++++++++++++++++ Complexity/DataStructure/Memory/Frame.lean | 426 ++++++++++++------ Complexity/DataStructure/Queue/Memory.lean | 158 ++----- Complexity/DataStructure/Stack/Memory.lean | 58 ++- Complexity/DataStructure/StateTransition.lean | 26 +- .../DataStructureMemoryProduct.lean | 73 ++- 7 files changed, 903 insertions(+), 262 deletions(-) create mode 100644 Complexity/DataStructure/Memory/Alloc.lean diff --git a/Complexity.lean b/Complexity.lean index 5f2e2464a6..37402802bc 100644 --- a/Complexity.lean +++ b/Complexity.lean @@ -2873,6 +2873,7 @@ public import Complexity.DataStructure.HashFamily.WordRAM.DoubleTabulation public import Complexity.DataStructure.HashFamily.WordRAM.KWise public import Complexity.DataStructure.HashFamily.WordRAM.MultiplyShift public import Complexity.DataStructure.HashFamily.WordRAM.Tabulation +public import Complexity.DataStructure.Memory.Alloc public import Complexity.DataStructure.Memory.Field public import Complexity.DataStructure.Memory.Frame public import Complexity.DataStructure.Memory.Spec diff --git a/Complexity/DataStructure/Memory/Alloc.lean b/Complexity/DataStructure/Memory/Alloc.lean new file mode 100644 index 0000000000..8b0d8416af --- /dev/null +++ b/Complexity/DataStructure/Memory/Alloc.lean @@ -0,0 +1,423 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: candidate + +public import Complexity.DataStructure.Memory.Frame +public import Complexity.Foundations.Analysis.Asymptotics.CostBound + +/-! +# Allocating data structures over the memory oracle + +A program that uses data structures over `Memory.spec w` first allocates their storage, then runs +a client of their specification through their methods. A `Memory.Allocator D w` packages the +first half for a specification `D`: + +* the allocation program, which returns a handle such as a layout of named fields; +* the methods, given the handle; +* the abstract state the allocation sets up, the room it needs, and a bound on its cost; +* for each memory with that room, the framed refinement it sets up (`Allocator.framed`), with the + proof that the allocation sets it up (`Memory.Allocates`). + +`Allocator.run` allocates and then runs a client. `Allocator.run_correct` and +`Allocator.run_cost` hold for every allocator and every client: a client admissible for the +specification issues only admissible memory operations, answers as the specification does, and +costs at most the allocation's cost, the potential once allocated, and the upper charges of its +operations. A certificate `Allocator.Amortized` carries the cost side. `Allocator.run_cost_isBigO` +exports a constant per-operation charge as `O(m)` in the number `m` of operations. + +Allocators combine as their structures do. + +* `Allocator.prod` allocates two structures one after the other, and sets up their product over + one memory (`Memory.Framed.prod`). Its room is the sum of the rooms, so it reserves both + structures' growth. Iterated, `A₁.prod (A₂.prod A₃)` allocates the three structures of a client + such as Dijkstra's algorithm, which runs over `product PQ (product Dict Adj)`. +* `Allocator.comp` puts a refinement on top, such as a queue over the pair of its stacks, with + `Memory.Framed.comp`. + +Certificates combine along with them (`Allocator.Amortized.prod`, `Allocator.Amortized.comp`). +A structure that lives in its input rather than in fresh memory, such as a graph given as input, +states what it needs of the memory as the precondition `Allocator.Pre`, which allocations of +other structures preserve. + +## Main definitions + +* `Memory.Allocates`: what an allocation program establishes for the structure it sets up. +* `Memory.Allocator`: an allocation program, the methods it sets up, and their framed refinement. +* `Memory.Allocator.run`: allocate, then run a client. +* `Memory.Allocator.prod`, `Memory.Allocator.comp`: products and refinements on top. +* `Memory.Allocator.Amortized`: an amortized cost certificate of an allocator. + +## Main results + +* `Memory.Allocator.run_correct`: the run is admissible and answers as the specification. +* `Memory.Allocator.run_cost`, `Memory.Allocator.run_cost_le`: its memory cost. +* `Memory.Allocator.run_cost_isBigO`: the cost as `O(m)` for constant per-operation charges. +* `Memory.Allocator.Amortized.prod`, `Memory.Allocator.Amortized.comp`: certificates combine. +-/ + +@[expose] public section + +namespace Cslib.DataStructure.Memory + +open Cslib.WordRAM + +variable {w : ℕ} + +/-- What running the allocation program `alloc` from the memory `s` establishes for the framed +refinement `M`: its operations are admissible and cost at most `cost`, it changes no allocated +cell, and it sets up `M` with the methods of the handle it returns, holding at the abstract state +`init` on fresh cells, with the frontier plus `M`'s room at most `room` beyond the old +frontier. -/ +structure Allocates {D : StateTransitionSpec.{0, 0, 0}} {H : Type} (alloc : Prog H) + (methods : H → Methods D (spec w)) (init : D.State) (room cost : ℕ) (s : State) + (M : Framed D w) : Prop where + /-- The allocation's operations are admissible. -/ + validHistory : (spec w).ValidHistory s ((spec w).clientHistory s alloc) + /-- The allocation costs at most `cost`. -/ + cost_le : (spec w).totalCost charge s ((spec w).clientHistory s alloc) ≤ cost + /-- The allocation changes no allocated cell. -/ + mem_eq : ∀ a < s.frontier, ((spec w).clientState s alloc).mem a = s.mem a + /-- The structure runs the methods of the returned handle. -/ + methods_eq : M.methods = methods ((spec w).clientResult s alloc) + /-- The structure holds after the allocation. -/ + inv : M.inv ((spec w).clientState s alloc) + /-- It represents `init`. -/ + abs_eq : M.abs ((spec w).clientState s alloc) = init + /-- Its footprint is fresh. -/ + le_footprint : ∀ a ∈ M.footprint ((spec w).clientState s alloc), s.frontier ≤ a + /-- The allocation and the structure's reserved room take at most `room` cells. -/ + room_le : ((spec w).clientState s alloc).frontier + M.room ((spec w).clientState s alloc) ≤ + s.frontier + room + +/-- **Allocators.** A memory program `alloc` that allocates a structure implementing `D`, and +the structure's methods, given the handle that `alloc` returns. From a memory satisfying `Pre` in +which `room` cells beyond the frontier fit a word, the allocation sets up the framed refinement +`framed s`, which represents `init`, at a memory cost of at most `cost`. -/ +structure Allocator (D : StateTransitionSpec.{0, 0, 0}) (w : ℕ) where + /-- What the allocation returns, such as a layout of named fields. -/ + Handle : Type + /-- The allocation program. -/ + alloc : Prog Handle + /-- The structure's methods, given the handle. -/ + methods : Handle → Methods D (spec w) + /-- The abstract state the allocation sets up. -/ + init : D.State + /-- The cells the allocation takes and the room it reserves for growth. -/ + room : ℕ + /-- A bound on the memory cost of the allocation. -/ + cost : ℕ + /-- What the allocation needs of the memory besides room, such as an input it reads. -/ + Pre : State → Prop := fun _ => True + /-- Runs that keep every allocated cell, such as other allocations, keep the precondition. -/ + pre_fresh : ∀ {s s' : State}, Pre s → (∀ a < s.frontier, s'.mem a = s.mem a) → + s.frontier ≤ s'.frontier → Pre s' := by intros; trivial + /-- The framed refinement that allocating from `s` sets up. -/ + framed : (s : State) → Pre s → Fits w (s.frontier + room) → Framed D w + /-- Allocating from `s` sets it up. -/ + allocates : ∀ s (hs : Pre s) (hroom : Fits w (s.frontier + room)), + Allocates alloc methods init room cost s (framed s hs hroom) + +namespace Allocator + +variable {D D₁ D₂ E : StateTransitionSpec.{0, 0, 0}} + +/-! ## Runs -/ + +section Run + +variable (A : Allocator D w) + +/-- Allocate the structure, then run a client of its specification through its methods. -/ +def run {β : Type} (p : D.Client β) : Prog β := A.alloc >>= fun h => link (A.methods h) p + +/-- The history of a run: the allocation's, then the linked client's. -/ +theorem clientHistory_run {β : Type} (p : D.Client β) (s : State) : + (spec w).clientHistory s (A.run p) = (spec w).clientHistory s A.alloc ++ + (spec w).clientHistory ((spec w).clientState s A.alloc) + (link (A.methods ((spec w).clientResult s A.alloc)) p) := + (spec w).clientHistory_bind _ _ s + +/-- The answer of a run is the linked client's. -/ +theorem clientResult_run {β : Type} (p : D.Client β) (s : State) : + (spec w).clientResult s (A.run p) = + (spec w).clientResult ((spec w).clientState s A.alloc) + (link (A.methods ((spec w).clientResult s A.alloc)) p) := + (spec w).clientResult_bind _ _ s + +/-- A run ends where the linked client ends. -/ +theorem clientState_run {β : Type} (p : D.Client β) (s : State) : + (spec w).clientState s (A.run p) = + (spec w).clientState ((spec w).clientState s A.alloc) + (link (A.methods ((spec w).clientResult s A.alloc)) p) := + (spec w).clientState_bind _ _ s + +variable {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} [D.HasStepFn fD] + +/-- **Correctness of a run.** From a memory with the allocator's precondition and room, a client +admissible for the specification issues only admissible memory operations and answers as the +specification does from `A.init`; the structure then holds at the specified final state. -/ +theorem run_correct {β : Type} (p : D.Client β) {s : State} (hs : A.Pre s) + (hroom : Fits w (s.frontier + A.room)) (hv : D.ValidHistory A.init (D.clientHistory A.init p)) : + (spec w).ValidHistory s ((spec w).clientHistory s (A.run p)) ∧ + (spec w).clientResult s (A.run p) = D.clientResult A.init p ∧ + (A.framed s hs hroom).inv ((spec w).clientState s (A.run p)) ∧ + (A.framed s hs hroom).abs ((spec w).clientState s (A.run p)) = D.clientState A.init p := by + have hA := A.allocates s hs hroom + obtain ⟨hvE, hres, habs, hinv⟩ := + (A.framed s hs hroom).simulates_client p hA.inv (by rw [hA.abs_eq]; exact hv) + rw [hA.methods_eq] at hvE hres habs hinv + rw [hA.abs_eq] at hres habs + rw [clientHistory_run, clientResult_run, clientState_run, + StateTransitionSpec.validHistory_append, StateTransitionSpec.run_runClient] + exact ⟨⟨hA.validHistory, hvE⟩, hres, hinv, habs⟩ + +end Run + +/-! ## Cost certificates -/ + +/-- **An amortized cost certificate of an allocator.** From every memory it can allocate from, +the structure it sets up is amortized against `upper` with a potential that reads only its +footprint and is at most `Φ₀` once allocated. -/ +def Amortized (A : Allocator D w) (upper : D.State → D.Op → ℕ) (Φ₀ : ℕ) : Prop := + ∀ s (hs : A.Pre s) (hroom : Fits w (s.frontier + A.room)), ∃ Φ : State → ℕ, + (A.framed s hs hroom).Amortized charge upper Φ ∧ (A.framed s hs hroom).Local Φ ∧ + Φ ((spec w).clientState s A.alloc) ≤ Φ₀ + +section Cost + +variable {A : Allocator D w} {fD : (s : D.State) → (o : D.Op) → D.State × D.Result o} + [D.HasStepFn fD] + +/-- **The cost of a run.** A client admissible for the specification costs at most the +allocation's cost, the potential once allocated, and the upper charges of its operations. -/ +theorem run_cost {upper : D.State → D.Op → ℕ} {Φ₀ : ℕ} (hA : A.Amortized upper Φ₀) {β : Type} + (p : D.Client β) {s : State} (hs : A.Pre s) (hroom : Fits w (s.frontier + A.room)) + (hv : D.ValidHistory A.init (D.clientHistory A.init p)) : + (spec w).totalCost charge s ((spec w).clientHistory s (A.run p)) ≤ + A.cost + Φ₀ + D.totalCost upper A.init (D.clientHistory A.init p) := by + have hal := A.allocates s hs hroom + obtain ⟨Φ, hc, -, hΦ⟩ := hA s hs hroom + have h := hc.client p hal.inv (by rw [hal.abs_eq]; exact hv) + rw [hal.methods_eq, hal.abs_eq] at h + rw [clientHistory_run, StateTransitionSpec.totalCost_append, StateTransitionSpec.run_runClient] + have := hal.cost_le + omega + +/-- The cost of a run for a constant charge `K` per operation: at most the allocation's cost, +the potential once allocated, and `K` per operation. -/ +theorem run_cost_le {K Φ₀ : ℕ} (hA : A.Amortized (fun _ _ => K) Φ₀) {β : Type} + (p : D.Client β) {s : State} (hs : A.Pre s) (hroom : Fits w (s.frontier + A.room)) + (hv : D.ValidHistory A.init (D.clientHistory A.init p)) : + (spec w).totalCost charge s ((spec w).clientHistory s (A.run p)) ≤ + A.cost + Φ₀ + K * (D.clientHistory A.init p).length := by + have h := run_cost hA p hs hroom hv + rw [StateTransitionSpec.totalCost_const, Nat.mul_comm] at h + exact h + +/-- **The exported form.** For a constant charge per operation, the memory cost of a run is +`O(m)` in the number `m` of operations, over admissible clients. -/ +theorem run_cost_isBigO {K Φ₀ : ℕ} (hA : A.Amortized (fun _ _ => K) Φ₀) {β : Type} {s : State} + (hs : A.Pre s) (hroom : Fits w (s.frontier + A.room)) : + CostIsBigO Filter.atTop (fun m : ℕ => (m : ℝ)) + (fun p : {p : D.Client β // D.ValidHistory A.init (D.clientHistory A.init p)} => + (D.clientHistory A.init p.1).length) + (fun p => (spec w).totalCost charge s ((spec w).clientHistory s (A.run p.1))) := + CostIsBigO.of_le_add_mul (A.cost + Φ₀) K fun p => run_cost_le hA p.1 hs hroom p.2 + +end Cost + +/-! ## Refinements on top -/ + +/-- **A refinement on top of an allocated structure.** The same allocation sets up the +composite `Memory.Framed.comp R M`, whose methods link `R`'s methods through the structure's, +provided `R`'s invariant holds of the allocated abstract state. -/ +def comp (R : Refinement D E) (A : Allocator E w) (hinit : R.inv A.init) : Allocator D w where + Handle := A.Handle + alloc := A.alloc + methods h o := link (A.methods h) (R.methods o) + init := R.abs A.init + room := A.room + cost := A.cost + Pre := A.Pre + pre_fresh := A.pre_fresh + framed s hs hroom := Framed.comp R (A.framed s hs hroom) + allocates s hs hroom := + have h := A.allocates s hs hroom + { validHistory := h.validHistory + cost_le := h.cost_le + mem_eq := h.mem_eq + methods_eq := by rw [Framed.comp_methods, h.methods_eq] + inv := ⟨h.inv, by rw [h.abs_eq]; exact hinit⟩ + abs_eq := by rw [Framed.comp_abs, h.abs_eq] + le_footprint := h.le_footprint + room_le := h.room_le } + +/-- **Potentials stack through a refinement on top.** A certificate of `R` against the middle +charges and one of the allocator against them give a certificate of the composite, with `R`'s +potential at the allocated state added. -/ +theorem Amortized.comp {R : Refinement D E} {A : Allocator E w} {hinit : R.inv A.init} + {middle : E.State → E.Op → ℕ} {upper : D.State → D.Op → ℕ} {Φ : E.State → ℕ} {Ψ₀ : ℕ} + (hR : R.Amortized middle upper Φ) (hA : A.Amortized middle Ψ₀) : + (A.comp R hinit).Amortized upper (Φ A.init + Ψ₀) := by + intro s hs hroom + obtain ⟨Ψ, hc, hl, hΨ⟩ := hA s hs hroom + have hal := A.allocates s hs hroom + refine ⟨fun s' => Φ ((A.framed s hs hroom).abs s') + Ψ s', Framed.amortized_comp hR hc, + Framed.Local.add (Framed.Local.comp (Framed.Local.comp_abs _ Φ)) (Framed.Local.comp hl), ?_⟩ + change Φ ((A.framed s hs hroom).abs ((spec w).clientState s A.alloc)) + + Ψ ((spec w).clientState s A.alloc) ≤ Φ A.init + Ψ₀ + rw [hal.abs_eq] + omega + +/-! ## Products -/ + +section Prod + +variable (A₁ : Allocator D₁ w) (A₂ : Allocator D₂ w) + +/-- Allocate both structures, one after the other. -/ +def allocPair : Prog (A₁.Handle × A₂.Handle) := do + let h₁ ← A₁.alloc + let h₂ ← A₂.alloc + pure (h₁, h₂) + +/-- Both allocations' histories, one after the other. -/ +theorem clientHistory_allocPair (s : State) : + (spec w).clientHistory s (allocPair A₁ A₂) = (spec w).clientHistory s A₁.alloc ++ + (spec w).clientHistory ((spec w).clientState s A₁.alloc) A₂.alloc := by + rw [allocPair, (spec w).clientHistory_bind, (spec w).clientHistory_bind] + exact congrArg (_ ++ ·) (List.append_nil _) + +/-- The memory after both allocations. -/ +theorem clientState_allocPair (s : State) : + (spec w).clientState s (allocPair A₁ A₂) = + (spec w).clientState ((spec w).clientState s A₁.alloc) A₂.alloc := by + rw [allocPair, (spec w).clientState_bind, (spec w).clientState_bind] + rfl + +/-- Both handles. -/ +theorem clientResult_allocPair (s : State) : + (spec w).clientResult s (allocPair A₁ A₂) = + ((spec w).clientResult s A₁.alloc, + (spec w).clientResult ((spec w).clientState s A₁.alloc) A₂.alloc) := by + rw [allocPair, (spec w).clientResult_bind, (spec w).clientResult_bind] + rfl + +variable {A₁} + +/-- After allocating the first structure, the second one's precondition still holds. -/ +theorem pre_after {s : State} (hs₁ : A₁.Pre s) (hroom : Fits w (s.frontier + A₁.room)) + (hs₂ : A₂.Pre s) : A₂.Pre ((spec w).clientState s A₁.alloc) := + A₂.pre_fresh hs₂ (A₁.allocates s hs₁ hroom).mem_eq (frontier_le_clientState s _) + +/-- After allocating the first structure, room for both leaves room for the second. -/ +theorem room_after {s : State} (hs₁ : A₁.Pre s) {r : ℕ} + (hroom : Fits w (s.frontier + (A₁.room + r))) : + Fits w (((spec w).clientState s A₁.alloc).frontier + r) := + hroom.of_le (by + have := (A₁.allocates s hs₁ (hroom.of_le (by omega))).room_le + omega) + +variable (A₁) + +/-- **Products of allocators.** Allocate the two structures one after the other, and set up +their product over one memory. The room and the cost add. -/ +def prod : Allocator (product D₁ D₂) w where + Handle := A₁.Handle × A₂.Handle + alloc := allocPair A₁ A₂ + methods := fun (h₁, h₂) => Methods.prod (A₁.methods h₁) (A₂.methods h₂) + init := (A₁.init, A₂.init) + room := A₁.room + A₂.room + cost := A₁.cost + A₂.cost + Pre s := A₁.Pre s ∧ A₂.Pre s + pre_fresh hs hmem hf := ⟨A₁.pre_fresh hs.1 hmem hf, A₂.pre_fresh hs.2 hmem hf⟩ + framed s hs hroom := + (A₁.framed s hs.1 (hroom.of_le (by omega))).prod + (A₂.framed ((spec w).clientState s A₁.alloc) + (pre_after A₂ hs.1 (hroom.of_le (by omega)) hs.2) (room_after hs.1 hroom)) + allocates s hs hroom := by + -- The two allocations, from `s` to `s₁` and from `s₁` to `s₂`. + have h₁ := A₁.allocates s hs.1 (hroom.of_le (by omega)) + have h₂ := A₂.allocates _ (pre_after A₂ hs.1 (hroom.of_le (by omega)) hs.2) + (room_after hs.1 hroom) + have hf₁ := frontier_le_clientState (w := w) s A₁.alloc + have hfit := h₁.room_le + have hfit₂ := h₂.room_le + obtain ⟨hinv, habs, hfp, hr⟩ := Framed.prod_of_fresh h₁.inv h₂.mem_eq + (frontier_le_clientState _ _) h₂.inv h₂.le_footprint (hroom.of_le (by omega)) + refine + { validHistory := ?_, cost_le := ?_, mem_eq := ?_, methods_eq := ?_, inv := ?_, + abs_eq := ?_, le_footprint := ?_, room_le := ?_ } + · rw [clientHistory_allocPair, StateTransitionSpec.validHistory_append, + StateTransitionSpec.run_runClient] + exact ⟨h₁.validHistory, h₂.validHistory⟩ + · rw [clientHistory_allocPair, StateTransitionSpec.totalCost_append, + StateTransitionSpec.run_runClient] + have := h₁.cost_le + have := h₂.cost_le + omega + · intro a ha + rw [clientState_allocPair, h₂.mem_eq a (ha.trans_le hf₁), h₁.mem_eq a ha] + · rw [clientResult_allocPair, Framed.prod_methods, h₁.methods_eq, h₂.methods_eq] + · rw [clientState_allocPair] + exact hinv + · rw [clientState_allocPair, habs, h₁.abs_eq, h₂.abs_eq] + · intro a ha + rw [clientState_allocPair, hfp] at ha + rcases Finset.mem_union.mp ha with ha | ha + · exact h₁.le_footprint a ha + · exact hf₁.trans (h₂.le_footprint a ha) + · rw [clientState_allocPair, hr] + omega + +/-- The product's methods run each side's methods with its own handle. -/ +@[simp] theorem prod_methods (h₁ : A₁.Handle) (h₂ : A₂.Handle) : + (A₁.prod A₂).methods (h₁, h₂) = Methods.prod (A₁.methods h₁) (A₂.methods h₂) := rfl + +/-- The product sets up both abstract states. -/ +@[simp] theorem prod_init : (A₁.prod A₂).init = (A₁.init, A₂.init) := rfl + +/-- The product's room is the sum of the rooms. -/ +@[simp] theorem prod_room : (A₁.prod A₂).room = A₁.room + A₂.room := rfl + +/-- The product's allocation costs at most the sum of the costs. -/ +@[simp] theorem prod_cost : (A₁.prod A₂).cost = A₁.cost + A₂.cost := rfl + +variable {A₁ A₂} + +/-- **Certificates add across a product of allocators**: the charges componentwise, the +potentials once allocated by sum. -/ +theorem Amortized.prod {upper₁ : D₁.State → D₁.Op → ℕ} {upper₂ : D₂.State → D₂.Op → ℕ} + {Φ₁ Φ₂ : ℕ} (hA₁ : A₁.Amortized upper₁ Φ₁) (hA₂ : A₂.Amortized upper₂ Φ₂) : + (A₁.prod A₂).Amortized (productCharge upper₁ upper₂) (Φ₁ + Φ₂) := by + intro s hs hroom + have hroom₁ : Fits w (s.frontier + A₁.room) := hroom.of_le (by simp only [prod_room]; omega) + have hs₂ := pre_after A₂ hs.1 hroom₁ hs.2 + have hroom₂ := room_after hs.1 hroom + obtain ⟨Ψ₁, hc₁, hl₁, hΨ₁⟩ := hA₁ s hs.1 hroom₁ + obtain ⟨Ψ₂, hc₂, hl₂, hΨ₂⟩ := hA₂ _ hs₂ hroom₂ + refine ⟨fun s' => Ψ₁ s' + Ψ₂ s', Framed.amortized_prod hc₁ hc₂ hl₁ hl₂, + Framed.Local.add hl₁.prod_left hl₂.prod_right, ?_⟩ + -- The first potential is unchanged by the second allocation. + have h₁ := A₁.allocates s hs.1 hroom₁ + have h₂ := A₂.allocates _ hs₂ hroom₂ + have hfit := h₁.room_le + have hfit₂ := h₂.room_le + have hkeep := hl₁ h₁.inv + (fun a ha => h₂.mem_eq a ((A₁.framed s hs.1 hroom₁).lt_frontier h₁.inv ha)) + (frontier_le_clientState _ _) (hroom.of_le (by simp only [prod_room]; omega)) + change Ψ₁ ((spec w).clientState s (allocPair A₁ A₂)) + + Ψ₂ ((spec w).clientState s (allocPair A₁ A₂)) ≤ Φ₁ + Φ₂ + rw [clientState_allocPair, hkeep] + omega + +end Prod + +end Allocator + +end Cslib.DataStructure.Memory diff --git a/Complexity/DataStructure/Memory/Frame.lean b/Complexity/DataStructure/Memory/Frame.lean index 9c41b35613..a2bec2f0be 100644 --- a/Complexity/DataStructure/Memory/Frame.lean +++ b/Complexity/DataStructure/Memory/Frame.lean @@ -11,54 +11,64 @@ public import Complexity.DataStructure.Memory.Spec public import Complexity.DataStructure.StateTransition.Refinement /-! -# Footprints, and products of data structures sharing one memory - -A `Memory.Framed D w` is a refinement of `D` over `Memory.spec w` together with a footprint: the -finite set of allocated cells that holds the representation of each memory state. Three -obligations make the footprint honest. - -* `congr`: the invariant, the abstraction and the footprint itself read only the footprint's - cells, and survive a growing frontier. -* `modifies`: a method run changes no allocated cell outside the footprint, and the footprint - grows only into cells that the run allocated. +# Footprints, room, and products of data structures sharing one memory + +A `Memory.Framed D w` is a refinement of `D` over `Memory.spec w` together with a footprint, the +finite set of allocated cells that holds the representation of each memory state, and a room, +the number of cells the structure has reserved for its future allocations. Three obligations make +them honest. + +* `congr`: the invariant, the abstraction, the footprint and the room read only the footprint's + cells, and survive a growing frontier that leaves the reserved room fitting a word. +* `modifies`: a method run changes no allocated cell outside the footprint, the footprint grows + only into cells that the run allocated, and the frontier advances by at most the room the run + gives up. * `lt_frontier`: the footprint is allocated. -Footprints may change from state to state, so that a structure can grow into cells it allocates -inside its operations. A structure laid out in fixed blocks has a constant footprint, and -`Framed.ofStatic` asks for less. Allocating inside an operation also needs room, since -`Memory.valid` asks the new frontier to fit a word. Neither the abstract state nor the structure's -own cells determine the frontier, and the other side of a product moves it, so such a structure -needs a shared budget of room, which this file does not provide. The structures built so far -allocate their storage once, up front. - -Two framed refinements implement the product of their specifications over one shared memory, -as long as their footprints are disjoint (`Framed.prod`). Disjointness is part of the product's -invariant. It holds after each method run because a run of one side writes only its own cells -and fresh ones, and the other side's footprint is allocated, hence not fresh. The product is -framed again, with the union of the footprints, so products iterate: -`(M₁.prod M₂).prod M₃` implements three structures in one memory. A potential that reads only the -footprint (`Framed.Local`) is left unchanged by the other side's methods, so amortized +A structure laid out in fixed blocks has a constant footprint and no room, and `Framed.ofStatic` +asks for less. A structure that allocates inside its operations, such as a growing array, +reserves room for all of its future growth. Its invariant may then ask that the frontier plus its +room fit a word, which is what `Memory.valid` needs of each allocation. That fact is not +preserved by other structures' allocations on its own, so the product keeps it for everybody. + +**Products.** Two framed refinements implement the product of their specifications over one +shared memory (`Framed.prod`). The product's invariant asks that the footprints be disjoint and +that the frontier plus both rooms fit a word. This is the shared room budget: a method run of one +side writes only its own cells and fresh ones, and advances the frontier by at most the room it +gives up, so the other side keeps its cells and its room, and the budget still fits. The product +is framed again, with the union of the footprints and the sum of the rooms, so products iterate: +`(M₁.prod M₂).prod M₃` implements three structures in one memory. A potential that reads only +the footprint (`Framed.Local`) is left unchanged by the other side's methods, so amortized certificates add across the product (`Framed.amortized_prod`), and so do worst-case `Module.ChargeBound`s, as the case of zero potentials. -Allocation is a memory program like any other. A run that leaves every allocated cell unchanged, -such as the allocation of a further structure, preserves a framed refinement (`Framed.of_fresh`), -and `Framed.prod_of_fresh` sets up a product from structures allocated one after the other. +**Composition.** A refinement of `D` over `E` composed with a framed refinement of `E` is framed +by the same footprint and room (`Framed.comp`), since a whole linked client keeps the frame of its +calls (`Framed.modifies_demonic`). So a structure built on others, such as a queue over two +stacks, joins a product like any other. + +**Allocation** is a memory program like any other. A run that leaves every allocated cell +unchanged and leaves the room fitting, such as the allocation of a further structure, preserves a +framed refinement (`Framed.of_fresh`), and `Framed.prod_of_fresh` sets up a product from +structures allocated one after the other. `Memory.Allocator` packages this. ## Main definitions -* `Memory.Framed`: a refinement over memory with a footprint. -* `Memory.Framed.ofStatic`: the constructor for a constant footprint. +* `Memory.Framed`: a refinement over memory with a footprint and a room. +* `Memory.Framed.ofStatic`: the constructor for a constant footprint and no room. * `Memory.Framed.Local`: a function of memory that reads only the footprint. * `Memory.Framed.prod`: two framed refinements with disjoint footprints implement the product. +* `Memory.Framed.comp`: a refinement over a framed refinement is framed. ## Main results -* `Memory.frontier_le_of_clientRun`: a run never lowers the frontier. +* `Memory.frontier_le_of_clientRun`, `Memory.frontier_le_clientState`: a run never lowers the + frontier. * `Memory.mem_run_of_lt_frontier`, `Memory.mem_clientState_of_lt_frontier`: a run changes an allocated cell only by writing it. +* `Memory.Framed.modifies_demonic`: a linked client keeps the frame of its calls. * `Memory.Framed.frame`: a method run of one structure leaves a structure with a disjoint - footprint unchanged. + footprint unchanged, and the shared room budget fitting. * `Memory.Framed.prod_methods`: the product's methods are the two sides' methods. * `Memory.Framed.amortized_prod`: potentials and charges add across the product. * `Memory.Framed.of_fresh`, `Memory.Framed.prod_of_fresh`: allocation preserves structures and @@ -75,6 +85,8 @@ explicit finite footprints as in `Cslib.WordRAM.HeapAssertion`; see namespace Cslib.DataStructure.Memory +open Cslib.WordRAM + variable {w : ℕ} /-! ## Runs and allocated cells -/ @@ -87,6 +99,12 @@ theorem frontier_le_of_clientRun {α : Type} {s s' : State} {p : Prog α} {a : rw [← (spec w).run_runClient] exact frontier_le_run s _ +/-- No memory program lowers the frontier. -/ +theorem frontier_le_clientState {α : Type} (s : State) (p : Prog α) : + s.frontier ≤ ((spec w).clientState s p).frontier := by + rw [← (spec w).run_runClient] + exact frontier_le_run s _ + /-- A sequence of operations changes an allocated cell only by writing it. -/ theorem mem_run_of_lt_frontier (s : State) (os : List Op) {a : ℕ} (ha : a < s.frontier) (hw : ∀ v, Op.write a v ∉ os) : ((spec w).run s os).1.mem a = s.mem a := by @@ -107,74 +125,185 @@ theorem mem_clientState_of_lt_frontier {α : Type} (s : State) (p : Prog α) {a /-! ## Framed refinements -/ -/-- A refinement of `D` over memory whose representation lives in a footprint of allocated -cells. The invariant, the abstraction and the footprint read only the footprint's cells; a -method run changes no allocated cell outside the footprint; and the footprint grows only into -cells at or above the old frontier, which the run allocated. -/ +/-- A refinement of `D` over memory whose representation lives in a footprint of allocated cells, +with room reserved for its growth. The invariant, the abstraction, the footprint and the room +read only the footprint's cells; a method run changes no allocated cell outside the footprint; +the footprint grows only into cells at or above the old frontier, which the run allocated; and +the frontier advances by at most the room the run gives up. -/ structure Framed (D : StateTransitionSpec.{0, 0, 0}) (w : ℕ) extends Refinement D (spec w) where /-- The cells holding the representation. -/ footprint : State → Finset ℕ + /-- The cells reserved for the structure's future allocations. -/ + room : State → ℕ /-- The footprint is allocated. -/ lt_frontier : ∀ {s a}, inv s → a ∈ footprint s → a < s.frontier - /-- The invariant, the abstraction and the footprint read only the footprint's cells, and - survive a growing frontier. -/ + /-- The invariant, the abstraction, the footprint and the room read only the footprint's + cells, and survive a growing frontier that leaves the room fitting a word. -/ congr : ∀ {s s'}, inv s → (∀ a ∈ footprint s, s'.mem a = s.mem a) → - s.frontier ≤ s'.frontier → inv s' ∧ abs s' = abs s ∧ footprint s' = footprint s - /-- A method run changes no allocated cell outside the footprint, and the footprint grows - only into fresh cells. -/ + s.frontier ≤ s'.frontier → Fits w (s'.frontier + room s) → + inv s' ∧ abs s' = abs s ∧ footprint s' = footprint s ∧ room s' = room s + /-- A method run changes no allocated cell outside the footprint, the footprint grows only + into fresh cells, and the frontier advances by at most the room given up. -/ modifies : ∀ {s} o {a s' tr}, inv s → D.valid (abs s) o → (spec w).ClientRun s (methods o) a s' tr → (∀ b < s.frontier, b ∉ footprint s → s'.mem b = s.mem b) ∧ - ∀ b ∈ footprint s', b ∈ footprint s ∨ s.frontier ≤ b + (∀ b ∈ footprint s', b ∈ footprint s ∨ s.frontier ≤ b) ∧ + s'.frontier + room s' ≤ s.frontier + room s namespace Framed -variable {D D₁ D₂ : StateTransitionSpec.{0, 0, 0}} +variable {D D₁ D₂ E : StateTransitionSpec.{0, 0, 0}} -/-- The constructor for a constant footprint `fp`: the invariant and abstraction read only `fp`, -and each method's final state agrees with its initial state on the allocated cells outside -`fp`. -/ +/-- The constructor for a constant footprint `fp` and no room: the invariant and abstraction read +only `fp`, and each method's final state agrees with its initial state on the allocated cells +outside `fp` and has the same frontier. -/ def ofStatic (M : Refinement D (spec w)) (fp : Finset ℕ) (lt_frontier : ∀ {s a}, M.inv s → a ∈ fp → a < s.frontier) (congr : ∀ {s s'}, M.inv s → (∀ a ∈ fp, s'.mem a = s.mem a) → s.frontier ≤ s'.frontier → M.inv s' ∧ M.abs s' = M.abs s) (modifies : ∀ {s} o, M.inv s → D.valid (M.abs s) o → ∀ b < s.frontier, b ∉ fp → - ((spec w).clientState s (M.methods o)).mem b = s.mem b) : + ((spec w).clientState s (M.methods o)).mem b = s.mem b) + (frontier_eq : ∀ {s} o, M.inv s → D.valid (M.abs s) o → + ((spec w).clientState s (M.methods o)).frontier = s.frontier) : Framed D w where toRefinement := M footprint _ := fp + room _ := 0 lt_frontier := lt_frontier - congr hs h hf := ⟨(congr hs h hf).1, (congr hs h hf).2, rfl⟩ + congr hs h hf _ := ⟨(congr hs h hf).1, (congr hs h hf).2, rfl, rfl⟩ modifies {s} o {a s' tr} hs hv hrun := by obtain ⟨_, _, rfl, _⟩ := ((spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun - exact ⟨modifies o hs hv, fun b hb => .inl hb⟩ + exact ⟨modifies o hs hv, fun b hb => .inl hb, (frontier_eq o hs hv).le⟩ variable (M : Framed D w) /-- A function of memory reads only the footprint: it is unchanged when the footprint's cells -are and the frontier grows. The abstraction is one (`Local.abs`); so are potentials that a -product must keep apart. -/ +are and the frontier grows, leaving the room fitting a word. The abstraction is one +(`Local.abs`), and so is the room (`Local.room`); so are potentials that a product must keep +apart. -/ def Local {β : Type*} (f : State → β) : Prop := ∀ {s s'}, M.inv s → (∀ a ∈ M.footprint s, s'.mem a = s.mem a) → - s.frontier ≤ s'.frontier → f s' = f s + s.frontier ≤ s'.frontier → Fits w (s'.frontier + M.room s) → f s' = f s /-- The abstraction reads only the footprint. -/ -theorem Local.abs : M.Local M.abs := fun hs h hf => (M.congr hs h hf).2.1 +theorem Local.abs : M.Local M.abs := fun hs h hf hr => by + obtain ⟨-, habs, -⟩ := M.congr hs h hf hr + exact habs + +/-- The room reads only the footprint. -/ +theorem Local.room : M.Local M.room := fun hs h hf hr => by + obtain ⟨-, -, -, hroom⟩ := M.congr hs h hf hr + exact hroom /-- A constant reads no cell. -/ -theorem Local.const {β : Type*} (b : β) : M.Local fun _ => b := fun _ _ _ => rfl +theorem Local.const {β : Type*} (b : β) : M.Local fun _ => b := fun _ _ _ _ => rfl /-- Sums of local functions are local. -/ -theorem Local.add {f g : State → ℕ} (hf : M.Local f) (hg : M.Local g) : - M.Local fun s => f s + g s := fun hs h hfr => - congrArg₂ (· + ·) (hf hs h hfr) (hg hs h hfr) +theorem Local.add {M : Framed D w} {f g : State → ℕ} (hf : M.Local f) (hg : M.Local g) : + M.Local fun s => f s + g s := fun hs h hfr hr => + congrArg₂ (· + ·) (hf hs h hfr hr) (hg hs h hfr hr) -/-- A run that leaves every allocated cell unchanged preserves a framed refinement. Allocating -another structure is such a run. -/ +/-- A function of the abstract state is local. -/ +theorem Local.comp_abs {β : Type*} (f : D.State → β) : M.Local fun s => f (M.abs s) := + fun hs h hfr hr => congrArg f (Local.abs M hs h hfr hr) + +/-- A run that leaves every allocated cell unchanged and leaves the room fitting preserves a +framed refinement. Allocating another structure is such a run. -/ theorem of_fresh {s s' : State} (hs : M.inv s) - (hmem : ∀ a < s.frontier, s'.mem a = s.mem a) (hf : s.frontier ≤ s'.frontier) : - M.inv s' ∧ M.abs s' = M.abs s ∧ M.footprint s' = M.footprint s := - M.congr hs (fun a ha => hmem a (M.lt_frontier hs ha)) hf + (hmem : ∀ a < s.frontier, s'.mem a = s.mem a) (hf : s.frontier ≤ s'.frontier) + (hroom : Fits w (s'.frontier + M.room s)) : + M.inv s' ∧ M.abs s' = M.abs s ∧ M.footprint s' = M.footprint s ∧ M.room s' = M.room s := + M.congr hs (fun a ha => hmem a (M.lt_frontier hs ha)) hf hroom + +/-- **The frame of a client.** A run of a linked admissible client changes no cell outside the +footprint that was allocated before it, grows the footprint only into fresh cells, and advances +the frontier by at most the room it gives up, as each method run does. -/ +theorem modifies_demonic {α : Type} (p : D.Client α) {s s' : State} {a : α} + {tr : (spec w).Transcript} (hs : M.inv s) (hv : D.DemonicValid (M.abs s) p) + (hrun : (spec w).ClientRun s (link M.methods p) a s' tr) : + (∀ b < s.frontier, b ∉ M.footprint s → s'.mem b = s.mem b) ∧ + (∀ b ∈ M.footprint s', b ∈ M.footprint s ∨ s.frontier ≤ b) ∧ + s'.frontier + M.room s' ≤ s.frontier + M.room s := by + induction p generalizing s tr with + | pure b => + obtain ⟨-, rfl, -⟩ := ((spec w).clientRun_pure_iff _ _ _ _ _).mp hrun + exact ⟨fun _ _ _ => rfl, fun _ hb => .inl hb, le_rfl⟩ + | liftBind o k ih => + obtain ⟨hv_o, hv_k⟩ := (D.demonicValid_liftBind_iff _ o k).mp hv + have heq : link M.methods (.liftBind o k) = + (M.methods o).bind (fun r => link M.methods (k r)) := rfl + rw [heq] at hrun + obtain ⟨r, s₁, tr₁, tr₂, hrun₁, hrun₂, rfl⟩ := (spec w).clientRun_bind_iff.mp hrun + obtain ⟨hstep, hs₁⟩ := M.correct o hs hv_o hrun₁ + obtain ⟨hmod₁, hgrow₁, hroom₁⟩ := M.modifies o hs hv_o hrun₁ + obtain ⟨hmod₂, hgrow₂, hroom₂⟩ := ih r hs₁ (hv_k _ r hstep) hrun₂ + have hf₁ := frontier_le_of_clientRun hrun₁ + refine ⟨fun b hb hb' => ?_, fun b hb => ?_, by omega⟩ + · have hb₁ : b ∉ M.footprint s₁ := fun h => + (hgrow₁ b h).elim hb' fun h' => absurd hb (Nat.not_lt.mpr h') + rw [hmod₂ b (hb.trans_le hf₁) hb₁, hmod₁ b hb hb'] + · rcases hgrow₂ b hb with h | h + · exact hgrow₁ b h + · exact .inr (hf₁.trans h) + +/-! ## Composition -/ + +/-- **A refinement over a framed refinement is framed**, by the same footprint and room: the +upper invariant and abstraction read the lower state only through its abstraction, and each +upper method is a linked client of the lower methods. -/ +def comp (R : Refinement D E) (M : Framed E w) : Framed D w where + toRefinement := R.comp M.toRefinement + footprint := M.footprint + room := M.room + lt_frontier hs ha := M.lt_frontier hs.1 ha + congr {s _} hs h hf hr := by + obtain ⟨hM, hR⟩ := hs + obtain ⟨hM', habs, hfp, hroom⟩ := M.congr hM h hf hr + refine ⟨⟨hM', ?_⟩, ?_, hfp, hroom⟩ + · rwa [habs] + · change R.abs (M.abs _) = R.abs (M.abs s) + rw [habs] + modifies {_} o {_ _ _} hs hv hrun := + M.modifies_demonic (R.methods o) hs.1 (R.valid o hs.2 hv) hrun + +/-- The composite's methods link the upper methods through the lower ones. -/ +theorem comp_methods (R : Refinement D E) (M : Framed E w) : + (comp R M).methods = fun o => link M.methods (R.methods o) := rfl + +/-- The composite is the composition of the refinements. -/ +theorem comp_toRefinement (R : Refinement D E) (M : Framed E w) : + (comp R M).toRefinement = R.comp M.toRefinement := rfl + +/-- The composite's invariant: the lower invariant, and the upper invariant of the lower +abstraction. -/ +theorem comp_inv_iff (R : Refinement D E) (M : Framed E w) (s : State) : + (comp R M).inv s ↔ M.inv s ∧ R.inv (M.abs s) := Iff.rfl + +/-- The composite represents a memory state by the upper abstraction of the lower one. -/ +@[simp] theorem comp_abs (R : Refinement D E) (M : Framed E w) (s : State) : + (comp R M).abs s = R.abs (M.abs s) := rfl + +/-- The composite's footprint is the lower footprint. -/ +@[simp] theorem comp_footprint (R : Refinement D E) (M : Framed E w) (s : State) : + (comp R M).footprint s = M.footprint s := rfl + +/-- The composite's room is the lower room. -/ +@[simp] theorem comp_room (R : Refinement D E) (M : Framed E w) (s : State) : + (comp R M).room s = M.room s := rfl + +/-- A function local to the lower framed refinement is local to the composite. -/ +theorem Local.comp {R : Refinement D E} {M : Framed E w} {β : Type*} {f : State → β} + (hf : M.Local f) : (comp R M).Local f := fun hs h hfr hr => hf hs.1 h hfr hr + +/-- **Potentials stack across composition** with a framed refinement, as for refinements +(`Refinement.Amortized.comp`). -/ +theorem amortized_comp {R : Refinement D E} {M : Framed E w} {lower : State → Op → ℕ} + {middle : E.State → E.Op → ℕ} {upper : D.State → D.Op → ℕ} {Φ : E.State → ℕ} + {Ψ : State → ℕ} (A : R.Amortized middle upper Φ) (B : M.Amortized lower middle Ψ) : + (comp R M).Amortized lower upper fun s => Φ (M.abs s) + Ψ s := + A.comp B + +/-! ## Frames of disjoint structures -/ variable {M} (N : Framed D₂ w) @@ -186,96 +315,122 @@ theorem agree_of_run {s : State} (o : D.Op) {a : D.Result o} {s' : State} ∀ b ∈ N.footprint s, s'.mem b = s.mem b := fun b hb => (M.modifies o hM hv hrun).1 b (N.lt_frontier hN hb) (Finset.disjoint_right.mp hd hb) +/-- A method run of `M` leaves room for `N` when both rooms fitted before. -/ +theorem room_of_run {s : State} (o : D.Op) {a : D.Result o} {s' : State} + {tr : (spec w).Transcript} (hM : M.inv s) (hv : D.valid (M.abs s) o) + (hrun : (spec w).ClientRun s (M.methods o) a s' tr) {r : ℕ} + (hroom : Fits w (s.frontier + (M.room s + r))) : Fits w (s'.frontier + (M.room s' + r)) := + hroom.of_le (by obtain ⟨-, -, h⟩ := M.modifies o hM hv hrun; omega) + /-- **The frame of a framed refinement.** A method run of `M` leaves a framed refinement with a -disjoint footprint unchanged, and the footprints stay disjoint. -/ +disjoint footprint unchanged, and the footprints disjoint and the shared room budget fitting. -/ theorem frame {s : State} (o : D.Op) {a : D.Result o} {s' : State} {tr : (spec w).Transcript} (hM : M.inv s) (hN : N.inv s) - (hd : Disjoint (M.footprint s) (N.footprint s)) (hv : D.valid (M.abs s) o) + (hd : Disjoint (M.footprint s) (N.footprint s)) + (hroom : Fits w (s.frontier + (M.room s + N.room s))) (hv : D.valid (M.abs s) o) (hrun : (spec w).ClientRun s (M.methods o) a s' tr) : - N.abs s' = N.abs s ∧ N.inv s' ∧ N.footprint s' = N.footprint s ∧ - Disjoint (M.footprint s') (N.footprint s') := by + N.abs s' = N.abs s ∧ N.inv s' ∧ N.footprint s' = N.footprint s ∧ N.room s' = N.room s ∧ + Disjoint (M.footprint s') (N.footprint s') ∧ + Fits w (s'.frontier + (M.room s' + N.room s')) := by have hf := frontier_le_of_clientRun hrun - obtain ⟨hN', habs, hfp⟩ := N.congr hN (agree_of_run N o hM hN hd hv hrun) hf - refine ⟨habs, hN', hfp, Finset.disjoint_left.mpr fun b hb hb' => ?_⟩ + have hroom' := room_of_run o hM hv hrun hroom + obtain ⟨hN', habs, hfp, hr⟩ := N.congr hN (agree_of_run N o hM hN hd hv hrun) hf + (hroom'.of_le (by omega)) + refine ⟨habs, hN', hfp, hr, Finset.disjoint_left.mpr fun b hb hb' => ?_, hr ▸ hroom'⟩ rw [hfp] at hb' - rcases (M.modifies o hM hv hrun).2 b hb with hb | hb + obtain ⟨-, hgrow, -⟩ := M.modifies o hM hv hrun + rcases hgrow b hb with hb | hb · exact Finset.disjoint_left.mp hd hb hb' · exact absurd (N.lt_frontier hN hb') (Nat.not_lt.mpr hb) /-- A method run of `M` leaves unchanged every function that reads only a disjoint footprint. -/ theorem Local.frame {β : Type*} {f : State → β} (hf : N.Local f) {s : State} (o : D.Op) {a : D.Result o} {s' : State} {tr : (spec w).Transcript} (hM : M.inv s) (hN : N.inv s) - (hd : Disjoint (M.footprint s) (N.footprint s)) (hv : D.valid (M.abs s) o) + (hd : Disjoint (M.footprint s) (N.footprint s)) + (hroom : Fits w (s.frontier + (M.room s + N.room s))) (hv : D.valid (M.abs s) o) (hrun : (spec w).ClientRun s (M.methods o) a s' tr) : f s' = f s := hf hN (agree_of_run N o hM hN hd hv hrun) (frontier_le_of_clientRun hrun) + ((room_of_run o hM hv hrun hroom).of_le (by omega)) /-! ## Products -/ variable (M₁ : Framed D₁ w) (M₂ : Framed D₂ w) -/-- A method run of the left side keeps the right side and the disjointness of the footprints. -/ +/-- What keeps two framed refinements apart in one memory: disjoint footprints, and room for +both. -/ +def Apart (s : State) : Prop := + Disjoint (M₁.footprint s) (M₂.footprint s) ∧ Fits w (s.frontier + (M₁.room s + M₂.room s)) + +/-- A method run of the left side keeps the right side and keeps the two apart. -/ theorem prod_frame_left {s : State} (o : D₁.Op) {a : D₁.Result o} {s' : State} - {tr : (spec w).Transcript} (h₁ : M₁.inv s) (h₂ : M₂.inv s) - (hd : Disjoint (M₁.footprint s) (M₂.footprint s)) (hv : D₁.valid (M₁.abs s) o) - (hrun : (spec w).ClientRun s (M₁.methods o) a s' tr) : - M₂.abs s' = M₂.abs s ∧ M₂.inv s' ∧ Disjoint (M₁.footprint s') (M₂.footprint s') := - have h := frame M₂ o h₁ h₂ hd hv hrun - ⟨h.1, h.2.1, h.2.2.2⟩ - -/-- A method run of the right side keeps the left side and the disjointness of the footprints. -/ + {tr : (spec w).Transcript} (h₁ : M₁.inv s) (h₂ : M₂.inv s) (hJ : Apart M₁ M₂ s) + (hv : D₁.valid (M₁.abs s) o) (hrun : (spec w).ClientRun s (M₁.methods o) a s' tr) : + M₂.abs s' = M₂.abs s ∧ M₂.inv s' ∧ Apart M₁ M₂ s' := by + obtain ⟨habs, hinv, -, -, hapart⟩ := frame M₂ o h₁ h₂ hJ.1 hJ.2 hv hrun + exact ⟨habs, hinv, hapart⟩ + +/-- A method run of the right side keeps the left side and keeps the two apart. -/ theorem prod_frame_right {s : State} (o : D₂.Op) {a : D₂.Result o} {s' : State} - {tr : (spec w).Transcript} (h₁ : M₁.inv s) (h₂ : M₂.inv s) - (hd : Disjoint (M₁.footprint s) (M₂.footprint s)) (hv : D₂.valid (M₂.abs s) o) - (hrun : (spec w).ClientRun s (M₂.methods o) a s' tr) : - M₁.abs s' = M₁.abs s ∧ M₁.inv s' ∧ Disjoint (M₁.footprint s') (M₂.footprint s') := - have h := frame M₁ o h₂ h₁ hd.symm hv hrun - ⟨h.1, h.2.1, h.2.2.2.symm⟩ + {tr : (spec w).Transcript} (h₁ : M₁.inv s) (h₂ : M₂.inv s) (hJ : Apart M₁ M₂ s) + (hv : D₂.valid (M₂.abs s) o) (hrun : (spec w).ClientRun s (M₂.methods o) a s' tr) : + M₁.abs s' = M₁.abs s ∧ M₁.inv s' ∧ Apart M₁ M₂ s' := by + obtain ⟨habs, hinv, -, -, hd, hroom⟩ := + frame M₁ o h₂ h₁ hJ.1.symm (by rw [Nat.add_comm (M₂.room s)]; exact hJ.2) hv hrun + exact ⟨habs, hinv, hd.symm, by rw [Nat.add_comm (M₁.room s')]; exact hroom⟩ /-- **Products over one memory.** Two framed refinements implement the product of their -specifications over one memory, with the union of their footprints, while the footprints are -disjoint. Disjointness is part of the product's invariant. -/ +specifications over one memory, with the union of their footprints and the sum of their rooms, +while they are apart: their footprints are disjoint and the frontier plus both rooms fits a word. +Being apart is part of the product's invariant. -/ def prod : Framed (product D₁ D₂) w where - toRefinement := M₁.toRefinement.prod M₂.toRefinement - (fun s => Disjoint (M₁.footprint s) (M₂.footprint s)) + toRefinement := M₁.toRefinement.prod M₂.toRefinement (Apart M₁ M₂) (prod_frame_left M₁ M₂) (prod_frame_right M₁ M₂) footprint s := M₁.footprint s ∪ M₂.footprint s + room s := M₁.room s + M₂.room s lt_frontier {_ _} hs ha := by + obtain ⟨h₁, h₂, -⟩ := hs rcases Finset.mem_union.mp ha with ha | ha - · exact M₁.lt_frontier hs.1 ha - · exact M₂.lt_frontier hs.2.1 ha - congr {_ _} hs h hf := by - obtain ⟨h₁, h₂, hd⟩ := hs - obtain ⟨h₁', habs₁, hfp₁⟩ := M₁.congr h₁ (fun a ha => h a (Finset.mem_union_left _ ha)) hf - obtain ⟨h₂', habs₂, hfp₂⟩ := M₂.congr h₂ (fun a ha => h a (Finset.mem_union_right _ ha)) hf - refine ⟨⟨h₁', h₂', ?_⟩, ?_, ?_⟩ - · change Disjoint (M₁.footprint _) (M₂.footprint _) - rwa [hfp₁, hfp₂] + · exact M₁.lt_frontier h₁ ha + · exact M₂.lt_frontier h₂ ha + congr {_ _} hs h hf hr := by + obtain ⟨h₁, h₂, hd, -⟩ := hs + obtain ⟨h₁', habs₁, hfp₁, hr₁⟩ := M₁.congr h₁ + (fun a ha => h a (Finset.mem_union_left _ ha)) hf (hr.of_le (by omega)) + obtain ⟨h₂', habs₂, hfp₂, hr₂⟩ := M₂.congr h₂ + (fun a ha => h a (Finset.mem_union_right _ ha)) hf (hr.of_le (by omega)) + refine ⟨⟨h₁', h₂', ?_, ?_⟩, ?_, ?_, ?_⟩ + · rwa [hfp₁, hfp₂] + · rwa [hr₁, hr₂] · change (M₁.abs _, M₂.abs _) = (M₁.abs _, M₂.abs _) rw [habs₁, habs₂] · rw [hfp₁, hfp₂] + · rw [hr₁, hr₂] modifies {s} o {a s' tr} hs hv hrun := by - obtain ⟨h₁, h₂, hd⟩ := hs + obtain ⟨h₁, h₂, hd, hroom⟩ := hs cases o with | inl o => - obtain ⟨hmod, hgrow⟩ := M₁.modifies o h₁ hv hrun - obtain ⟨-, -, hfp₂, -⟩ := frame M₂ o h₁ h₂ hd hv hrun + obtain ⟨hmod, hgrow, hr⟩ := M₁.modifies o h₁ hv hrun + obtain ⟨-, -, hfp₂, hr₂, -⟩ := frame M₂ o h₁ h₂ hd hroom hv hrun refine ⟨fun b hb hb' => hmod b hb fun h => hb' (Finset.mem_union_left _ h), - fun b hb => ?_⟩ - change b ∈ M₁.footprint s' ∪ M₂.footprint s' at hb - rw [hfp₂] at hb - rcases Finset.mem_union.mp hb with hb | hb - · exact (hgrow b hb).imp_left (Finset.mem_union_left _) - · exact .inl (Finset.mem_union_right _ hb) + fun b hb => ?_, ?_⟩ + · change b ∈ M₁.footprint s' ∪ M₂.footprint s' at hb + rw [hfp₂] at hb + rcases Finset.mem_union.mp hb with hb | hb + · exact (hgrow b hb).imp_left (Finset.mem_union_left _) + · exact .inl (Finset.mem_union_right _ hb) + · omega | inr o => - obtain ⟨hmod, hgrow⟩ := M₂.modifies o h₂ hv hrun - obtain ⟨-, -, hfp₁, -⟩ := frame M₁ o h₂ h₁ hd.symm hv hrun + obtain ⟨hmod, hgrow, hr⟩ := M₂.modifies o h₂ hv hrun + obtain ⟨-, -, hfp₁, hr₁, -⟩ := frame M₁ o h₂ h₁ hd.symm + (by rw [Nat.add_comm (M₂.room s)]; exact hroom) hv hrun refine ⟨fun b hb hb' => hmod b hb fun h => hb' (Finset.mem_union_right _ h), - fun b hb => ?_⟩ - change b ∈ M₁.footprint s' ∪ M₂.footprint s' at hb - rw [hfp₁] at hb - rcases Finset.mem_union.mp hb with hb | hb - · exact .inl (Finset.mem_union_left _ hb) - · exact (hgrow b hb).imp_left (Finset.mem_union_right _) + fun b hb => ?_, ?_⟩ + · change b ∈ M₁.footprint s' ∪ M₂.footprint s' at hb + rw [hfp₁] at hb + rcases Finset.mem_union.mp hb with hb | hb + · exact .inl (Finset.mem_union_left _ hb) + · exact (hgrow b hb).imp_left (Finset.mem_union_right _) + · omega /-- The product's methods run each side's methods. They do not depend on the proofs. -/ theorem prod_methods : (M₁.prod M₂).methods = Methods.prod M₁.methods M₂.methods := rfl @@ -283,26 +438,30 @@ theorem prod_methods : (M₁.prod M₂).methods = Methods.prod M₁.methods M₂ /-- The product represents each side by its own abstraction. -/ @[simp] theorem prod_abs (s : State) : (M₁.prod M₂).abs s = (M₁.abs s, M₂.abs s) := rfl -/-- The product's invariant: both sides' invariants, and disjoint footprints. -/ +/-- The product's invariant: both sides' invariants, and the two sides apart. -/ theorem prod_inv_iff (s : State) : - (M₁.prod M₂).inv s ↔ - M₁.inv s ∧ M₂.inv s ∧ Disjoint (M₁.footprint s) (M₂.footprint s) := Iff.rfl + (M₁.prod M₂).inv s ↔ M₁.inv s ∧ M₂.inv s ∧ Apart M₁ M₂ s := Iff.rfl /-- The product's footprint is the union of the footprints. -/ @[simp] theorem prod_footprint (s : State) : (M₁.prod M₂).footprint s = M₁.footprint s ∪ M₂.footprint s := rfl +/-- The product's room is the sum of the rooms. -/ +@[simp] theorem prod_room (s : State) : (M₁.prod M₂).room s = M₁.room s + M₂.room s := rfl + variable {M₁ M₂} /-- A function local to the left side is local to the product. -/ theorem Local.prod_left {β : Type*} {f : State → β} (hf : M₁.Local f) : - (M₁.prod M₂).Local f := fun hs h hfr => - hf hs.1 (fun a ha => h a (Finset.mem_union_left _ ha)) hfr + (M₁.prod M₂).Local f := fun ⟨h₁, _, _⟩ h hfr hr => + hf h₁ (fun a ha => h a (Finset.mem_union_left _ ha)) hfr + (hr.of_le (Nat.add_le_add_left (Nat.le_add_right _ _) _)) /-- A function local to the right side is local to the product. -/ theorem Local.prod_right {β : Type*} {f : State → β} (hf : M₂.Local f) : - (M₁.prod M₂).Local f := fun hs h hfr => - hf hs.2.1 (fun a ha => h a (Finset.mem_union_right _ ha)) hfr + (M₁.prod M₂).Local f := fun ⟨_, h₂, _⟩ h hfr hr => + hf h₂ (fun a ha => h a (Finset.mem_union_right _ ha)) hfr + (hr.of_le (Nat.add_le_add_left (Nat.le_add_left _ _) _)) /-- **Potentials add across a product over one memory.** Certificates of the two sides against one lower charge, with potentials that read only their own footprints, give a certificate of @@ -313,19 +472,24 @@ theorem amortized_prod {lower : State → Op → ℕ} {upper₁ : D₁.State → (hΦ₁ : M₁.Local Φ₁) (hΦ₂ : M₂.Local Φ₂) : (M₁.prod M₂).Amortized lower (productCharge upper₁ upper₂) fun s => Φ₁ s + Φ₂ s := Refinement.Amortized.prod (frame₁ := prod_frame_left M₁ M₂) (frame₂ := prod_frame_right M₁ M₂) - A₁ A₂ (fun {_} o {_ _ _} h₁ h₂ hd hv hrun => hΦ₂.frame M₂ o h₁ h₂ hd hv hrun) - (fun {_} o {_ _ _} h₁ h₂ hd hv hrun => hΦ₁.frame M₁ o h₂ h₁ hd.symm hv hrun) + A₁ A₂ (fun {_} o {_ _ _} h₁ h₂ hJ hv hrun => hΦ₂.frame M₂ o h₁ h₂ hJ.1 hJ.2 hv hrun) + (fun {_} o {_ _ _} h₁ h₂ hJ hv hrun => hΦ₁.frame M₁ o h₂ h₁ hJ.1.symm + (by rw [Nat.add_comm (M₂.room _)]; exact hJ.2) hv hrun) /-- Setting up a product from structures allocated one after the other: `M₁` holds at `s`; a -run from `s` to `s'` leaves the cells below `s.frontier` unchanged; and `M₂` holds at `s'` on a -footprint at or above `s.frontier`. Then the product holds at `s'`, with `M₁`'s abstraction -unchanged. -/ +run from `s` to `s'` leaves the cells below `s.frontier` unchanged; `M₂` holds at `s'` on a +footprint at or above `s.frontier`; and the frontier plus both rooms fits. Then the product holds +at `s'`, with `M₁`'s abstraction, footprint and room unchanged. -/ theorem prod_of_fresh {s s' : State} (h₁ : M₁.inv s) (hmem : ∀ a < s.frontier, s'.mem a = s.mem a) (hf : s.frontier ≤ s'.frontier) - (h₂ : M₂.inv s') (hfp : ∀ a ∈ M₂.footprint s', s.frontier ≤ a) : - (M₁.prod M₂).inv s' ∧ (M₁.prod M₂).abs s' = (M₁.abs s, M₂.abs s') := by - obtain ⟨h₁', habs, hfp₁⟩ := M₁.of_fresh h₁ hmem hf - refine ⟨⟨h₁', h₂, Finset.disjoint_left.mpr fun a ha ha' => ?_⟩, by rw [prod_abs, habs]⟩ + (h₂ : M₂.inv s') (hfp : ∀ a ∈ M₂.footprint s', s.frontier ≤ a) + (hroom : Fits w (s'.frontier + (M₁.room s + M₂.room s'))) : + (M₁.prod M₂).inv s' ∧ (M₁.prod M₂).abs s' = (M₁.abs s, M₂.abs s') ∧ + (M₁.prod M₂).footprint s' = M₁.footprint s ∪ M₂.footprint s' ∧ + (M₁.prod M₂).room s' = M₁.room s + M₂.room s' := by + obtain ⟨h₁', habs, hfp₁, hr₁⟩ := M₁.of_fresh h₁ hmem hf (hroom.of_le (by omega)) + refine ⟨⟨h₁', h₂, Finset.disjoint_left.mpr fun a ha ha' => ?_, by rwa [hr₁]⟩, + by rw [prod_abs, habs], by rw [prod_footprint, hfp₁], by rw [prod_room, hr₁]⟩ rw [hfp₁] at ha exact absurd (M₁.lt_frontier h₁ ha) (Nat.not_lt.mpr (hfp a ha')) diff --git a/Complexity/DataStructure/Queue/Memory.lean b/Complexity/DataStructure/Queue/Memory.lean index 89acafde8b..5b53a71444 100644 --- a/Complexity/DataStructure/Queue/Memory.lean +++ b/Complexity/DataStructure/Queue/Memory.lean @@ -22,16 +22,18 @@ proved once: (`Queue.potential_step`); only the capacity and word-fit admissibility of the bounded stacks is new. Each queue operation costs at most three stack calls, amortized, with two credits per rear element. -2. `pair`: two array stacks with disjoint layouts in one memory, as the generic product - `Memory.Framed.prod` of two framed stacks, each stack call costing at most three memory +2. Two array stacks allocated one after the other, as the generic product + `Memory.Allocator.prod` of two stack allocators, each stack call costing at most three memory units. -`memRefinement` composes them, and `mem_amortized` stacks the potentials across the composition -(`Refinement.Amortized.comp`): every queue operation costs at most nine memory units, amortized, -with six credits per rear element. `run` allocates the two stacks and then runs a queue client. -From any memory with room for both stacks, `run_correct` gives its answer and the admissibility of -its memory operations, `run_cost` bounds its memory cost by `6 + 9 m` for `m` queue operations, -and `run_cost_isBigO` exports that bound as `O(m)`. +`allocator` puts the first step on top of the second (`Memory.Allocator.comp`), and +`amortized_allocator` stacks the potentials across the composition: every queue operation costs +at most nine memory units, amortized, with six credits per rear element. `run` allocates the two +stacks and then runs a queue client. From any memory with room for both stacks, `run_correct` +gives its answer and the admissibility of its memory operations, `run_cost` bounds its memory +cost by `6 + 9 m` for `m` queue operations, and `run_cost_isBigO` exports that bound as `O(m)`. +All three are instances of the generic theorems of `Memory.Allocator`, so this file has no setup +proof of its own. The machine-level counterpart is `Queue.WordRAM`, whose dispatcher is hand-stepped instruction by instruction. Here the proofs mention neither registers nor instructions. Executing the memory @@ -43,16 +45,15 @@ provide. * `boundedStacks`: the pair of bounded stacks. * `Inv`: a FIFO list within capacity holding words. * `refinement`: the two-stack queue over the pair of bounded stacks. -* `pair`: the two stacks in one memory. -* `memRefinement`, `memMethods`: the queue over two array stacks over memory, and its methods as - plain memory programs. +* `allocator`: the queue over two array stacks, allocated in memory. * `run`: allocate the stacks, then run a queue client. ## Main results * `runClient_boundedStacks`: bounded stacks run clients exactly as unbounded ones. * `amortized`: three stack calls per queue operation, with two credits per rear element. -* `mem_amortized`: nine memory units per queue operation, with six credits per rear element. +* `amortized_allocator`: nine memory units per queue operation, with six credits per rear + element. * `run_correct`, `run_cost`: the allocated queue answers like the specification and costs at most `6 + 9 m` memory units for `m` queue operations. * `run_cost_glue`: with glue charged per memory operation, at most `(2 K + 1) (6 + 9 m)`. @@ -191,100 +192,32 @@ theorem amortized : /-! ## Over memory -/ -/-- The two stacks over one memory, in disjoint layouts. -/ -abbrev pair {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) : - Memory.Framed (boundedStacks w C) w := - (Stack.Memory.framed h₁).prod (Stack.Memory.framed h₂) +/-- The queue over two array stacks of capacity `C`: allocate the stacks one after the other, +and run the two-stack queue on their product. -/ +def allocator (w C : ℕ) : Allocator (Queue.WordRAM.spec w C) w := + ((Stack.Memory.allocator w C).prod (Stack.Memory.allocator w C)).comp (refinement w C) + (show Inv w C [] from ⟨Nat.zero_le C, fun _ h => absurd h List.not_mem_nil⟩) -/-- The queue over two array stacks over memory. -/ -def memRefinement {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) : - Refinement (Queue.WordRAM.spec w C) (Memory.spec w) := - (refinement w C).comp (pair h₁ h₂).toRefinement +/-- The allocated queue is empty. -/ +@[simp] theorem init_allocator : (allocator w C).init = [] := rfl + +/-- The two stacks take `2 (C + 1)` cells. -/ +@[simp] theorem room_allocator : (allocator w C).room = 2 * (C + 1) := by + change (C + 1) + (C + 1) = _ + omega /-- **Stacked potentials.** Every queue operation costs at most nine memory units, amortized, with six credits per element of the rear stack: the queue's potential, at three memory units per -stack call, read through the stacks' abstraction. -/ -theorem mem_amortized {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) (h₂ : L₂.Valid w C) : - (memRefinement h₁ h₂).Amortized charge (fun _ _ => 9) - fun s => 6 * (Stack.Memory.abs L₂ s).length := by - have hpair := Memory.Framed.amortized_prod (Stack.Memory.amortized h₁) - (Stack.Memory.amortized h₂) (Memory.Framed.Local.const _ 0) (Memory.Framed.Local.const _ 0) +stack call, read through the stacks' abstraction. Allocation leaves no potential. -/ +theorem amortized_allocator : (allocator w C).Amortized (fun _ _ => 9) 0 := by + have hpair := (Stack.Memory.amortized_allocator (w := w) (C := C)).prod + (Stack.Memory.amortized_allocator (w := w) (C := C)) rw [productCharge_const] at hpair - have h := (amortized.mul_left 3).comp hpair - have hΦ : (fun s : Memory.State => - 3 * (2 * (Stack.Memory.abs L₂ s).length) + (0 + 0)) = - fun s => 6 * (Stack.Memory.abs L₂ s).length := by - funext s - omega - exact hΦ ▸ h - -/-! ## Allocation and a whole run -/ - -/-- The queue methods over two array stacks, as plain memory programs. -/ -def memMethods (L₁ L₂ : Stack.Memory.Layout) (w C : ℕ) : - Methods (Queue.WordRAM.spec w C) (Memory.spec w) := - fun o => link (Methods.prod (Stack.Memory.methods L₁ w C) (Stack.Memory.methods L₂ w C)) - (methods o) - -/-- The composite's methods are `memMethods`, whatever the proofs about the layouts. -/ -theorem memRefinement_methods {L₁ L₂ : Stack.Memory.Layout} (h₁ : L₁.Valid w C) - (h₂ : L₂.Valid w C) : (memRefinement h₁ h₂).methods = memMethods L₁ L₂ w C := rfl + exact Allocator.Amortized.comp (amortized.mul_left 3) hpair /-- Allocate two empty stacks of capacity `C`, then run a queue client on them. -/ -def run (w C : ℕ) {β : Type} (p : (Queue.WordRAM.spec w C).Client β) : Prog β := do - let L₁ ← Stack.Memory.allocLayout C - let L₂ ← Stack.Memory.allocLayout C - link (memMethods L₁ L₂ w C) p - -/-- The first stack allocated from `s`. -/ -abbrev frontLayout (s : Memory.State) (C : ℕ) : Stack.Memory.Layout := - ⟨⟨s.frontier, 1⟩, ⟨s.frontier + 1, C⟩⟩ - -/-- The second stack allocated from `s`. -/ -abbrev rearLayout (s : Memory.State) (C : ℕ) : Stack.Memory.Layout := - ⟨⟨s.frontier + 1 + C, 1⟩, ⟨s.frontier + 1 + C + 1, C⟩⟩ - -/-- The memory after both allocations. -/ -abbrev allocated (s : Memory.State) (C : ℕ) : Memory.State := - ⟨clear (clear s.mem s.frontier 1) (s.frontier + 1 + C) 1, s.frontier + 1 + C + 1 + C⟩ - -/-- A run allocates both stacks, then runs the linked client from the allocated memory. -/ -theorem runClient_run {β : Type} (p : (Queue.WordRAM.spec w C).Client β) (s : Memory.State) : - (Memory.spec w).runClient s (run w C p) = - let (v, s', os) := (Memory.spec w).runClient (allocated s C) - (link (memMethods (frontLayout s C) (rearLayout s C) w C) p) - (v, s', .alloc 1 :: .allocDirty C :: .alloc 1 :: .allocDirty C :: os) := rfl - -/-- The facts that start the refinement after allocation. -/ -private theorem setup (s : Memory.State) (hfit : Fits w (s.frontier + 2 * (C + 1))) : - ∃ (h₁ : (frontLayout s C).Valid w C) (h₂ : (rearLayout s C).Valid w C), - (memRefinement h₁ h₂).inv (allocated s C) ∧ - (memRefinement h₁ h₂).abs (allocated s C) = [] ∧ - Stack.Memory.abs (rearLayout s C) (allocated s C) = [] := by - have h₁ : (frontLayout s C).Valid w C := Stack.Memory.valid_allocLayout (hfit.of_le (by omega)) - have h₂ : (rearLayout s C).Valid w C := Stack.Memory.valid_allocLayout (hfit.of_le (by omega)) - -- Each stack is empty once allocated, the second one from the state after the first. - obtain ⟨hi₁, ha₁⟩ := Stack.Memory.inv_allocLayout (C := C) s - obtain ⟨hi₂, ha₂⟩ := Stack.Memory.inv_allocLayout (C := C) - ⟨clear s.mem s.frontier 1, s.frontier + 1 + C⟩ - -- The second allocation is fresh, so the stacks form a product. - obtain ⟨hinv, habs⟩ := Memory.Framed.prod_of_fresh (M₁ := Stack.Memory.framed h₁) - (M₂ := Stack.Memory.framed h₂) hi₁ - (fun a ha => clear_of_not_owns (by simp only [Block.Owns] at ha ⊢; omega)) (by simp only; omega) - hi₂ - (fun a ha => by - simp only [Stack.Memory.framed_footprint, Stack.Memory.Layout.footprint, Finset.mem_union, - Block.mem_footprint, Block.Owns] at ha - simp only - omega) - have hempty : (pair h₁ h₂).abs (allocated s C) = ([], []) := habs.trans (Prod.ext ha₁ ha₂) - refine ⟨h₁, h₂, ⟨hinv, ?_⟩, ?_, ha₂⟩ - · change Inv w C (Queue.WordRAM.contents ((pair h₁ h₂).abs (allocated s C))) - rw [hempty] - exact ⟨by simp [Queue.WordRAM.contents], by simp [Queue.WordRAM.contents]⟩ - · change Queue.WordRAM.contents ((pair h₁ h₂).abs (allocated s C)) = [] - rw [hempty] - rfl +def run (w C : ℕ) {β : Type} (p : (Queue.WordRAM.spec w C).Client β) : Prog β := + (allocator w C).run p /-- **Correctness.** From any memory with room for both stacks, an admissible queue client run over two freshly allocated array stacks issues only admissible memory operations and returns the @@ -293,17 +226,9 @@ theorem run_correct {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Me (hfit : Fits w (s.frontier + 2 * (C + 1))) (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : (Memory.spec w).ValidHistory s ((Memory.spec w).clientHistory s (run w C p)) ∧ - (Memory.spec w).clientResult s (run w C p) = (Queue.WordRAM.spec w C).clientResult [] p := by - obtain ⟨h₁, h₂, hinv, habs, -⟩ := setup s hfit - obtain ⟨hvE, hres, -, -⟩ := - (memRefinement h₁ h₂).simulates_client p hinv (by rw [habs]; exact hv) - rw [memRefinement_methods] at hvE hres - simp only [StateTransitionSpec.clientHistory, StateTransitionSpec.clientResult, - runClient_run] at hvE hres ⊢ - simp only [StateTransitionSpec.validHistory_cons, StateTransitionSpec.stepFn, step_alloc, - step_allocDirty, valid_alloc, valid_allocDirty] - refine ⟨⟨hfit.of_le (by omega), hfit.of_le (by omega), hfit.of_le (by omega), - hfit.of_le (by omega), hvE⟩, hres.trans (by rw [habs])⟩ + (Memory.spec w).clientResult s (run w C p) = (Queue.WordRAM.spec w C).clientResult [] p := + have ⟨hvE, hres, _⟩ := (allocator w C).run_correct p ⟨trivial, trivial⟩ (room_allocator ▸ hfit) hv + ⟨hvE, hres⟩ /-- **Cost.** The same run costs at most `6 + 9 m` memory units for a client of `m` queue operations: six for the two allocations and nine per operation, amortized. -/ @@ -311,15 +236,8 @@ theorem run_cost {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Memor (hfit : Fits w (s.frontier + 2 * (C + 1))) (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : (Memory.spec w).totalCost charge s ((Memory.spec w).clientHistory s (run w C p)) ≤ - 6 + 9 * ((Queue.WordRAM.spec w C).clientHistory [] p).length := by - obtain ⟨h₁, h₂, hinv, habs, hrear⟩ := setup s hfit - have h := (mem_amortized h₁ h₂).client p hinv (by rw [habs]; exact hv) - rw [memRefinement_methods, habs, StateTransitionSpec.totalCost_const, hrear] at h - simp only [StateTransitionSpec.clientHistory, runClient_run, - StateTransitionSpec.totalCost_cons, charge_alloc, charge_allocDirty, - StateTransitionSpec.stepFn, step_alloc, step_allocDirty] at h ⊢ - simp only [List.length_nil, Nat.mul_zero, Nat.add_zero, allocated] at h ⊢ - omega + 6 + 9 * ((Queue.WordRAM.spec w C).clientHistory [] p).length := + Allocator.run_cost_le amortized_allocator p ⟨trivial, trivial⟩ (room_allocator ▸ hfit) hv /-- **Cost with glue.** A realization also charges the glue that runs after each memory operation. If that glue costs at most `K` times the operation's charge plus `K`, the run costs at most @@ -345,6 +263,6 @@ theorem run_cost_isBigO (s : Memory.State) (hfit : Fits w (s.frontier + 2 * (C + ((Queue.WordRAM.spec w C).clientHistory [] p.1).length) (fun p => (Memory.spec w).totalCost charge s ((Memory.spec w).clientHistory s (run w C p.1))) := - CostIsBigO.of_le_add_mul 6 9 fun p => run_cost p.1 hfit p.2 + Allocator.run_cost_isBigO amortized_allocator ⟨trivial, trivial⟩ (room_allocator ▸ hfit) end Cslib.DataStructure.Queue.Memory diff --git a/Complexity/DataStructure/Stack/Memory.lean b/Complexity/DataStructure/Stack/Memory.lean index 5fb09e09e6..647b390338 100644 --- a/Complexity/DataStructure/Stack/Memory.lean +++ b/Complexity/DataStructure/Stack/Memory.lean @@ -8,7 +8,7 @@ module -- upstream: candidate public import Complexity.DataStructure.Memory.Field -public import Complexity.DataStructure.Memory.Frame +public import Complexity.DataStructure.Memory.Alloc public import Complexity.DataStructure.Stack.WordRAM.Basic /-! @@ -25,7 +25,8 @@ Every operation costs at most three memory units, with no potential (`amortized` write only the layout's cells and never allocate (`modifies_methods`), and the abstraction and invariant depend only on those cells (`abs_congr`, `Inv.congr`). So the stack is framed by its layout (`framed`), and stacks with disjoint layouts share one memory through the generic product -`Memory.Framed.prod`. +`Memory.Framed.prod`. `allocator` packages the allocation of an empty stack (`allocLayout`) with +the framed stack it sets up, so that clients allocate stacks through `Memory.Allocator`. This is the same specification that `Stack.WordRAM` implements by a hand-stepped nine-register dispatcher. Here no register or instruction count appears. Word fit enters only through the @@ -40,6 +41,7 @@ admissibility of `Memory.spec w` and one static fact of the layout, that the cap * `refinement`: the array stack as a `Refinement` of the bounded stack. * `framed`: the same refinement, framed by the layout's cells. * `allocLayout`: allocate an empty stack. +* `allocator`: allocation and the framed stack, as a `Memory.Allocator`. ## Main results @@ -47,8 +49,9 @@ admissibility of `Memory.spec w` and one static fact of the layout, that the cap * `runClient_push`, `runClient_pop_of_eq_zero`, `runClient_pop_of_pos`, `runClient_size`: the operations evaluate definitionally. * `modifies_methods`, `abs_congr`, `Inv.congr`, `Inv.lt_frontier`: the frame of a stack. -* `runClient_allocLayout_bind`, `valid_allocLayout`, `inv_allocLayout`: allocation yields a - well-formed empty stack in three memory units. +* `runClient_allocLayout_bind`, `runClient_allocLayout`, `valid_allocLayout`, + `inv_allocLayout`: allocation yields a well-formed empty stack in three memory units. +* `amortized_allocator`: the allocator's cost certificate, three units per operation. ## References @@ -318,6 +321,7 @@ def framed (hL : L.Valid w C) : Memory.Framed (Stack.WordRAM.spec w C) w := .ofStatic (refinement hL) L.footprint (fun hs ha => Inv.lt_frontier hs ha) (fun hs h hf => ⟨hs.congr hL h hf, abs_congr hL hs h⟩) (fun o _ hv b _ hb => (modifies_methods hL o hv).1 b hb) + (fun o _ hv => (modifies_methods hL o hv).2) /-- Every operation costs at most three memory units, so the stack needs no potential. -/ theorem amortized (hL : L.Valid w C) : @@ -385,4 +389,50 @@ theorem inv_allocLayout (s : Memory.State) : exact Nat.zero_le C · simp [abs, h0] +/-- Allocation returns the layout at the frontier, clears the count cell, and advances the +frontier past the items. -/ +theorem runClient_allocLayout (C : ℕ) (s : Memory.State) : + (Memory.spec w).runClient s (allocLayout C) = + (⟨⟨s.frontier, 1⟩, ⟨s.frontier + 1, C⟩⟩, ⟨clear s.mem s.frontier 1, s.frontier + 1 + C⟩, + [.alloc 1, .allocDirty C]) := rfl + +/-- **The array stack as an allocator**: allocate an empty stack of capacity `C` in `C + 1` +cells, at a memory cost of three units. -/ +def allocator (w C : ℕ) : Allocator (Stack.WordRAM.spec w C) w where + Handle := Layout + alloc := allocLayout C + methods L := methods L w C + init := [] + room := C + 1 + cost := 3 + framed s _ hroom := framed (valid_allocLayout (f := s.frontier) (hroom.of_le (by omega))) + allocates s _ hroom := by + obtain ⟨hinv, habs⟩ := inv_allocLayout (C := C) s + refine + { validHistory := ?_, cost_le := ?_, mem_eq := ?_, methods_eq := rfl, inv := ?_, + abs_eq := ?_, le_footprint := ?_, room_le := ?_ } <;> + simp only [StateTransitionSpec.clientHistory, StateTransitionSpec.clientState, + runClient_allocLayout] + · simp only [StateTransitionSpec.validHistory_cons, StateTransitionSpec.stepFn, step_alloc, + valid_alloc, valid_allocDirty] + exact ⟨hroom.of_le (by omega), hroom.of_le (by omega), trivial⟩ + · simp + · exact fun a ha => clear_of_not_owns (by simp only [Block.Owns]; omega) + · exact hinv + · exact habs + · intro a ha + simp only [framed_footprint, Layout.footprint, Finset.mem_union, Block.mem_footprint, + Block.Owns] at ha + omega + · simp only [framed, Memory.Framed.ofStatic] + omega + +/-- The allocated stack is empty. -/ +@[simp] theorem init_allocator (w C : ℕ) : (allocator w C).init = [] := rfl + +/-- Every stack operation costs at most three memory units, and allocation leaves no +potential. -/ +theorem amortized_allocator : (allocator w C).Amortized (fun _ _ => 3) 0 := + fun _ _ _ => ⟨fun _ => 0, amortized _, Memory.Framed.Local.const _ 0, le_rfl⟩ + end Cslib.DataStructure.Stack.Memory diff --git a/Complexity/DataStructure/StateTransition.lean b/Complexity/DataStructure/StateTransition.lean index 623ad75564..032f4dac9f 100644 --- a/Complexity/DataStructure/StateTransition.lean +++ b/Complexity/DataStructure/StateTransition.lean @@ -71,8 +71,10 @@ The relational semantics is **demonic** for callers and **angelic** for implemen under `[StateTransitionSpec.HasStepFn]`. * `StateTransitionSpec.fst_run_eq_toFLTS_mtr`, `StateTransitionSpec.run_append`, `StateTransitionSpec.validHistory_append`, `StateTransitionSpec.totalCost_append`, - `StateTransitionSpec.run_runClient`, `StateTransitionSpec.runClient_bind`, `eval_bind`, - `eval_eq_liftM`: functional history and client laws under `[StateTransitionSpec.HasStepFn]`. + `StateTransitionSpec.run_runClient`, `StateTransitionSpec.runClient_bind`, + `StateTransitionSpec.clientResult_bind`, `StateTransitionSpec.clientState_bind`, + `StateTransitionSpec.clientHistory_bind`, `eval_bind`, `eval_eq_liftM`: functional history and + client laws under `[StateTransitionSpec.HasStepFn]`. ## References @@ -870,6 +872,26 @@ theorem runClient_bind (p : D.Client α) (k : α → D.Client β) (s : D.State) exact congrArg (fun r : β × D.State × List D.Op => (r.1, r.2.1, o :: r.2.2)) (ih (D.stepFn s o).2 (D.stepFn s o).1) +/-- A sequential client answers as its continuation, run from where the first client stopped. -/ +theorem clientResult_bind (p : D.Client α) (k : α → D.Client β) (s : D.State) : + D.clientResult s (p >>= k) = D.clientResult (D.clientState s p) (k (D.clientResult s p)) := by + change D.clientResult s (p.bind k) = _ + rw [clientResult, runClient_bind] + +/-- A sequential client stops where its continuation, run from where the first client stopped, +stops. -/ +theorem clientState_bind (p : D.Client α) (k : α → D.Client β) (s : D.State) : + D.clientState s (p >>= k) = D.clientState (D.clientState s p) (k (D.clientResult s p)) := by + change D.clientState s (p.bind k) = _ + rw [clientState, runClient_bind] + +/-- The history of a sequential client is the first client's history, then its continuation's. -/ +theorem clientHistory_bind (p : D.Client α) (k : α → D.Client β) (s : D.State) : + D.clientHistory s (p >>= k) = + D.clientHistory s p ++ D.clientHistory (D.clientState s p) (k (D.clientResult s p)) := by + change D.clientHistory s (p.bind k) = _ + rw [clientHistory, runClient_bind] + /-- On a deterministic specification (`[D.HasStepFn f]`), demonic operation-sequence admissibility `ValidOpSeq` coincides with `D.ValidHistory`. -/ @[simp] diff --git a/ComplexityTests/DataStructureMemoryProduct.lean b/ComplexityTests/DataStructureMemoryProduct.lean index 52d5e75184..f058b1a2af 100644 --- a/ComplexityTests/DataStructureMemoryProduct.lean +++ b/ComplexityTests/DataStructureMemoryProduct.lean @@ -99,15 +99,16 @@ theorem inv_layout (i : ℕ) (hi : i < 3) : Stack.Memory.Inv (layout i) empty wh /-- The three stacks hold in the empty memory, with disjoint footprints. -/ theorem inv_empty : triple.inv empty := - ⟨⟨inv_layout 0 (by decide), inv_layout 1 (by decide), by decide⟩, inv_layout 2 (by decide), - by decide⟩ + ⟨⟨inv_layout 0 (by decide), inv_layout 1 (by decide), by decide, by decide⟩, + inv_layout 2 (by decide), by decide, by decide⟩ /-- The specification's answer, transported to the memory run by the iterated product. -/ example : (spec 8).clientResult empty (link triple.methods client) = (product (product S S) S).clientResult (triple.abs empty) client := - (triple.simulates_client client inv_empty + have ⟨_, hres, _⟩ := triple.simulates_client client inv_empty ⟨⟨by decide, by decide⟩, ⟨by decide, by decide⟩, ⟨by decide, by decide⟩, trivial, trivial, - trivial, trivial, trivial⟩).2.1 + trivial, trivial, trivial⟩ + hres /-- Every operation of the three stacks costs at most three memory units, amortized with zero potential: the stacks' certificates add across both products. -/ @@ -117,11 +118,73 @@ theorem amortized : triple.Amortized charge (fun _ _ => 3) fun _ => 0 := by (Framed.Local.const _ 0) rw [productCharge_const] at h₀₁ have h := Framed.amortized_prod h₀₁ (Stack.Memory.amortized (valid 2 (by decide))) - (fun _ _ _ => rfl) (Framed.Local.const _ 0) + (fun _ _ _ _ => rfl) (Framed.Local.const _ 0) rwa [productCharge_const] at h /-- With zero potential, the same certificate is a worst-case charge bound. -/ example : triple.toModule.ChargeBound charge fun _ _ => 3 := amortized.chargeBound +/-! ## Allocated one after the other + +The same three stacks, allocated by a program rather than laid out by hand, in the shape of a +client of three structures such as Dijkstra's algorithm over `product PQ (product Dict Adj)`. +The generic theorems of `Memory.Allocator` give the run's admissibility, answer and cost, with no +setup proof for this client. -/ + +/-- Three stacks of capacity `2`, allocated one after the other. -/ +abbrev allocated : Allocator (product S (product S S)) 8 := + (Stack.Memory.allocator 8 2).prod + ((Stack.Memory.allocator 8 2).prod (Stack.Memory.allocator 8 2)) + +/-- The client above, on the right-nested product. -/ +def client' : (product S (product S S)).Client (List (Option ℕ) × List ℕ) := do + let _ ← (product S (product S S)).call (.inl (.push 1)) + let _ ← (product S (product S S)).call (.inl (.push 2)) + let _ ← (product S (product S S)).call (.inr (.inr (.push 3))) + let a ← (product S (product S S)).call (.inl .pop) + let b ← (product S (product S S)).call (.inl .pop) + let m ← (product S (product S S)).call (.inr (.inl .size)) + let n ← (product S (product S S)).call (.inr (.inr .size)) + pure ([a, b], [m, n]) + +/-- Memory with nothing allocated. -/ +def nothing : State := ⟨fun _ => 0, 0⟩ + +/-- The allocated run answers as the linked client on hand-placed stacks. -/ +example : (spec 8).clientResult nothing (allocated.run client') = ([some 2, some 1], [0, 1]) := by + decide + +/-- It costs the 17 units of the client and three units per allocated stack. -/ +example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing + (allocated.run client')) = 9 + 17 := by + decide + +/-- The client is admissible for the three bounded stacks. -/ +theorem client'_valid : (product S (product S S)).ValidHistory allocated.init + ((product S (product S S)).clientHistory allocated.init client') := + ⟨⟨by decide, by decide⟩, ⟨by decide, by decide⟩, ⟨by decide, by decide⟩, trivial, trivial, + trivial, trivial, trivial⟩ + +/-- The stacks' certificates add across both products of allocators. -/ +theorem amortized_allocated : allocated.Amortized (fun _ _ => 3) 0 := by + have h := Allocator.Amortized.prod (Stack.Memory.amortized_allocator (w := 8) (C := 2)) + (Allocator.Amortized.prod (Stack.Memory.amortized_allocator (w := 8) (C := 2)) + (Stack.Memory.amortized_allocator (w := 8) (C := 2))) + simp only [productCharge_const] at h + exact h + +/-- The generic correctness theorem gives the specification's answer. -/ +example : (spec 8).clientResult nothing (allocated.run client') = + (product S (product S S)).clientResult allocated.init client' := + have ⟨_, hres, _⟩ := + allocated.run_correct client' ⟨trivial, trivial, trivial⟩ (by decide) client'_valid + hres + +/-- The generic cost theorem: nine units to allocate, and three per operation. -/ +example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing + (allocated.run client')) ≤ 9 + 0 + 3 * 7 := + Allocator.run_cost_le amortized_allocated client' ⟨trivial, trivial, trivial⟩ (by decide) + client'_valid + end ComplexityTests.DataStructureMemoryProduct From a46363d8666ed8d0518a0edc86ad5278051d01e1 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 20:06:19 -0500 Subject: [PATCH 15/18] feat(DataStructure/Memory): structures already in memory, and a growth test An allocator may now take over allocated cells instead of allocating them: `Allocator.owned` names them, `Allocates.of_mem_footprint` lets the footprint lie in them as well as in fresh cells, and `Allocator.ProdPre` asks a product's two structures to take over disjoint cells. Fresh allocators default to taking over none. `Allocator.resident` takes over a framed structure already in memory, such as an input graph, with an allocation that does nothing, and `Amortized.resident` carries its certificate over. `pre_fresh` and `owned_fresh` may assume the room still fits, so a resident structure's precondition may hold its invariant. `Framed.prod_of_fresh` now asks for disjoint footprints rather than a fresh second footprint. The new test `DataStructureMemoryAlloc` checks two write-once cells that each reserve one cell and allocate it inside their first `set`, with the footprints, abstractions and frontier plus room after each step and the tight word-size boundary; and stacks held by the input, taken over beside stacks allocated from scratch in the Dijkstra shape. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity/DataStructure/Memory/Alloc.lean | 240 ++++++-- Complexity/DataStructure/Memory/Frame.lean | 13 +- Complexity/DataStructure/Queue/Memory.lean | 11 +- Complexity/DataStructure/Stack/Memory.lean | 3 +- ComplexityTests/DataStructureMemoryAlloc.lean | 572 ++++++++++++++++++ .../DataStructureMemoryProduct.lean | 9 +- 6 files changed, 793 insertions(+), 55 deletions(-) create mode 100644 ComplexityTests/DataStructureMemoryAlloc.lean diff --git a/Complexity/DataStructure/Memory/Alloc.lean b/Complexity/DataStructure/Memory/Alloc.lean index 8b0d8416af..76b96f40c4 100644 --- a/Complexity/DataStructure/Memory/Alloc.lean +++ b/Complexity/DataStructure/Memory/Alloc.lean @@ -23,6 +23,13 @@ first half for a specification `D`: * for each memory with that room, the framed refinement it sets up (`Allocator.framed`), with the proof that the allocation sets it up (`Memory.Allocates`). +The structure's footprint lies in fresh cells, which the allocation and later operations take +from the frontier, and in the allocated cells `Allocator.owned` that it takes over. A structure +allocated from scratch takes over none. One that the input already holds, such as a graph given +as adjacency arrays, takes over the cells holding it, and its precondition `Allocator.Pre` says +that they do. `Allocator.resident` is the allocator of a framed structure already in memory, +whose allocation does nothing. + `Allocator.run` allocates and then runs a client. `Allocator.run_correct` and `Allocator.run_cost` hold for every allocator and every client: a client admissible for the specification issues only admissible memory operations, answers as the specification does, and @@ -39,10 +46,12 @@ Allocators combine as their structures do. * `Allocator.comp` puts a refinement on top, such as a queue over the pair of its stacks, with `Memory.Framed.comp`. -Certificates combine along with them (`Allocator.Amortized.prod`, `Allocator.Amortized.comp`). -A structure that lives in its input rather than in fresh memory, such as a graph given as input, -states what it needs of the memory as the precondition `Allocator.Pre`, which allocations of -other structures preserve. +Certificates combine along with them (`Allocator.Amortized.prod`, `Allocator.Amortized.comp`, +`Allocator.Amortized.resident`). An allocation changes no allocated cell, so it keeps the other +structures' preconditions and the cells they take over (`Allocator.pre_fresh`, +`Allocator.owned_fresh`). The product's precondition (`Allocator.ProdPre`) asks in addition that +the cells the two structures take over be disjoint. So `A₁.prod (A₂.prod A₃)` may combine +structures allocated from scratch with structures that the input holds. ## Main definitions @@ -50,6 +59,8 @@ other structures preserve. * `Memory.Allocator`: an allocation program, the methods it sets up, and their framed refinement. * `Memory.Allocator.run`: allocate, then run a client. * `Memory.Allocator.prod`, `Memory.Allocator.comp`: products and refinements on top. +* `Memory.Allocator.ProdPre`: the precondition of a product. +* `Memory.Allocator.resident`: a structure already in memory. * `Memory.Allocator.Amortized`: an amortized cost certificate of an allocator. ## Main results @@ -57,7 +68,9 @@ other structures preserve. * `Memory.Allocator.run_correct`: the run is admissible and answers as the specification. * `Memory.Allocator.run_cost`, `Memory.Allocator.run_cost_le`: its memory cost. * `Memory.Allocator.run_cost_isBigO`: the cost as `O(m)` for constant per-operation charges. -* `Memory.Allocator.Amortized.prod`, `Memory.Allocator.Amortized.comp`: certificates combine. +* `Memory.Allocator.Amortized.prod`, `Memory.Allocator.Amortized.comp`, + `Memory.Allocator.Amortized.resident`: certificates combine, and carry over to structures + already in memory. -/ @[expose] public section @@ -71,11 +84,11 @@ variable {w : ℕ} /-- What running the allocation program `alloc` from the memory `s` establishes for the framed refinement `M`: its operations are admissible and cost at most `cost`, it changes no allocated cell, and it sets up `M` with the methods of the handle it returns, holding at the abstract state -`init` on fresh cells, with the frontier plus `M`'s room at most `room` beyond the old -frontier. -/ +`init`. The footprint lies in the allocated cells `owned` that the structure takes over and in +fresh cells, and the frontier plus `M`'s room is at most `room` beyond the old frontier. -/ structure Allocates {D : StateTransitionSpec.{0, 0, 0}} {H : Type} (alloc : Prog H) - (methods : H → Methods D (spec w)) (init : D.State) (room cost : ℕ) (s : State) - (M : Framed D w) : Prop where + (methods : H → Methods D (spec w)) (init : D.State) (owned : Finset ℕ) (room cost : ℕ) + (s : State) (M : Framed D w) : Prop where /-- The allocation's operations are admissible. -/ validHistory : (spec w).ValidHistory s ((spec w).clientHistory s alloc) /-- The allocation costs at most `cost`. -/ @@ -88,8 +101,8 @@ structure Allocates {D : StateTransitionSpec.{0, 0, 0}} {H : Type} (alloc : Prog inv : M.inv ((spec w).clientState s alloc) /-- It represents `init`. -/ abs_eq : M.abs ((spec w).clientState s alloc) = init - /-- Its footprint is fresh. -/ - le_footprint : ∀ a ∈ M.footprint ((spec w).clientState s alloc), s.frontier ≤ a + /-- Its footprint lies in the cells it takes over and in fresh cells. -/ + of_mem_footprint : ∀ a ∈ M.footprint ((spec w).clientState s alloc), a ∈ owned ∨ s.frontier ≤ a /-- The allocation and the structure's reserved room take at most `room` cells. -/ room_le : ((spec w).clientState s alloc).frontier + M.room ((spec w).clientState s alloc) ≤ s.frontier + room @@ -97,7 +110,9 @@ structure Allocates {D : StateTransitionSpec.{0, 0, 0}} {H : Type} (alloc : Prog /-- **Allocators.** A memory program `alloc` that allocates a structure implementing `D`, and the structure's methods, given the handle that `alloc` returns. From a memory satisfying `Pre` in which `room` cells beyond the frontier fit a word, the allocation sets up the framed refinement -`framed s`, which represents `init`, at a memory cost of at most `cost`. -/ +`framed s`, which represents `init`, at a memory cost of at most `cost`. Its footprint lies in +fresh cells and in the allocated cells `owned s` that it takes over, such as an input that holds +the structure already. -/ structure Allocator (D : StateTransitionSpec.{0, 0, 0}) (w : ℕ) where /-- What the allocation returns, such as a layout of named fields. -/ Handle : Type @@ -111,16 +126,26 @@ structure Allocator (D : StateTransitionSpec.{0, 0, 0}) (w : ℕ) where room : ℕ /-- A bound on the memory cost of the allocation. -/ cost : ℕ - /-- What the allocation needs of the memory besides room, such as an input it reads. -/ + /-- What the allocation needs of the memory besides room, such as the structure it takes + over. -/ Pre : State → Prop := fun _ => True - /-- Runs that keep every allocated cell, such as other allocations, keep the precondition. -/ + /-- The allocated cells that the structure takes over instead of allocating them. -/ + owned : State → Finset ℕ := fun _ => ∅ + /-- The cells taken over are allocated. -/ + owned_lt : ∀ {s a}, Pre s → a ∈ owned s → a < s.frontier := by intros; simp_all + /-- Runs that keep every allocated cell and leave the room fitting, such as other allocations, + keep the precondition. -/ pre_fresh : ∀ {s s' : State}, Pre s → (∀ a < s.frontier, s'.mem a = s.mem a) → - s.frontier ≤ s'.frontier → Pre s' := by intros; trivial + s.frontier ≤ s'.frontier → Fits w (s'.frontier + room) → Pre s' := by intros; trivial + /-- Such runs keep the cells taken over. -/ + owned_fresh : ∀ {s s' : State}, Pre s → (∀ a < s.frontier, s'.mem a = s.mem a) → + s.frontier ≤ s'.frontier → Fits w (s'.frontier + room) → owned s' = owned s := by + intros; rfl /-- The framed refinement that allocating from `s` sets up. -/ framed : (s : State) → Pre s → Fits w (s.frontier + room) → Framed D w /-- Allocating from `s` sets it up. -/ allocates : ∀ s (hs : Pre s) (hroom : Fits w (s.frontier + room)), - Allocates alloc methods init room cost s (framed s hs hroom) + Allocates alloc methods init (owned s) room cost s (framed s hs hroom) namespace Allocator @@ -244,7 +269,10 @@ def comp (R : Refinement D E) (A : Allocator E w) (hinit : R.inv A.init) : Alloc room := A.room cost := A.cost Pre := A.Pre + owned := A.owned + owned_lt := A.owned_lt pre_fresh := A.pre_fresh + owned_fresh := A.owned_fresh framed s hs hroom := Framed.comp R (A.framed s hs hroom) allocates s hs hroom := have h := A.allocates s hs hroom @@ -254,7 +282,7 @@ def comp (R : Refinement D E) (A : Allocator E w) (hinit : R.inv A.init) : Alloc methods_eq := by rw [Framed.comp_methods, h.methods_eq] inv := ⟨h.inv, by rw [h.abs_eq]; exact hinit⟩ abs_eq := by rw [Framed.comp_abs, h.abs_eq] - le_footprint := h.le_footprint + of_mem_footprint := h.of_mem_footprint room_le := h.room_le } /-- **Potentials stack through a refinement on top.** A certificate of `R` against the middle @@ -310,11 +338,6 @@ theorem clientResult_allocPair (s : State) : variable {A₁} -/-- After allocating the first structure, the second one's precondition still holds. -/ -theorem pre_after {s : State} (hs₁ : A₁.Pre s) (hroom : Fits w (s.frontier + A₁.room)) - (hs₂ : A₂.Pre s) : A₂.Pre ((spec w).clientState s A₁.alloc) := - A₂.pre_fresh hs₂ (A₁.allocates s hs₁ hroom).mem_eq (frontier_le_clientState s _) - /-- After allocating the first structure, room for both leaves room for the second. -/ theorem room_after {s : State} (hs₁ : A₁.Pre s) {r : ℕ} (hroom : Fits w (s.frontier + (A₁.room + r))) : @@ -323,10 +346,35 @@ theorem room_after {s : State} (hs₁ : A₁.Pre s) {r : ℕ} have := (A₁.allocates s hs₁ (hroom.of_le (by omega))).room_le omega) +/-- After allocating the first structure, the second one's precondition still holds. -/ +theorem pre_after {s : State} (hs₁ : A₁.Pre s) + (hroom : Fits w (s.frontier + (A₁.room + A₂.room))) (hs₂ : A₂.Pre s) : + A₂.Pre ((spec w).clientState s A₁.alloc) := + A₂.pre_fresh hs₂ (A₁.allocates s hs₁ (hroom.of_le (by omega))).mem_eq + (frontier_le_clientState s _) (room_after hs₁ hroom) + +/-- After allocating the first structure, the second one takes over the same cells. -/ +theorem owned_after {s : State} (hs₁ : A₁.Pre s) + (hroom : Fits w (s.frontier + (A₁.room + A₂.room))) (hs₂ : A₂.Pre s) : + A₂.owned ((spec w).clientState s A₁.alloc) = A₂.owned s := + A₂.owned_fresh hs₂ (A₁.allocates s hs₁ (hroom.of_le (by omega))).mem_eq + (frontier_le_clientState s _) (room_after hs₁ hroom) + variable (A₁) +/-- The precondition of a product of allocators: both preconditions, and the cells that the two +structures take over are disjoint. -/ +structure ProdPre (s : State) : Prop where + /-- The first allocator's precondition. -/ + left : A₁.Pre s + /-- The second allocator's precondition. -/ + right : A₂.Pre s + /-- The two structures take over disjoint cells. -/ + disjoint : Disjoint (A₁.owned s) (A₂.owned s) + /-- **Products of allocators.** Allocate the two structures one after the other, and set up -their product over one memory. The room and the cost add. -/ +their product over one memory. The room and the cost add, and the product takes over the cells +that either structure takes over. -/ def prod : Allocator (product D₁ D₂) w where Handle := A₁.Handle × A₂.Handle alloc := allocPair A₁ A₂ @@ -334,25 +382,53 @@ def prod : Allocator (product D₁ D₂) w where init := (A₁.init, A₂.init) room := A₁.room + A₂.room cost := A₁.cost + A₂.cost - Pre s := A₁.Pre s ∧ A₂.Pre s - pre_fresh hs hmem hf := ⟨A₁.pre_fresh hs.1 hmem hf, A₂.pre_fresh hs.2 hmem hf⟩ + Pre := ProdPre A₁ A₂ + owned s := A₁.owned s ∪ A₂.owned s + owned_lt hs ha := (Finset.mem_union.mp ha).elim (A₁.owned_lt hs.left) (A₂.owned_lt hs.right) + pre_fresh hs hmem hf hr := + have hr₁ := hr.of_le (Nat.add_le_add_left (Nat.le_add_right A₁.room A₂.room) _) + have hr₂ := hr.of_le (Nat.add_le_add_left (Nat.le_add_left A₂.room A₁.room) _) + { left := A₁.pre_fresh hs.left hmem hf hr₁ + right := A₂.pre_fresh hs.right hmem hf hr₂ + disjoint := by + rw [A₁.owned_fresh hs.left hmem hf hr₁, A₂.owned_fresh hs.right hmem hf hr₂] + exact hs.disjoint } + owned_fresh hs hmem hf hr := by + rw [A₁.owned_fresh hs.left hmem hf + (hr.of_le (Nat.add_le_add_left (Nat.le_add_right A₁.room A₂.room) _)), + A₂.owned_fresh hs.right hmem hf + (hr.of_le (Nat.add_le_add_left (Nat.le_add_left A₂.room A₁.room) _))] framed s hs hroom := - (A₁.framed s hs.1 (hroom.of_le (by omega))).prod - (A₂.framed ((spec w).clientState s A₁.alloc) - (pre_after A₂ hs.1 (hroom.of_le (by omega)) hs.2) (room_after hs.1 hroom)) + (A₁.framed s hs.left (hroom.of_le (by omega))).prod + (A₂.framed ((spec w).clientState s A₁.alloc) (pre_after A₂ hs.left hroom hs.right) + (room_after hs.left hroom)) allocates s hs hroom := by -- The two allocations, from `s` to `s₁` and from `s₁` to `s₂`. - have h₁ := A₁.allocates s hs.1 (hroom.of_le (by omega)) - have h₂ := A₂.allocates _ (pre_after A₂ hs.1 (hroom.of_le (by omega)) hs.2) - (room_after hs.1 hroom) + obtain ⟨hs₁, hs₂, hd⟩ := hs + have hroom₁ : Fits w (s.frontier + A₁.room) := hroom.of_le (by omega) + have h₁ := A₁.allocates s hs₁ hroom₁ + have h₂ := A₂.allocates _ (pre_after A₂ hs₁ hroom hs₂) (room_after hs₁ hroom) + have ho₂ := owned_after A₂ hs₁ hroom hs₂ have hf₁ := frontier_le_clientState (w := w) s A₁.alloc have hfit := h₁.room_le have hfit₂ := h₂.room_le + -- The second footprint lies in cells taken over, which are allocated and apart from the + -- first structure's, and in cells fresh after the first allocation. obtain ⟨hinv, habs, hfp, hr⟩ := Framed.prod_of_fresh h₁.inv h₂.mem_eq - (frontier_le_clientState _ _) h₂.inv h₂.le_footprint (hroom.of_le (by omega)) + (frontier_le_clientState _ _) h₂.inv + (Finset.disjoint_left.mpr fun a ha ha' => by + have hlt := (A₁.framed s hs₁ hroom₁).lt_frontier h₁.inv ha + rcases h₂.of_mem_footprint a ha' with ha' | ha' + · rw [ho₂] at ha' + have := A₂.owned_lt hs₂ ha' + rcases h₁.of_mem_footprint a ha with ha | ha + · exact Finset.disjoint_left.mp hd ha ha' + · omega + · omega) + (hroom.of_le (by omega)) refine { validHistory := ?_, cost_le := ?_, mem_eq := ?_, methods_eq := ?_, inv := ?_, - abs_eq := ?_, le_footprint := ?_, room_le := ?_ } + abs_eq := ?_, of_mem_footprint := ?_, room_le := ?_ } · rw [clientHistory_allocPair, StateTransitionSpec.validHistory_append, StateTransitionSpec.run_runClient] exact ⟨h₁.validHistory, h₂.validHistory⟩ @@ -370,8 +446,11 @@ def prod : Allocator (product D₁ D₂) w where · intro a ha rw [clientState_allocPair, hfp] at ha rcases Finset.mem_union.mp ha with ha | ha - · exact h₁.le_footprint a ha - · exact hf₁.trans (h₂.le_footprint a ha) + · exact (h₁.of_mem_footprint a ha).imp_left (Finset.mem_union_left _) + · rcases h₂.of_mem_footprint a ha with ha | ha + · rw [ho₂] at ha + exact .inl (Finset.mem_union_right _ ha) + · exact .inr (hf₁.trans ha) · rw [clientState_allocPair, hr] omega @@ -388,6 +467,13 @@ def prod : Allocator (product D₁ D₂) w where /-- The product's allocation costs at most the sum of the costs. -/ @[simp] theorem prod_cost : (A₁.prod A₂).cost = A₁.cost + A₂.cost := rfl +/-- The product takes over the cells that either structure takes over. -/ +@[simp] theorem prod_owned (s : State) : + (A₁.prod A₂).owned s = A₁.owned s ∪ A₂.owned s := rfl + +/-- The product's precondition. -/ +theorem prod_pre_iff (s : State) : (A₁.prod A₂).Pre s ↔ ProdPre A₁ A₂ s := Iff.rfl + variable {A₁ A₂} /-- **Certificates add across a product of allocators**: the charges componentwise, the @@ -396,20 +482,21 @@ theorem Amortized.prod {upper₁ : D₁.State → D₁.Op → ℕ} {upper₂ : D {Φ₁ Φ₂ : ℕ} (hA₁ : A₁.Amortized upper₁ Φ₁) (hA₂ : A₂.Amortized upper₂ Φ₂) : (A₁.prod A₂).Amortized (productCharge upper₁ upper₂) (Φ₁ + Φ₂) := by intro s hs hroom + obtain ⟨hs₁, hs₂, -⟩ := hs have hroom₁ : Fits w (s.frontier + A₁.room) := hroom.of_le (by simp only [prod_room]; omega) - have hs₂ := pre_after A₂ hs.1 hroom₁ hs.2 - have hroom₂ := room_after hs.1 hroom - obtain ⟨Ψ₁, hc₁, hl₁, hΨ₁⟩ := hA₁ s hs.1 hroom₁ - obtain ⟨Ψ₂, hc₂, hl₂, hΨ₂⟩ := hA₂ _ hs₂ hroom₂ + have hs₂' := pre_after A₂ hs₁ hroom hs₂ + have hroom₂ := room_after hs₁ hroom + obtain ⟨Ψ₁, hc₁, hl₁, hΨ₁⟩ := hA₁ s hs₁ hroom₁ + obtain ⟨Ψ₂, hc₂, hl₂, hΨ₂⟩ := hA₂ _ hs₂' hroom₂ refine ⟨fun s' => Ψ₁ s' + Ψ₂ s', Framed.amortized_prod hc₁ hc₂ hl₁ hl₂, Framed.Local.add hl₁.prod_left hl₂.prod_right, ?_⟩ -- The first potential is unchanged by the second allocation. - have h₁ := A₁.allocates s hs.1 hroom₁ - have h₂ := A₂.allocates _ hs₂ hroom₂ + have h₁ := A₁.allocates s hs₁ hroom₁ + have h₂ := A₂.allocates _ hs₂' hroom₂ have hfit := h₁.room_le have hfit₂ := h₂.room_le have hkeep := hl₁ h₁.inv - (fun a ha => h₂.mem_eq a ((A₁.framed s hs.1 hroom₁).lt_frontier h₁.inv ha)) + (fun a ha => h₂.mem_eq a ((A₁.framed s hs₁ hroom₁).lt_frontier h₁.inv ha)) (frontier_le_clientState _ _) (hroom.of_le (by simp only [prod_room]; omega)) change Ψ₁ ((spec w).clientState s (allocPair A₁ A₂)) + Ψ₂ ((spec w).clientState s (allocPair A₁ A₂)) ≤ Φ₁ + Φ₂ @@ -418,6 +505,75 @@ theorem Amortized.prod {upper₁ : D₁.State → D₁.Op → ℕ} {upper₂ : D end Prod +/-! ## Structures already in memory -/ + +section Resident + +variable (M : Framed D w) (init : D.State) (room : ℕ) + +/-- **A structure already in memory**, such as a graph that the input holds as adjacency arrays. +Its allocation does nothing. It takes over the footprint of `M`, and needs `M` to hold, to +represent `init` and to have at most `room` cells of room. -/ +def resident : Allocator D w where + Handle := Unit + alloc := pure () + methods _ := M.methods + init := init + room := room + cost := 0 + Pre s := M.inv s ∧ M.abs s = init ∧ M.room s ≤ room + owned := M.footprint + owned_lt hs ha := M.lt_frontier hs.1 ha + pre_fresh {s _} hs hmem hf hr := by + obtain ⟨hinv, habs, hle⟩ := hs + obtain ⟨hinv', habs', -, hroom'⟩ := M.of_fresh hinv hmem hf (hr.of_le (by omega)) + exact ⟨hinv', habs'.trans habs, hroom' ▸ hle⟩ + owned_fresh {s _} hs hmem hf hr := by + obtain ⟨hinv, -, hle⟩ := hs + obtain ⟨-, -, hfp, -⟩ := M.of_fresh hinv hmem hf (hr.of_le (by omega)) + exact hfp + framed _ _ _ := M + allocates s hs _ := by + obtain ⟨hinv, habs, hle⟩ := hs + exact + { validHistory := trivial + cost_le := le_rfl + mem_eq := fun _ _ => rfl + methods_eq := rfl + inv := hinv + abs_eq := habs + of_mem_footprint := fun _ ha => .inl ha + room_le := Nat.add_le_add_left hle s.frontier } + +/-- A structure already in memory runs `M`'s methods. -/ +@[simp] theorem resident_methods (h : Unit) : (resident M init room).methods h = M.methods := rfl + +/-- It represents `init`. -/ +@[simp] theorem resident_init : (resident M init room).init = init := rfl + +/-- It takes over `M`'s footprint. -/ +@[simp] theorem resident_owned (s : State) : (resident M init room).owned s = M.footprint s := rfl + +/-- Its allocation costs nothing. -/ +@[simp] theorem resident_cost : (resident M init room).cost = 0 := rfl + +/-- Its precondition: `M` holds, represents `init` and has at most `room` cells of room. -/ +theorem resident_pre_iff (s : State) : + (resident M init room).Pre s ↔ M.inv s ∧ M.abs s = init ∧ M.room s ≤ room := Iff.rfl + +variable {M init room} + +/-- **The certificate of a structure already in memory** is the structure's own: a potential +that reads only its footprint and is at most `Φ₀` wherever the structure represents `init`. -/ +theorem Amortized.resident {upper : D.State → D.Op → ℕ} {Φ : State → ℕ} {Φ₀ : ℕ} + (hc : M.Amortized charge upper Φ) (hl : M.Local Φ) + (hΦ : ∀ s, M.inv s → M.abs s = init → Φ s ≤ Φ₀) : + (resident M init room).Amortized upper Φ₀ := fun s hs _ => by + obtain ⟨hinv, habs, -⟩ := hs + exact ⟨Φ, hc, hl, hΦ s hinv habs⟩ + +end Resident + end Allocator end Cslib.DataStructure.Memory diff --git a/Complexity/DataStructure/Memory/Frame.lean b/Complexity/DataStructure/Memory/Frame.lean index a2bec2f0be..77c9e4598a 100644 --- a/Complexity/DataStructure/Memory/Frame.lean +++ b/Complexity/DataStructure/Memory/Frame.lean @@ -478,20 +478,19 @@ theorem amortized_prod {lower : State → Op → ℕ} {upper₁ : D₁.State → /-- Setting up a product from structures allocated one after the other: `M₁` holds at `s`; a run from `s` to `s'` leaves the cells below `s.frontier` unchanged; `M₂` holds at `s'` on a -footprint at or above `s.frontier`; and the frontier plus both rooms fits. Then the product holds -at `s'`, with `M₁`'s abstraction, footprint and room unchanged. -/ +footprint disjoint from `M₁`'s, such as one at or above `s.frontier`; and the frontier plus both +rooms fits. Then the product holds at `s'`, with `M₁`'s abstraction, footprint and room +unchanged. -/ theorem prod_of_fresh {s s' : State} (h₁ : M₁.inv s) (hmem : ∀ a < s.frontier, s'.mem a = s.mem a) (hf : s.frontier ≤ s'.frontier) - (h₂ : M₂.inv s') (hfp : ∀ a ∈ M₂.footprint s', s.frontier ≤ a) + (h₂ : M₂.inv s') (hd : Disjoint (M₁.footprint s) (M₂.footprint s')) (hroom : Fits w (s'.frontier + (M₁.room s + M₂.room s'))) : (M₁.prod M₂).inv s' ∧ (M₁.prod M₂).abs s' = (M₁.abs s, M₂.abs s') ∧ (M₁.prod M₂).footprint s' = M₁.footprint s ∪ M₂.footprint s' ∧ (M₁.prod M₂).room s' = M₁.room s + M₂.room s' := by obtain ⟨h₁', habs, hfp₁, hr₁⟩ := M₁.of_fresh h₁ hmem hf (hroom.of_le (by omega)) - refine ⟨⟨h₁', h₂, Finset.disjoint_left.mpr fun a ha ha' => ?_, by rwa [hr₁]⟩, - by rw [prod_abs, habs], by rw [prod_footprint, hfp₁], by rw [prod_room, hr₁]⟩ - rw [hfp₁] at ha - exact absurd (M₁.lt_frontier h₁ ha) (Nat.not_lt.mpr (hfp a ha')) + exact ⟨⟨h₁', h₂, by rwa [hfp₁], by rwa [hr₁]⟩, by rw [prod_abs, habs], + by rw [prod_footprint, hfp₁], by rw [prod_room, hr₁]⟩ end Framed diff --git a/Complexity/DataStructure/Queue/Memory.lean b/Complexity/DataStructure/Queue/Memory.lean index 5b53a71444..e3845a1645 100644 --- a/Complexity/DataStructure/Queue/Memory.lean +++ b/Complexity/DataStructure/Queue/Memory.lean @@ -206,6 +206,11 @@ def allocator (w C : ℕ) : Allocator (Queue.WordRAM.spec w C) w := change (C + 1) + (C + 1) = _ omega +/-- The two stacks are allocated from scratch, so the allocation needs nothing of the memory +besides room. -/ +theorem pre_allocator (s : Memory.State) : (allocator w C).Pre s := + ⟨trivial, trivial, Finset.disjoint_empty_left _⟩ + /-- **Stacked potentials.** Every queue operation costs at most nine memory units, amortized, with six credits per element of the rear stack: the queue's potential, at three memory units per stack call, read through the stacks' abstraction. Allocation leaves no potential. -/ @@ -227,7 +232,7 @@ theorem run_correct {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Me (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : (Memory.spec w).ValidHistory s ((Memory.spec w).clientHistory s (run w C p)) ∧ (Memory.spec w).clientResult s (run w C p) = (Queue.WordRAM.spec w C).clientResult [] p := - have ⟨hvE, hres, _⟩ := (allocator w C).run_correct p ⟨trivial, trivial⟩ (room_allocator ▸ hfit) hv + have ⟨hvE, hres, _⟩ := (allocator w C).run_correct p (pre_allocator s) (room_allocator ▸ hfit) hv ⟨hvE, hres⟩ /-- **Cost.** The same run costs at most `6 + 9 m` memory units for a client of `m` queue @@ -237,7 +242,7 @@ theorem run_cost {β : Type} (p : (Queue.WordRAM.spec w C).Client β) {s : Memor (hv : (Queue.WordRAM.spec w C).ValidHistory [] ((Queue.WordRAM.spec w C).clientHistory [] p)) : (Memory.spec w).totalCost charge s ((Memory.spec w).clientHistory s (run w C p)) ≤ 6 + 9 * ((Queue.WordRAM.spec w C).clientHistory [] p).length := - Allocator.run_cost_le amortized_allocator p ⟨trivial, trivial⟩ (room_allocator ▸ hfit) hv + Allocator.run_cost_le amortized_allocator p (pre_allocator s) (room_allocator ▸ hfit) hv /-- **Cost with glue.** A realization also charges the glue that runs after each memory operation. If that glue costs at most `K` times the operation's charge plus `K`, the run costs at most @@ -263,6 +268,6 @@ theorem run_cost_isBigO (s : Memory.State) (hfit : Fits w (s.frontier + 2 * (C + ((Queue.WordRAM.spec w C).clientHistory [] p.1).length) (fun p => (Memory.spec w).totalCost charge s ((Memory.spec w).clientHistory s (run w C p.1))) := - Allocator.run_cost_isBigO amortized_allocator ⟨trivial, trivial⟩ (room_allocator ▸ hfit) + Allocator.run_cost_isBigO amortized_allocator (pre_allocator s) (room_allocator ▸ hfit) end Cslib.DataStructure.Queue.Memory diff --git a/Complexity/DataStructure/Stack/Memory.lean b/Complexity/DataStructure/Stack/Memory.lean index 647b390338..4db5ea5025 100644 --- a/Complexity/DataStructure/Stack/Memory.lean +++ b/Complexity/DataStructure/Stack/Memory.lean @@ -410,7 +410,7 @@ def allocator (w C : ℕ) : Allocator (Stack.WordRAM.spec w C) w where obtain ⟨hinv, habs⟩ := inv_allocLayout (C := C) s refine { validHistory := ?_, cost_le := ?_, mem_eq := ?_, methods_eq := rfl, inv := ?_, - abs_eq := ?_, le_footprint := ?_, room_le := ?_ } <;> + abs_eq := ?_, of_mem_footprint := ?_, room_le := ?_ } <;> simp only [StateTransitionSpec.clientHistory, StateTransitionSpec.clientState, runClient_allocLayout] · simp only [StateTransitionSpec.validHistory_cons, StateTransitionSpec.stepFn, step_alloc, @@ -423,6 +423,7 @@ def allocator (w C : ℕ) : Allocator (Stack.WordRAM.spec w C) w where · intro a ha simp only [framed_footprint, Layout.footprint, Finset.mem_union, Block.mem_footprint, Block.Owns] at ha + right omega · simp only [framed, Memory.Framed.ofStatic] omega diff --git a/ComplexityTests/DataStructureMemoryAlloc.lean b/ComplexityTests/DataStructureMemoryAlloc.lean new file mode 100644 index 0000000000..26680b3cc1 --- /dev/null +++ b/ComplexityTests/DataStructureMemoryAlloc.lean @@ -0,0 +1,572 @@ +/- +Copyright (c) 2026 Samuel Schlesinger. All rights reserved. +Released under Apache 2.0 license as described in the file LICENSE. +Authors: Samuel Schlesinger +-/ + +module +-- upstream: local-only (test driver) + +public import Complexity.DataStructure.Stack.Memory + +/-! +# Allocation inside operations, and structures already in memory + +Two checks of the allocator contract of `Memory.Allocator`, at word size `8`. + +**Room consumed inside operations.** A write-once cell reserves one cell of room when it is +allocated, and allocates that cell inside its first `set`. Two such cells share one memory as +`cells`, the product of their allocators. A client sets them alternately, so each `set` +allocates above the other structure's newest cell. The tests check: + +* the answers and the admissibility of the whole run, by the generic `Allocator.run_correct`; +* the allocations inside the operations, in the run's memory history; +* after each step, the footprints, the abstractions and the frontier plus the remaining room, + which stays at the frontier the allocation reserved up to; +* the boundary: from frontier `251`, where `251 + 4` fits a byte, the run is admissible. From + `252` it is not, and only the right `set` fails: its allocation, of the last of the four + reserved cells, would leave the frontier at `256`. + +**Structures already in memory.** A stack that the input holds is taken over by +`Allocator.resident`, in the shape of Dijkstra's algorithm: two stacks are allocated from +scratch above the input, and the input's stack is used in place. The generic theorems give the +run's answer and cost. Two stacks that the input holds in disjoint cells combine too. Taking +over the same cells twice is excluded by the product's precondition. +-/ + +@[expose] public section + +namespace ComplexityTests.DataStructureMemoryAlloc + +open Cslib.WordRAM Cslib.DataStructure Cslib.DataStructure.Memory + +/-! ## A write-once cell that allocates on its first `set` -/ + +/-- The operations of a write-once cell: set it once, and get its value. -/ +inductive CellOp where + /-- Set the cell to a value. -/ + | set (v : ℕ) + /-- Get the value, if set. -/ + | get + deriving DecidableEq + +/-- Setting answers nothing; getting answers the value, if set. -/ +abbrev CellOp.Result : CellOp → Type + | .set _ => Unit + | .get => Option ℕ + +/-- Setting stores the value, and getting returns it. -/ +def cellStep (c : Option ℕ) : (o : CellOp) → Option ℕ × o.Result + | .set v => (some v, ()) + | .get => (c, c) + +/-- A cell is set once, to a word. -/ +def cellValid (w : ℕ) (c : Option ℕ) : CellOp → Prop + | .set v => c = none ∧ Fits w v + | .get => True + +instance (w : ℕ) (c : Option ℕ) (o : CellOp) : Decidable (cellValid w c o) := by + cases o <;> unfold cellValid <;> infer_instance + +/-- The write-once cell at word size `w`. -/ +abbrev Cell (w : ℕ) : StateTransitionSpec := + StateTransitionSpec.ofFn (State := Option ℕ) (Op := CellOp) (Result := CellOp.Result) cellStep + (cellValid w) + +/-- Admissibility of a concrete history of cell operations is decidable. -/ +instance (w : ℕ) (c : Option ℕ) (os : List CellOp) : Decidable ((Cell w).ValidHistory c os) := by + induction os generalizing c with + | nil => exact isTrue trivial + | cons o os ih => + exact decidable_of_iff (cellValid w c o ∧ (Cell w).ValidHistory (cellStep c o).1 os) Iff.rfl + +variable (h w : ℕ) + +/-- Set: allocate the value cell, write the value there, and point the header `h` at it. -/ +def cellSet (v : ℕ) : Prog Unit := do + let p ← allocDirty 1 + write p v + write h p + +/-- Get: a zero header means unset; otherwise read the cell that it points to. -/ +def cellGet : Prog (Option ℕ) := do + let p ← read h + if p = 0 then pure none else do + let v ← read p + pure (some v) + +/-- The cell's operations as memory programs, with header `h`. -/ +def cellMethods : Methods (Cell w) (spec w) + | .set v => cellSet h v + | .get => cellGet h + +/-- The value cell, if the header points to one. -/ +def cellAbs (s : State) : Option ℕ := if s.mem h = 0 then none else some (s.mem (s.mem h)) + +/-- The header, and the value cell once set. -/ +def cellFootprint (s : State) : Finset ℕ := if s.mem h = 0 then {h} else {h, s.mem h} + +/-- One cell of room until the cell is set. -/ +def cellRoom (s : State) : ℕ := if s.mem h = 0 then 1 else 0 + +/-- The header and the value cell are allocated, and the frontier plus the room fits a word, so +that the first `set` may allocate. -/ +structure CellInv (s : State) : Prop where + /-- The header is allocated. -/ + header_lt : h < s.frontier + /-- The value cell is allocated. -/ + value_lt : s.mem h ≠ 0 → s.mem h < s.frontier + /-- The reserved cell fits. -/ + fits_room : Fits w (s.frontier + cellRoom h s) + +/-- A set allocates one cell at the frontier, writes the value there and points the header at +it. -/ +theorem runClient_cellSet (s : State) (v : ℕ) : + (spec w).runClient s (cellSet h v) = + ((), ⟨Function.update (Function.update s.mem s.frontier v) h s.frontier, s.frontier + 1⟩, + [.allocDirty 1, .write s.frontier v, .write h s.frontier]) := rfl + +/-- A get of an unset cell reads the header. -/ +theorem runClient_cellGet_of_eq_zero (s : State) (h0 : s.mem h = 0) : + (spec w).runClient s (cellGet h) = (none, s, [.read h]) := by + simp [cellGet, h0] + +/-- A get of a set cell reads the header and the value cell. -/ +theorem runClient_cellGet_of_ne_zero (s : State) (h0 : s.mem h ≠ 0) : + (spec w).runClient s (cellGet h) = (some (s.mem (s.mem h)), s, [.read h, .read (s.mem h)]) := by + unfold cellGet + simp only [StateTransitionSpec.runClient_lift_bind', StateTransitionSpec.stepFn, step_read, h0, + ↓reduceIte] + rfl + +/-- Allocating a header clears one cell at the frontier. -/ +theorem runClient_alloc_one (s : State) : + (spec w).runClient s (alloc 1) = + (s.frontier, ⟨clear s.mem s.frontier 1, s.frontier + 1⟩, [.alloc 1]) := rfl + +variable {h w} + +/-- A cell is unset exactly when its header is zero. -/ +theorem cellAbs_eq_none {s : State} : cellAbs h s = none ↔ s.mem h = 0 := by + unfold cellAbs + split <;> simp_all + +variable (h w) + +/-- The write-once cell with header `h` implements its specification. -/ +def cellRefinement : Refinement (Cell w) (spec w) := + Refinement.ofFn (cellMethods h w) (CellInv h w) (cellAbs h) + (by + intro s o hs hv + have hh := CellInv.header_lt hs + cases o with + | set v => + obtain ⟨hnone, hfit⟩ := hv + have h0 := cellAbs_eq_none.mp hnone + have hr := CellInv.fits_room hs + simp only [cellRoom, h0, ↓reduceIte] at hr + simp only [cellMethods, StateTransitionSpec.clientHistory, runClient_cellSet, + StateTransitionSpec.validHistory_cons, StateTransitionSpec.stepFn, step_allocDirty, + step_write, valid_allocDirty, valid_write] + exact ⟨hr, ⟨Nat.lt_add_one _, hfit⟩, ⟨by omega, hr.of_le (by omega)⟩, trivial⟩ + | get => + by_cases h0 : s.mem h = 0 + · simp only [cellMethods, StateTransitionSpec.clientHistory, + runClient_cellGet_of_eq_zero h w s h0] + exact ⟨hh, trivial⟩ + · simp only [cellMethods, StateTransitionSpec.clientHistory, + runClient_cellGet_of_ne_zero h w s h0] + exact ⟨hh, CellInv.value_lt hs h0, trivial⟩) + (by + intro s o hs hv + have hh := CellInv.header_lt hs + cases o with + | set v => + obtain ⟨hnone, -⟩ := hv + have h0 := cellAbs_eq_none.mp hnone + have hr := CellInv.fits_room hs + simp only [cellRoom, h0, ↓reduceIte] at hr + have hne : s.frontier ≠ h := by omega + have hf0 : s.frontier ≠ 0 := by omega + simp only [cellMethods, StateTransitionSpec.clientState, StateTransitionSpec.clientResult, + runClient_cellSet] + refine ⟨?_, ⟨by simp only; omega, fun _ => ?_, ?_⟩⟩ + · simp [cellAbs, cellStep, hne, hf0] + · simp only [Function.update_self] + omega + · simpa [cellRoom, hf0] using hr + | get => + by_cases h0 : s.mem h = 0 + · simp only [cellMethods, StateTransitionSpec.clientState, + StateTransitionSpec.clientResult, runClient_cellGet_of_eq_zero h w s h0] + exact ⟨by simp [cellAbs, h0, cellStep], hs⟩ + · simp only [cellMethods, StateTransitionSpec.clientState, + StateTransitionSpec.clientResult, runClient_cellGet_of_ne_zero h w s h0] + exact ⟨by simp [cellAbs, h0, cellStep], hs⟩) + +/-- The refinement runs the cell's methods. -/ +@[simp] theorem cellRefinement_methods : (cellRefinement h w).methods = cellMethods h w := rfl + +/-- The refinement's abstraction. -/ +@[simp] theorem cellRefinement_abs : (cellRefinement h w).abs = cellAbs h := rfl + +/-- The refinement's invariant. -/ +@[simp] theorem cellRefinement_inv : (cellRefinement h w).inv = CellInv h w := rfl + +/-- The header belongs to the footprint. -/ +theorem header_mem_cellFootprint (s : State) : h ∈ cellFootprint h s := by + unfold cellFootprint + split <;> simp + +/-- **The write-once cell, framed with room.** Its footprint grows into the cell that its first +`set` allocates, and that `set` gives up the one cell of room it advances the frontier by. -/ +def cellFramed : Framed (Cell w) w where + toRefinement := cellRefinement h w + footprint := cellFootprint h + room := cellRoom h + lt_frontier {s a} hs ha := by + have hh := CellInv.header_lt hs + unfold cellFootprint at ha + split at ha + · rw [Finset.mem_singleton] at ha + omega + · rcases Finset.mem_insert.mp ha with ha | ha + · omega + · rw [Finset.mem_singleton] at ha + rw [ha] + exact CellInv.value_lt hs (by assumption) + congr {s s'} hs hmem hf hr := by + have hh : s'.mem h = s.mem h := hmem h (header_mem_cellFootprint h s) + have hroom : cellRoom h s' = cellRoom h s := by simp only [cellRoom, hh] + have hfp : cellFootprint h s' = cellFootprint h s := by simp only [cellFootprint, hh] + refine ⟨⟨(CellInv.header_lt hs).trans_le hf, fun h0 => ?_, hroom ▸ hr⟩, ?_, hfp, hroom⟩ + · rw [hh] at h0 ⊢ + exact (CellInv.value_lt hs h0).trans_le hf + · change cellAbs h s' = cellAbs h s + unfold cellAbs + by_cases h0 : s.mem h = 0 + · simp [hh, h0] + · have hv : s'.mem (s.mem h) = s.mem (s.mem h) := hmem _ (by simp [cellFootprint, h0]) + simp [hh, h0, hv] + modifies {s} o {a s' tr} hs hv hrun := by + obtain ⟨_, _, rfl, _⟩ := ((spec w).clientRun_iff_runClient _ _ _ _ _).mp hrun + simp only [cellRefinement_inv, cellRefinement_abs] at hs hv + have hh := CellInv.header_lt hs + cases o with + | set v => + obtain ⟨hnone, -⟩ := hv + have h0 := cellAbs_eq_none.mp hnone + have hne : s.frontier ≠ h := by omega + simp only [cellRefinement_methods, cellMethods, StateTransitionSpec.clientState, + runClient_cellSet] + refine ⟨fun b hb hb' => ?_, fun b hb => ?_, ?_⟩ + · have hbh : b ≠ h := fun e => hb' (e ▸ header_mem_cellFootprint h s) + change Function.update (Function.update s.mem s.frontier v) h s.frontier b = s.mem b + rw [Function.update_of_ne hbh, Function.update_of_ne (by omega)] + · have hf0 : s.frontier ≠ 0 := by omega + simp only [cellFootprint, Function.update_self, hf0, ↓reduceIte, Finset.mem_insert, + Finset.mem_singleton] at hb + rcases hb with hb | hb + · exact .inl (hb ▸ header_mem_cellFootprint h s) + · exact .inr hb.ge + · have hf0 : s.frontier ≠ 0 := by omega + simp [cellRoom, h0, hf0] + | get => + by_cases h0 : s.mem h = 0 + · simp only [cellRefinement_methods, cellMethods, StateTransitionSpec.clientState, + runClient_cellGet_of_eq_zero h w s h0] + exact ⟨fun _ _ _ => trivial, fun _ hb => .inl hb, le_rfl⟩ + · simp only [cellRefinement_methods, cellMethods, StateTransitionSpec.clientState, + runClient_cellGet_of_ne_zero h w s h0] + exact ⟨fun _ _ _ => trivial, fun _ hb => .inl hb, le_rfl⟩ + +/-- A set costs three memory units, a get at most two, with no potential. -/ +theorem cellAmortized : (cellFramed h w).Amortized charge (fun _ _ => 3) fun _ => 0 := by + apply Refinement.Amortized.ofFn + intro s o _ _ + change (spec w).totalCost charge s ((spec w).clientHistory s (cellMethods h w o)) + 0 ≤ 3 + 0 + cases o with + | set v => simp [cellMethods, StateTransitionSpec.clientHistory, runClient_cellSet] + | get => + by_cases h0 : s.mem h = 0 + · simp [cellMethods, StateTransitionSpec.clientHistory, runClient_cellGet_of_eq_zero h w s h0] + · simp [cellMethods, StateTransitionSpec.clientHistory, runClient_cellGet_of_ne_zero h w s h0] + +/-- **The write-once cell as an allocator**: allocate a zeroed header, and reserve one more cell +for the first `set`. -/ +def cellAllocator : Allocator (Cell w) w where + Handle := ℕ + alloc := alloc 1 + methods h := cellMethods h w + init := none + room := 2 + cost := 2 + framed s _ _ := cellFramed s.frontier w + allocates s _ hroom := by + have h0 : clear s.mem s.frontier 1 s.frontier = 0 := + clear_of_owns ⟨le_rfl, Nat.lt_add_one _⟩ + refine + { validHistory := ?_, cost_le := ?_, mem_eq := ?_, methods_eq := rfl, inv := ?_, + abs_eq := ?_, of_mem_footprint := ?_, room_le := ?_ } <;> + simp only [StateTransitionSpec.clientHistory, StateTransitionSpec.clientState, + runClient_alloc_one] + · exact ⟨hroom.of_le (by omega), trivial⟩ + · simp + · exact fun a ha => clear_of_not_owns (by simp only [Block.Owns]; omega) + · refine ⟨Nat.lt_add_one _, fun h => absurd h0 h, ?_⟩ + change Fits w (s.frontier + 1 + cellRoom s.frontier ⟨clear s.mem s.frontier 1, _⟩) + simp only [cellRoom, h0, ↓reduceIte] + exact hroom.of_le (by omega) + · change cellAbs s.frontier ⟨clear s.mem s.frontier 1, _⟩ = none + simp [cellAbs, h0] + · intro a ha + change a ∈ cellFootprint s.frontier ⟨clear s.mem s.frontier 1, _⟩ at ha + simp only [cellFootprint, h0, ↓reduceIte, Finset.mem_singleton] at ha + exact .inr ha.ge + · change s.frontier + 1 + cellRoom s.frontier ⟨clear s.mem s.frontier 1, _⟩ ≤ _ + simp only [cellRoom, h0, ↓reduceIte] + omega + +/-- Each cell costs three memory units per operation, and allocation leaves no potential. -/ +theorem cellAllocator_amortized : (cellAllocator w).Amortized (fun _ _ => 3) 0 := + fun s _ _ => ⟨fun _ => 0, cellAmortized s.frontier w, Framed.Local.const _ 0, le_rfl⟩ + +/-! ## Two cells that allocate inside their operations -/ + +/-- Two write-once cells in one memory, each with one cell of room for its `set`. -/ +abbrev cells : Allocator (product (Cell 8) (Cell 8)) 8 := (cellAllocator 8).prod (cellAllocator 8) + +/-- The two cells reserve four cells: two headers and two value cells. -/ +example : cells.room = 4 := rfl + +/-- Set the left cell. -/ +def setLeft : (product (Cell 8) (Cell 8)).Client Unit := + (product (Cell 8) (Cell 8)).call (.inl (.set 5)) + +/-- Set the left cell, then the right one. -/ +def setBoth : (product (Cell 8) (Cell 8)).Client Unit := do + let _ ← (product (Cell 8) (Cell 8)).call (.inl (.set 5)) + let _ ← (product (Cell 8) (Cell 8)).call (.inr (.set 7)) + pure () + +/-- Set both cells, then get both. -/ +def client : (product (Cell 8) (Cell 8)).Client (Option ℕ × Option ℕ) := do + setBoth + let a ← (product (Cell 8) (Cell 8)).call (.inl .get) + let b ← (product (Cell 8) (Cell 8)).call (.inr .get) + pure (a, b) + +/-- Memory with nothing allocated. -/ +def nothing : State := ⟨fun _ => 0, 0⟩ + +/-- Nothing is needed of the memory besides room. -/ +theorem pre (s : State) : cells.Pre s := ⟨trivial, trivial, Finset.disjoint_empty_left _⟩ + +/-- The four reserved cells fit a byte. -/ +theorem fits : Fits 8 (nothing.frontier + cells.room) := by decide + +/-- The two cells, as set up by the allocation from `nothing`. -/ +abbrev framed : Framed (product (Cell 8) (Cell 8)) 8 := cells.framed nothing (pre _) fits + +/-- The client is admissible: each cell is set once, to a byte. -/ +theorem client_valid : (product (Cell 8) (Cell 8)).ValidHistory cells.init + ((product (Cell 8) (Cell 8)).clientHistory cells.init client) := + ⟨⟨rfl, by decide⟩, ⟨rfl, by decide⟩, trivial, trivial, trivial⟩ + +/-- **The run is admissible and answers as the specification does**, by the generic theorem: no +proof about this client or this product. -/ +theorem run_correct : + (spec 8).ValidHistory nothing ((spec 8).clientHistory nothing (cells.run client)) ∧ + (spec 8).clientResult nothing (cells.run client) = (some 5, some 7) ∧ + framed.abs ((spec 8).clientState nothing (cells.run client)) = (some 5, some 7) := by + obtain ⟨hv, hres, -, habs⟩ := cells.run_correct client (pre _) fits client_valid + exact ⟨hv, hres, habs⟩ + +/-- The same answer, by evaluation. -/ +example : (spec 8).clientResult nothing (cells.run client) = (some 5, some 7) := by decide + +/-- **Each `set` allocates inside the operation**: the headers at `0` and `1`, then the left +value cell at `2` and the right one at `3`, above the other structure's newest cell. -/ +example : (spec 8).clientHistory nothing (cells.run client) = + [.alloc 1, .alloc 1, .allocDirty 1, .write 2 5, .write 0 2, .allocDirty 1, .write 3 7, + .write 1 3, .read 0, .read 2, .read 1, .read 3] := by + decide + +/-- After allocation: both headers, both cells unset, and two cells of room up to frontier +`4`. -/ +example : let s := (spec 8).clientState nothing (cells.run (pure ())) + framed.footprint s = {0, 1} ∧ framed.abs s = (none, none) ∧ s.frontier = 2 ∧ + framed.room s = 2 := by + decide + +/-- After the left `set`: the left cell has taken its reserved cell `2`, and the right cell keeps +its value, its footprint and its reservation. The frontier plus the room is still `4`. -/ +example : let s := (spec 8).clientState nothing (cells.run setLeft) + framed.footprint s = {0, 2, 1} ∧ framed.abs s = (some 5, none) ∧ s.frontier = 3 ∧ + framed.room s = 1 := by + decide + +/-- After both `set`s: all four reserved cells are used and no room is left. -/ +example : let s := (spec 8).clientState nothing (cells.run setBoth) + framed.footprint s = {0, 2, 1, 3} ∧ framed.abs s = (some 5, some 7) ∧ s.frontier = 4 ∧ + framed.room s = 0 := by + decide + +/-- It costs two units per header and three per `set` and two per `get`. -/ +example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing (cells.run client)) = + 14 := by + decide + +/-- The generic cost theorem: four units to allocate, and three per operation. -/ +example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing (cells.run client)) ≤ + 4 + 0 + 3 * 4 := by + have h := Allocator.Amortized.prod (cellAllocator_amortized (w := 8)) + (cellAllocator_amortized (w := 8)) + simp only [productCharge_const] at h + exact Allocator.run_cost_le h client (pre _) fits client_valid + +/-! ### The boundary of the reservation -/ + +/-- The highest frontier with room for both cells at word size `8`: `251 + 4 = 255`. -/ +def high : State := ⟨fun _ => 0, 251⟩ + +/-- One cell higher: `252 + 4 = 256` does not fit a byte. -/ +def tooHigh : State := ⟨fun _ => 0, 252⟩ + +example : Fits 8 (high.frontier + cells.room) ∧ ¬Fits 8 (tooHigh.frontier + cells.room) := by + decide + +/-- From the highest frontier with room, the generic theorem gives an admissible run. -/ +example : (spec 8).ValidHistory high ((spec 8).clientHistory high (cells.run client)) := + (cells.run_correct client (pre _) (by decide) client_valid).1 + +/-- One cell higher, the run is not admissible. -/ +example : ¬(spec 8).ValidHistory tooHigh ((spec 8).clientHistory tooHigh (cells.run client)) := by + decide + +/-- Only the right `set` fails, when it allocates the last of the four reserved cells: the +allocations and the left `set` are admissible. -/ +example : (spec 8).ValidHistory tooHigh ((spec 8).clientHistory tooHigh (cells.run setLeft)) ∧ + ¬(spec 8).ValidHistory tooHigh ((spec 8).clientHistory tooHigh (cells.run setBoth)) := by + decide + +/-! ## Stacks already in memory -/ + +/-- A bounded stack of capacity `2` at word size `8`. -/ +abbrev S : StateTransitionSpec := Stack.WordRAM.spec 8 2 + +/-- The layout of stack `i`: its count cell at `3 i`, its items at `3 i + 1` and `3 i + 2`. -/ +def layout (i : ℕ) : Stack.Memory.Layout := ⟨⟨3 * i, 1⟩, ⟨3 * i + 1, 2⟩⟩ + +/-- The layouts are well formed. -/ +theorem valid (i : ℕ) (hi : i < 3) : (layout i).Valid 8 2 := + Stack.Memory.valid_allocLayout (f := 3 * i) (Fits.of_le (show Fits 8 9 by decide) (by omega)) + +/-- The stack at layout `i`, taken over as it is in memory, holding `xs`. -/ +abbrev inputStack (i : ℕ) (hi : i < 3) (xs : List ℕ) : Allocator S 8 := + Allocator.resident (Stack.Memory.framed (valid i hi)) xs 0 + +/-- The input: stack `0` holds `7` on top of `5`, and stack `1` holds `4`. -/ +def input : State := + ⟨fun a => if a = 0 then 2 else if a = 1 then 5 else if a = 2 then 7 else if a = 3 then 1 + else if a = 4 then 4 else 0, 6⟩ + +/-- Stack `i` holds in the input. -/ +theorem inv_input (i : ℕ) (hi : i < 2) : Stack.Memory.Inv (layout i) input where + count_allocated := by simp only [Block.Allocated, layout, input]; omega + items_allocated := by simp only [Block.Allocated, layout, input]; omega + count_le := by + obtain _ | _ | i := i + · decide + · decide + · omega + +/-- **The Dijkstra shape**: two stacks allocated from scratch above the input, and the input's +stack `0` taken over in place, as Dijkstra's algorithm takes over its input graph. -/ +abbrev dijkstraShape : Allocator (product S (product S S)) 8 := + (Stack.Memory.allocator 8 2).prod ((Stack.Memory.allocator 8 2).prod (inputStack 0 (by decide) + [7, 5])) + +/-- The input holds the stack that the third structure takes over. -/ +theorem pre_dijkstraShape : dijkstraShape.Pre input := + ⟨trivial, ⟨trivial, ⟨inv_input 0 (by decide), by decide, le_rfl⟩, Finset.disjoint_empty_left _⟩, + Finset.disjoint_empty_left _⟩ + +/-- Pop the input's stack, push `1` on the first fresh stack and `2` on the second, then pop +all three. -/ +def dijkstraClient : (product S (product S S)).Client (List (Option ℕ)) := do + let a ← (product S (product S S)).call (.inr (.inr .pop)) + let _ ← (product S (product S S)).call (.inl (.push 1)) + let _ ← (product S (product S S)).call (.inr (.inl (.push 2))) + let b ← (product S (product S S)).call (.inl .pop) + let c ← (product S (product S S)).call (.inr (.inl .pop)) + let d ← (product S (product S S)).call (.inr (.inr .pop)) + pure [a, b, c, d] + +/-- The client is admissible from the allocated states. -/ +theorem dijkstraClient_valid : (product S (product S S)).ValidHistory dijkstraShape.init + ((product S (product S S)).clientHistory dijkstraShape.init dijkstraClient) := + ⟨trivial, ⟨by decide, by decide⟩, ⟨by decide, by decide⟩, trivial, trivial, trivial, trivial⟩ + +/-- **The generic correctness theorem**: the run answers as the specification does from the +input's stack and two empty ones. -/ +example : (spec 8).clientResult input (dijkstraShape.run dijkstraClient) = + [some 7, some 1, some 2, some 5] := by + obtain ⟨-, hres, -⟩ := + dijkstraShape.run_correct dijkstraClient pre_dijkstraShape (by decide) dijkstraClient_valid + rw [hres] + decide + +/-- The same answer, by evaluation. The fresh stacks take the six cells above the input, and +the input's stack is popped in place: its count cell, at address `0`, ends at `0`. -/ +example : (spec 8).clientResult input (dijkstraShape.run dijkstraClient) = + [some 7, some 1, some 2, some 5] ∧ + ((spec 8).clientState input (dijkstraShape.run dijkstraClient)).frontier = 12 ∧ + ((spec 8).clientState input (dijkstraShape.run dijkstraClient)).mem 0 = 0 := by + decide + +/-- The generic cost theorem: six units to allocate the fresh stacks, none for the input's, and +three per operation. -/ +example : (spec 8).totalCost charge input ((spec 8).clientHistory input + (dijkstraShape.run dijkstraClient)) ≤ 6 + 0 + 3 * 6 := by + have hres : (inputStack 0 (by decide) [7, 5]).Amortized (fun _ _ => 3) 0 := + Allocator.Amortized.resident (Stack.Memory.amortized _) (Framed.Local.const _ 0) + fun _ _ _ => le_rfl + have h := Allocator.Amortized.prod (Stack.Memory.amortized_allocator (w := 8) (C := 2)) + (Allocator.Amortized.prod (Stack.Memory.amortized_allocator (w := 8) (C := 2)) hres) + simp only [productCharge_const] at h + exact Allocator.run_cost_le h dijkstraClient pre_dijkstraShape (by decide) dijkstraClient_valid + +/-- Two stacks that the input holds in disjoint cells. -/ +abbrev bothInputs : Allocator (product S S) 8 := + (inputStack 0 (by decide) [7, 5]).prod (inputStack 1 (by decide) [4]) + +/-- Pop both input stacks twice. -/ +def popBoth : (product S S).Client (List (Option ℕ)) := do + let a ← (product S S).call (.inl .pop) + let b ← (product S S).call (.inr .pop) + let c ← (product S S).call (.inl .pop) + let d ← (product S S).call (.inr .pop) + pure [a, b, c, d] + +/-- Both stacks hold in the input, in disjoint cells. -/ +theorem pre_bothInputs : bothInputs.Pre input := + ⟨⟨inv_input 0 (by decide), by decide, le_rfl⟩, ⟨inv_input 1 (by decide), by decide, le_rfl⟩, + by decide⟩ + +/-- The generic correctness theorem for two structures taken over in place. -/ +example : (spec 8).clientResult input (bothInputs.run popBoth) = + [some 7, some 4, some 5, none] := by + obtain ⟨-, hres, -⟩ := bothInputs.run_correct popBoth pre_bothInputs (by decide) + ⟨trivial, trivial, trivial, trivial, trivial⟩ + rw [hres] + decide + +/-- **The same cells cannot be taken over twice**: the product's precondition asks for disjoint +cells, so no memory satisfies it for one stack taken over twice. -/ +example (s : State) : ¬((inputStack 0 (by decide) [7, 5]).prod + (inputStack 0 (by decide) [7, 5])).Pre s := fun hs => by + have hd : Disjoint (layout 0).footprint (layout 0).footprint := Allocator.ProdPre.disjoint hs + exact absurd hd (by decide) + +end ComplexityTests.DataStructureMemoryAlloc diff --git a/ComplexityTests/DataStructureMemoryProduct.lean b/ComplexityTests/DataStructureMemoryProduct.lean index f058b1a2af..2ad73fad46 100644 --- a/ComplexityTests/DataStructureMemoryProduct.lean +++ b/ComplexityTests/DataStructureMemoryProduct.lean @@ -160,6 +160,11 @@ example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing (allocated.run client')) = 9 + 17 := by decide +/-- The three stacks are allocated from scratch, so nothing is needed of the memory besides +room. -/ +theorem pre_allocated (s : State) : allocated.Pre s := + ⟨trivial, ⟨trivial, trivial, Finset.disjoint_empty_left _⟩, Finset.disjoint_empty_left _⟩ + /-- The client is admissible for the three bounded stacks. -/ theorem client'_valid : (product S (product S S)).ValidHistory allocated.init ((product S (product S S)).clientHistory allocated.init client') := @@ -178,13 +183,13 @@ theorem amortized_allocated : allocated.Amortized (fun _ _ => 3) 0 := by example : (spec 8).clientResult nothing (allocated.run client') = (product S (product S S)).clientResult allocated.init client' := have ⟨_, hres, _⟩ := - allocated.run_correct client' ⟨trivial, trivial, trivial⟩ (by decide) client'_valid + allocated.run_correct client' (pre_allocated _) (by decide) client'_valid hres /-- The generic cost theorem: nine units to allocate, and three per operation. -/ example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing (allocated.run client')) ≤ 9 + 0 + 3 * 7 := - Allocator.run_cost_le amortized_allocated client' ⟨trivial, trivial, trivial⟩ (by decide) + Allocator.run_cost_le amortized_allocated client' (pre_allocated _) (by decide) client'_valid end ComplexityTests.DataStructureMemoryProduct From 73ca9900d93fd98fa502c0075bdc0543fa7479fc Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 20:10:55 -0500 Subject: [PATCH 16/18] feat(DataStructure/Memory): allocations keep the memory beyond the frontier An admissible run changes no cell at or above its final frontier (`Memory.mem_run_of_frontier_le`, `mem_clientState_of_frontier_le`). `Allocator.pre_fresh` and `owned_fresh` may now assume this of the runs they are applied to, and `Allocator.prod` supplies it from the first allocation's admissibility. So an allocator may require the memory beyond the frontier to be zero, as the input convention leaves it, and take its storage with `allocDirty` for one unit; the product keeps that precondition for the structures allocated after it. The test's write-once cells now take their headers this way, and a test over a nonzero cell shows that the precondition is needed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity/DataStructure/Memory/Alloc.lean | 47 +++++----- Complexity/DataStructure/Memory/Frame.lean | 34 ++++++++ ComplexityTests/DataStructureMemoryAlloc.lean | 86 ++++++++++++------- 3 files changed, 113 insertions(+), 54 deletions(-) diff --git a/Complexity/DataStructure/Memory/Alloc.lean b/Complexity/DataStructure/Memory/Alloc.lean index 76b96f40c4..1d2d11b11c 100644 --- a/Complexity/DataStructure/Memory/Alloc.lean +++ b/Complexity/DataStructure/Memory/Alloc.lean @@ -47,11 +47,13 @@ Allocators combine as their structures do. `Memory.Framed.comp`. Certificates combine along with them (`Allocator.Amortized.prod`, `Allocator.Amortized.comp`, -`Allocator.Amortized.resident`). An allocation changes no allocated cell, so it keeps the other -structures' preconditions and the cells they take over (`Allocator.pre_fresh`, -`Allocator.owned_fresh`). The product's precondition (`Allocator.ProdPre`) asks in addition that -the cells the two structures take over be disjoint. So `A₁.prod (A₂.prod A₃)` may combine -structures allocated from scratch with structures that the input holds. +`Allocator.Amortized.resident`). An allocation changes no allocated cell, and being admissible, +no cell beyond its new frontier either (`Memory.mem_clientState_of_frontier_le`). So it keeps the +other structures' preconditions, such as that the memory beyond the frontier is zero, and the +cells they take over (`Allocator.pre_fresh`, `Allocator.owned_fresh`). The product's +precondition (`Allocator.ProdPre`) asks in addition that the cells the two structures take over +be disjoint. So `A₁.prod (A₂.prod A₃)` may combine structures allocated from scratch with +structures that the input holds. ## Main definitions @@ -133,14 +135,15 @@ structure Allocator (D : StateTransitionSpec.{0, 0, 0}) (w : ℕ) where owned : State → Finset ℕ := fun _ => ∅ /-- The cells taken over are allocated. -/ owned_lt : ∀ {s a}, Pre s → a ∈ owned s → a < s.frontier := by intros; simp_all - /-- Runs that keep every allocated cell and leave the room fitting, such as other allocations, - keep the precondition. -/ + /-- Runs that keep every allocated cell and every cell beyond the new frontier and leave the + room fitting, such as other allocations, keep the precondition. -/ pre_fresh : ∀ {s s' : State}, Pre s → (∀ a < s.frontier, s'.mem a = s.mem a) → - s.frontier ≤ s'.frontier → Fits w (s'.frontier + room) → Pre s' := by intros; trivial + (∀ a, s'.frontier ≤ a → s'.mem a = s.mem a) → s.frontier ≤ s'.frontier → + Fits w (s'.frontier + room) → Pre s' := by intros; trivial /-- Such runs keep the cells taken over. -/ owned_fresh : ∀ {s s' : State}, Pre s → (∀ a < s.frontier, s'.mem a = s.mem a) → - s.frontier ≤ s'.frontier → Fits w (s'.frontier + room) → owned s' = owned s := by - intros; rfl + (∀ a, s'.frontier ≤ a → s'.mem a = s.mem a) → s.frontier ≤ s'.frontier → + Fits w (s'.frontier + room) → owned s' = owned s := by intros; rfl /-- The framed refinement that allocating from `s` sets up. -/ framed : (s : State) → Pre s → Fits w (s.frontier + room) → Framed D w /-- Allocating from `s` sets it up. -/ @@ -350,14 +353,16 @@ theorem room_after {s : State} (hs₁ : A₁.Pre s) {r : ℕ} theorem pre_after {s : State} (hs₁ : A₁.Pre s) (hroom : Fits w (s.frontier + (A₁.room + A₂.room))) (hs₂ : A₂.Pre s) : A₂.Pre ((spec w).clientState s A₁.alloc) := - A₂.pre_fresh hs₂ (A₁.allocates s hs₁ (hroom.of_le (by omega))).mem_eq + have h₁ := A₁.allocates s hs₁ (hroom.of_le (by omega)) + A₂.pre_fresh hs₂ h₁.mem_eq (fun _ => mem_clientState_of_frontier_le s _ h₁.validHistory) (frontier_le_clientState s _) (room_after hs₁ hroom) /-- After allocating the first structure, the second one takes over the same cells. -/ theorem owned_after {s : State} (hs₁ : A₁.Pre s) (hroom : Fits w (s.frontier + (A₁.room + A₂.room))) (hs₂ : A₂.Pre s) : A₂.owned ((spec w).clientState s A₁.alloc) = A₂.owned s := - A₂.owned_fresh hs₂ (A₁.allocates s hs₁ (hroom.of_le (by omega))).mem_eq + have h₁ := A₁.allocates s hs₁ (hroom.of_le (by omega)) + A₂.owned_fresh hs₂ h₁.mem_eq (fun _ => mem_clientState_of_frontier_le s _ h₁.validHistory) (frontier_le_clientState s _) (room_after hs₁ hroom) variable (A₁) @@ -385,18 +390,18 @@ def prod : Allocator (product D₁ D₂) w where Pre := ProdPre A₁ A₂ owned s := A₁.owned s ∪ A₂.owned s owned_lt hs ha := (Finset.mem_union.mp ha).elim (A₁.owned_lt hs.left) (A₂.owned_lt hs.right) - pre_fresh hs hmem hf hr := + pre_fresh hs hmem habove hf hr := have hr₁ := hr.of_le (Nat.add_le_add_left (Nat.le_add_right A₁.room A₂.room) _) have hr₂ := hr.of_le (Nat.add_le_add_left (Nat.le_add_left A₂.room A₁.room) _) - { left := A₁.pre_fresh hs.left hmem hf hr₁ - right := A₂.pre_fresh hs.right hmem hf hr₂ + { left := A₁.pre_fresh hs.left hmem habove hf hr₁ + right := A₂.pre_fresh hs.right hmem habove hf hr₂ disjoint := by - rw [A₁.owned_fresh hs.left hmem hf hr₁, A₂.owned_fresh hs.right hmem hf hr₂] + rw [A₁.owned_fresh hs.left hmem habove hf hr₁, A₂.owned_fresh hs.right hmem habove hf hr₂] exact hs.disjoint } - owned_fresh hs hmem hf hr := by - rw [A₁.owned_fresh hs.left hmem hf + owned_fresh hs hmem habove hf hr := by + rw [A₁.owned_fresh hs.left hmem habove hf (hr.of_le (Nat.add_le_add_left (Nat.le_add_right A₁.room A₂.room) _)), - A₂.owned_fresh hs.right hmem hf + A₂.owned_fresh hs.right hmem habove hf (hr.of_le (Nat.add_le_add_left (Nat.le_add_left A₂.room A₁.room) _))] framed s hs hroom := (A₁.framed s hs.left (hroom.of_le (by omega))).prod @@ -524,11 +529,11 @@ def resident : Allocator D w where Pre s := M.inv s ∧ M.abs s = init ∧ M.room s ≤ room owned := M.footprint owned_lt hs ha := M.lt_frontier hs.1 ha - pre_fresh {s _} hs hmem hf hr := by + pre_fresh {s _} hs hmem _ hf hr := by obtain ⟨hinv, habs, hle⟩ := hs obtain ⟨hinv', habs', -, hroom'⟩ := M.of_fresh hinv hmem hf (hr.of_le (by omega)) exact ⟨hinv', habs'.trans habs, hroom' ▸ hle⟩ - owned_fresh {s _} hs hmem hf hr := by + owned_fresh {s _} hs hmem _ hf hr := by obtain ⟨hinv, -, hle⟩ := hs obtain ⟨-, -, hfp, -⟩ := M.of_fresh hinv hmem hf (hr.of_le (by omega)) exact hfp diff --git a/Complexity/DataStructure/Memory/Frame.lean b/Complexity/DataStructure/Memory/Frame.lean index 77c9e4598a..9992941dbc 100644 --- a/Complexity/DataStructure/Memory/Frame.lean +++ b/Complexity/DataStructure/Memory/Frame.lean @@ -66,6 +66,8 @@ structures allocated one after the other. `Memory.Allocator` packages this. frontier. * `Memory.mem_run_of_lt_frontier`, `Memory.mem_clientState_of_lt_frontier`: a run changes an allocated cell only by writing it. +* `Memory.mem_run_of_frontier_le`, `Memory.mem_clientState_of_frontier_le`: an admissible run + changes no cell at or above its final frontier. * `Memory.Framed.modifies_demonic`: a linked client keeps the frame of its calls. * `Memory.Framed.frame`: a method run of one structure leaves a structure with a disjoint footprint unchanged, and the shared room budget fitting. @@ -123,6 +125,38 @@ theorem mem_clientState_of_lt_frontier {α : Type} (s : State) (p : Prog α) {a rw [← (spec w).run_runClient] exact mem_run_of_lt_frontier s _ ha hw +/-- An admissible sequence of operations changes no cell at or above its final frontier: writes +go below the frontier, and an allocation clears only the block that it hands out. -/ +theorem mem_run_of_frontier_le {s : State} {os : List Op} (hv : (spec w).ValidHistory s os) + {a : ℕ} (ha : ((spec w).run s os).1.frontier ≤ a) : ((spec w).run s os).1.mem a = s.mem a := by + induction os generalizing s with + | nil => rfl + | cons o os ih => + obtain ⟨hvo, hvs⟩ := hv + change ((spec w).run (step s o).1 os).1.frontier ≤ a at ha + change ((spec w).run (step s o).1 os).1.mem a = s.mem a + rw [ih hvs ha] + have hf : (step s o).1.frontier ≤ a := (frontier_le_run _ os).trans ha + cases o with + | read => rfl + | write b v => + have hb : b < s.frontier := hvo.1 + simp only [step_write] at hf + have hab : a ≠ b := by omega + exact Function.update_of_ne hab v s.mem + | alloc k => + simp only [step_alloc] at hf + exact clear_of_not_owns (by simp only [Block.Owns]; omega) + | allocDirty => rfl + +/-- An admissible memory program changes no cell at or above its final frontier. -/ +theorem mem_clientState_of_frontier_le {α : Type} (s : State) (p : Prog α) + (hv : (spec w).ValidHistory s ((spec w).clientHistory s p)) {a : ℕ} + (ha : ((spec w).clientState s p).frontier ≤ a) : + ((spec w).clientState s p).mem a = s.mem a := by + rw [← (spec w).run_runClient] at ha ⊢ + exact mem_run_of_frontier_le hv ha + /-! ## Framed refinements -/ /-- A refinement of `D` over memory whose representation lives in a footprint of allocated cells, diff --git a/ComplexityTests/DataStructureMemoryAlloc.lean b/ComplexityTests/DataStructureMemoryAlloc.lean index 26680b3cc1..292ff6f507 100644 --- a/ComplexityTests/DataStructureMemoryAlloc.lean +++ b/ComplexityTests/DataStructureMemoryAlloc.lean @@ -15,9 +15,11 @@ public import Complexity.DataStructure.Stack.Memory Two checks of the allocator contract of `Memory.Allocator`, at word size `8`. **Room consumed inside operations.** A write-once cell reserves one cell of room when it is -allocated, and allocates that cell inside its first `set`. Two such cells share one memory as -`cells`, the product of their allocators. A client sets them alternately, so each `set` -allocates above the other structure's newest cell. The tests check: +allocated, and allocates that cell inside its first `set`. Its header is taken without clearing, +so its allocator needs the memory beyond the frontier to be zero, which the other cell's +allocation keeps. Two such cells share one memory as `cells`, the product of their allocators. +A client sets them alternately, so each `set` allocates above the other structure's newest cell. +The tests check: * the answers and the admissibility of the whole run, by the generic `Allocator.run_correct`; * the allocations inside the operations, in the run's memory history; @@ -25,7 +27,8 @@ allocates above the other structure's newest cell. The tests check: which stays at the frontier the allocation reserved up to; * the boundary: from frontier `251`, where `251 + 4` fits a byte, the run is admissible. From `252` it is not, and only the right `set` fails: its allocation, of the last of the four - reserved cells, would leave the frontier at `256`. + reserved cells, would leave the frontier at `256`; +* that the precondition is needed: over a nonzero cell, an unset cell reads as set. **Structures already in memory.** A stack that the input holds is taken over by `Allocator.resident`, in the shape of Dijkstra's algorithm: two stacks are allocated from @@ -139,10 +142,10 @@ theorem runClient_cellGet_of_ne_zero (s : State) (h0 : s.mem h ≠ 0) : ↓reduceIte] rfl -/-- Allocating a header clears one cell at the frontier. -/ -theorem runClient_alloc_one (s : State) : - (spec w).runClient s (alloc 1) = - (s.frontier, ⟨clear s.mem s.frontier 1, s.frontier + 1⟩, [.alloc 1]) := rfl +/-- Allocating a header takes one cell at the frontier, without clearing it. -/ +theorem runClient_allocDirty_one (s : State) : + (spec w).runClient s (allocDirty 1) = (s.frontier, ⟨s.mem, s.frontier + 1⟩, [.allocDirty 1]) := + rfl variable {h w} @@ -292,38 +295,40 @@ theorem cellAmortized : (cellFramed h w).Amortized charge (fun _ _ => 3) fun _ = · simp [cellMethods, StateTransitionSpec.clientHistory, runClient_cellGet_of_eq_zero h w s h0] · simp [cellMethods, StateTransitionSpec.clientHistory, runClient_cellGet_of_ne_zero h w s h0] -/-- **The write-once cell as an allocator**: allocate a zeroed header, and reserve one more cell -for the first `set`. -/ +/-- **The write-once cell as an allocator**: take a header at the frontier without clearing it, +since the memory beyond the frontier is zero, and reserve one more cell for the first `set`. +Another structure's allocation keeps the memory beyond the new frontier zero. -/ def cellAllocator : Allocator (Cell w) w where Handle := ℕ - alloc := alloc 1 + alloc := allocDirty 1 methods h := cellMethods h w init := none room := 2 - cost := 2 + cost := 1 + Pre s := ∀ a, s.frontier ≤ a → s.mem a = 0 + pre_fresh hs _ habove hf _ a ha := (habove a ha).trans (hs a (hf.trans ha)) framed s _ _ := cellFramed s.frontier w - allocates s _ hroom := by - have h0 : clear s.mem s.frontier 1 s.frontier = 0 := - clear_of_owns ⟨le_rfl, Nat.lt_add_one _⟩ + allocates s hs hroom := by + have h0 : s.mem s.frontier = 0 := hs _ le_rfl refine { validHistory := ?_, cost_le := ?_, mem_eq := ?_, methods_eq := rfl, inv := ?_, abs_eq := ?_, of_mem_footprint := ?_, room_le := ?_ } <;> simp only [StateTransitionSpec.clientHistory, StateTransitionSpec.clientState, - runClient_alloc_one] + runClient_allocDirty_one] · exact ⟨hroom.of_le (by omega), trivial⟩ · simp - · exact fun a ha => clear_of_not_owns (by simp only [Block.Owns]; omega) + · exact fun _ _ => trivial · refine ⟨Nat.lt_add_one _, fun h => absurd h0 h, ?_⟩ - change Fits w (s.frontier + 1 + cellRoom s.frontier ⟨clear s.mem s.frontier 1, _⟩) + change Fits w (s.frontier + 1 + cellRoom s.frontier ⟨s.mem, _⟩) simp only [cellRoom, h0, ↓reduceIte] exact hroom.of_le (by omega) - · change cellAbs s.frontier ⟨clear s.mem s.frontier 1, _⟩ = none + · change cellAbs s.frontier ⟨s.mem, _⟩ = none simp [cellAbs, h0] · intro a ha - change a ∈ cellFootprint s.frontier ⟨clear s.mem s.frontier 1, _⟩ at ha + change a ∈ cellFootprint s.frontier ⟨s.mem, _⟩ at ha simp only [cellFootprint, h0, ↓reduceIte, Finset.mem_singleton] at ha exact .inr ha.ge - · change s.frontier + 1 + cellRoom s.frontier ⟨clear s.mem s.frontier 1, _⟩ ≤ _ + · change s.frontier + 1 + cellRoom s.frontier ⟨s.mem, _⟩ ≤ _ simp only [cellRoom, h0, ↓reduceIte] omega @@ -359,14 +364,17 @@ def client : (product (Cell 8) (Cell 8)).Client (Option ℕ × Option ℕ) := do /-- Memory with nothing allocated. -/ def nothing : State := ⟨fun _ => 0, 0⟩ -/-- Nothing is needed of the memory besides room. -/ -theorem pre (s : State) : cells.Pre s := ⟨trivial, trivial, Finset.disjoint_empty_left _⟩ +/-- Besides room, the two cells need the memory beyond the frontier to be zero, as the input +convention leaves it. The second cell's need survives the first cell's allocation. -/ +theorem pre {s : State} (hs : ∀ a, s.frontier ≤ a → s.mem a = 0) : cells.Pre s := + ⟨hs, hs, Finset.disjoint_empty_left _⟩ /-- The four reserved cells fit a byte. -/ theorem fits : Fits 8 (nothing.frontier + cells.room) := by decide /-- The two cells, as set up by the allocation from `nothing`. -/ -abbrev framed : Framed (product (Cell 8) (Cell 8)) 8 := cells.framed nothing (pre _) fits +abbrev framed : Framed (product (Cell 8) (Cell 8)) 8 := + cells.framed nothing (pre fun _ _ => rfl) fits /-- The client is admissible: each cell is set once, to a byte. -/ theorem client_valid : (product (Cell 8) (Cell 8)).ValidHistory cells.init @@ -379,7 +387,7 @@ theorem run_correct : (spec 8).ValidHistory nothing ((spec 8).clientHistory nothing (cells.run client)) ∧ (spec 8).clientResult nothing (cells.run client) = (some 5, some 7) ∧ framed.abs ((spec 8).clientState nothing (cells.run client)) = (some 5, some 7) := by - obtain ⟨hv, hres, -, habs⟩ := cells.run_correct client (pre _) fits client_valid + obtain ⟨hv, hres, -, habs⟩ := cells.run_correct client (pre fun _ _ => rfl) fits client_valid exact ⟨hv, hres, habs⟩ /-- The same answer, by evaluation. -/ @@ -388,8 +396,8 @@ example : (spec 8).clientResult nothing (cells.run client) = (some 5, some 7) := /-- **Each `set` allocates inside the operation**: the headers at `0` and `1`, then the left value cell at `2` and the right one at `3`, above the other structure's newest cell. -/ example : (spec 8).clientHistory nothing (cells.run client) = - [.alloc 1, .alloc 1, .allocDirty 1, .write 2 5, .write 0 2, .allocDirty 1, .write 3 7, - .write 1 3, .read 0, .read 2, .read 1, .read 3] := by + [.allocDirty 1, .allocDirty 1, .allocDirty 1, .write 2 5, .write 0 2, .allocDirty 1, + .write 3 7, .write 1 3, .read 0, .read 2, .read 1, .read 3] := by decide /-- After allocation: both headers, both cells unset, and two cells of room up to frontier @@ -412,18 +420,30 @@ example : let s := (spec 8).clientState nothing (cells.run setBoth) framed.room s = 0 := by decide -/-- It costs two units per header and three per `set` and two per `get`. -/ +/-- It costs one unit per header, three per `set` and two per `get`. -/ example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing (cells.run client)) = - 14 := by + 12 := by decide -/-- The generic cost theorem: four units to allocate, and three per operation. -/ +/-- The generic cost theorem: two units to allocate, and three per operation. -/ example : (spec 8).totalCost charge nothing ((spec 8).clientHistory nothing (cells.run client)) ≤ - 4 + 0 + 3 * 4 := by + 2 + 0 + 3 * 4 := by have h := Allocator.Amortized.prod (cellAllocator_amortized (w := 8)) (cellAllocator_amortized (w := 8)) simp only [productCharge_const] at h - exact Allocator.run_cost_le h client (pre _) fits client_valid + exact Allocator.run_cost_le h client (pre fun _ _ => rfl) fits client_valid + +/-- Get the left cell. -/ +def getLeft : (product (Cell 8) (Cell 8)).Client (Option ℕ) := + (product (Cell 8) (Cell 8)).call (.inl .get) + +/-- A memory with a nonzero cell beyond the frontier, where the left header will be taken. -/ +def dirty : State := ⟨fun a => if a = 0 then 9 else 0, 0⟩ + +/-- **The precondition is needed**: from `dirty` it fails, and the left cell, never set, reads as +set, since its header was taken without clearing. -/ +example : ¬cells.Pre dirty ∧ (spec 8).clientResult dirty (cells.run getLeft) = some 0 := by + refine ⟨fun hs => absurd (Allocator.ProdPre.left hs 0 le_rfl) (by decide), by decide⟩ /-! ### The boundary of the reservation -/ @@ -438,7 +458,7 @@ example : Fits 8 (high.frontier + cells.room) ∧ ¬Fits 8 (tooHigh.frontier + c /-- From the highest frontier with room, the generic theorem gives an admissible run. -/ example : (spec 8).ValidHistory high ((spec 8).clientHistory high (cells.run client)) := - (cells.run_correct client (pre _) (by decide) client_valid).1 + (cells.run_correct client (pre fun _ _ => rfl) (by decide) client_valid).1 /-- One cell higher, the run is not admissible. -/ example : ¬(spec 8).ValidHistory tooHigh ((spec 8).clientHistory tooHigh (cells.run client)) := by From 0fe737373ba3038b05b1a452f71e3fca724703b5 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 22:40:41 -0500 Subject: [PATCH 17/18] fix(DataStructure/PriorityQueue): the binary heap's frame keeps the frontier (Framed.ofStatic) Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- .../DataStructure/PriorityQueue/BinaryHeap/Memory.lean | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Complexity/DataStructure/PriorityQueue/BinaryHeap/Memory.lean b/Complexity/DataStructure/PriorityQueue/BinaryHeap/Memory.lean index 0cb6b26d3e..82a58259e1 100644 --- a/Complexity/DataStructure/PriorityQueue/BinaryHeap/Memory.lean +++ b/Complexity/DataStructure/PriorityQueue/BinaryHeap/Memory.lean @@ -722,6 +722,13 @@ theorem methods_frame (hL : L.Valid w K) {s : Memory.State} (hs : Inv L s) obtain ⟨-, -, -, -, -, h⟩ := methods_run hL hs o hv exact h +/-- An operation leaves the frontier where it was. -/ +theorem methods_frontier (hL : L.Valid w K) {s : Memory.State} (hs : Inv L s) + (o : IndexedMinPQ.Op n) (hv : IndexedMinPQ.valid K (abs L s) o) : + ((Memory.spec w).clientState s (ops L o)).frontier = s.frontier := by + obtain ⟨-, -, -, -, h, -⟩ := methods_run hL hs o hv + exact h + /-! ## The refinement and its frame -/ /-- The binary heap implements the indexed min-priority queue with keys at most `K`. -/ @@ -764,6 +771,7 @@ def framed (hL : L.Valid w K) : Memory.Framed (indexedMinPQSpec n K) w := .ofStatic (refinement hL) L.footprint (fun hs ha => hs.lt_frontier hL ha) (fun hs h hf => ⟨hs.congr hL h hf, abs_congr h⟩) (fun o hs hv b _ hb => methods_frame hL hs o hv b hb) + (fun o hs hv => methods_frontier hL hs o hv) /-- The footprint of a heap is its layout's cells. -/ @[simp] theorem framed_footprint (hL : L.Valid w K) (s : Memory.State) : From fe2039b31cd03b7ce02a3d546a1d8620c8277177 Mon Sep 17 00:00:00 2001 From: Samuel Giuseppe Schlesinger Date: Sun, 1 Nov 2026 23:01:12 -0500 Subject: [PATCH 18/18] fix(DataStructure): migrate the landed memory clients to Framed.ofStatic's frontier_eq and Apart The binary heap (#2237), the adjacency arrays and the direct-address table (#2238) keep the frontier, so each supplies Framed.ofStatic's frontier_eq from its existing run lemma. Dijkstra.Memory.inv_input now assumes that the frontier after allocation fits a word, which the product invariant Memory.Framed.Apart requires; run_triple supplies it from its fitting bound. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015DLg8kwPs8yi7ePPdtoNvG --- Complexity/Algorithms/Lean/Dijkstra/Memory.lean | 13 +++++++++---- Complexity/DataStructure/AdjacencyList/Memory.lean | 4 ++++ Complexity/DataStructure/Dictionary/Memory.lean | 3 +++ 3 files changed, 16 insertions(+), 4 deletions(-) diff --git a/Complexity/Algorithms/Lean/Dijkstra/Memory.lean b/Complexity/Algorithms/Lean/Dijkstra/Memory.lean index 62e06022a5..8a9081023f 100644 --- a/Complexity/Algorithms/Lean/Dijkstra/Memory.lean +++ b/Complexity/Algorithms/Lean/Dijkstra/Memory.lean @@ -176,12 +176,14 @@ theorem holds_inputGraph (G : ListInstance) : /-- **The structures after allocation.** Past the input of `G`, a distance table and then a heap, allocated without clearing, together with the adjacency arrays of the input, represent the initial state of Dijkstra's algorithm on `G`: an empty queue, an empty distance map and the -graph. -/ +graph. The structures share memory only while its frontier fits a word (`Memory.Framed.Apart`). -/ theorem inv_input (G : ListInstance) {w K V : ℕ} (hH : (BinaryHeap.Memory.allocated G.n ((Memory.State.init G.encode).frontier + G.n)).Valid w K) (hT : Dictionary.Memory.Valid ⟨(Memory.State.init G.encode).frontier, G.n⟩ w G.n V) - (hA : (inputGraph G.n G.m).Valid G.n) : + (hA : (inputGraph G.n G.m).Valid G.n) + (hfit : Fits w ((Memory.State.init G.encode).frontier + G.n + 1 + + PriorityQueue.WordRAM.BinaryHeap.space G.n)) : (framed hH hT hA G.adj).inv ⟨(Memory.State.init G.encode).mem, (Memory.State.init G.encode).frontier + G.n + 1 + PriorityQueue.WordRAM.BinaryHeap.space G.n⟩ ∧ @@ -202,13 +204,15 @@ theorem inv_input (G : ListInstance) {w K V : ℕ} ⟨by simp only [inputGraph, Block.Allocated]; omega, by simp only [inputGraph, Block.Allocated]; omega, holds_inputGraph G⟩, ?_⟩, ?_⟩, Prod.ext hHabs (Prod.ext hTabs rfl)⟩ - · simp only [Dictionary.Memory.framed_footprint, AdjacencyList.Memory.framed_footprint] + · refine ⟨?_, hfit⟩ + simp only [Dictionary.Memory.framed_footprint, AdjacencyList.Memory.framed_footprint] rw [Finset.disjoint_left] intro a ha hb simp only [Block.mem_footprint, Block.Owns, AdjacencyList.Memory.Layout.footprint, inputGraph, Finset.mem_union] at ha hb omega - · simp only [Memory.Framed.prod_footprint, BinaryHeap.Memory.framed_footprint, + · refine ⟨?_, hfit⟩ + simp only [Memory.Framed.prod_footprint, BinaryHeap.Memory.framed_footprint, Dictionary.Memory.framed_footprint, AdjacencyList.Memory.framed_footprint] rw [Finset.disjoint_left] intro a ha hb @@ -300,6 +304,7 @@ theorem run_triple (G : ListInstance) {w : ℕ} have hA : (inputGraph G.n G.m).Valid G.n := ⟨rfl⟩ set M := framed hH hT hA G.adj with hM obtain ⟨hinv, habs⟩ := inv_input G hH hT hA + (hfit.of_le (by simp only [PriorityQueue.WordRAM.BinaryHeap.space]; nlinarith)) obtain ⟨hvalid, hdist⟩ := dijkstra_correct (K := K) (V := K + 1) G le_rfl (by omega) rw [← habs] at hvalid hdist obtain ⟨hvE, -, hstate, -⟩ := M.simulates_client (dijkstra G.source) hinv hvalid diff --git a/Complexity/DataStructure/AdjacencyList/Memory.lean b/Complexity/DataStructure/AdjacencyList/Memory.lean index 04c3ae9a8f..f87b81bddc 100644 --- a/Complexity/DataStructure/AdjacencyList/Memory.lean +++ b/Complexity/DataStructure/AdjacencyList/Memory.lean @@ -210,6 +210,10 @@ def framed (hL : L.Valid n) : Memory.Framed (AdjacencyList.spec n) w := obtain ⟨-, -, -, hs'⟩ := ops_run (w := w) hL hs o hv change ((Memory.spec w).clientState _ (ops L o)).mem b = _ rw [hs']) + (fun o hs hv => by + obtain ⟨-, -, -, hs'⟩ := ops_run (w := w) hL hs o hv + change ((Memory.spec w).clientState _ (ops L o)).frontier = _ + rw [hs']) /-- The footprint of the arrays is their two fields. -/ @[simp] theorem framed_footprint (hL : L.Valid n) (s : Memory.State) : diff --git a/Complexity/DataStructure/Dictionary/Memory.lean b/Complexity/DataStructure/Dictionary/Memory.lean index 433b139bc6..9d175ebd81 100644 --- a/Complexity/DataStructure/Dictionary/Memory.lean +++ b/Complexity/DataStructure/Dictionary/Memory.lean @@ -164,6 +164,9 @@ def framed (hT : Valid T w U V) : Memory.Framed (Dictionary.spec U V n_max) w := (fun o hs hv b _ hb => by obtain ⟨-, -, -, -, -, h⟩ := ops_run hT hs o hv exact h b hb) + (fun o hs hv => by + obtain ⟨-, -, -, -, h, -⟩ := ops_run hT hs o hv + exact h) /-- The footprint of a table is its field. -/ @[simp] theorem framed_footprint (hT : Valid T w U V) (s : Memory.State) :