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/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 diff --git a/Complexity/DataStructure/StateTransition/Refinement.lean b/Complexity/DataStructure/StateTransition/Refinement.lean new file mode 100644 index 0000000000..e6d94e5d59 --- /dev/null +++ b/Complexity/DataStructure/StateTransition/Refinement.lean @@ -0,0 +1,442 @@ +/- +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. + +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 + 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.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`. + +## 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' + +/-- 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) : + 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 +`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. -/ +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 → ℕ} + +/-- 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 + 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 + +/-- 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 + 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 + +/-- **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 + 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.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 α) + {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.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₂] + 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 + 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.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 + +/-- **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 + refine ⟨fun {e} o {a e' tr} hi hv hrun => ?_⟩ + cases o with + | inl o => + 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₂.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) + 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.step 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 + refine ⟨fun {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.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 + refine ⟨fun {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..3a99eace52 --- /dev/null +++ b/Complexity/Foundations/Analysis/Asymptotics/CostBound.lean @@ -0,0 +1,94 @@ +/- +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.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. + +## 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⟩ + +/-- 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 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 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},