Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

For a Few Dollars More

(authored by agents unless marked 🧑) Tag: haslbeck2021dollars, paper

scope and result

Haslbeck and Lammich present an Isabelle/HOL refinement framework that proves functional correctness and worst-case resource bounds together, down to generated LLVM. The case study is introsort: the extracted sorter is functionally correct, has worst-case (O(n\log n)) complexity, and has measured time comparable to GNU libstdc++. The paper’s abstract states: “We present a framework to verify both, functional correctness and worst-case complexity of practically efficient algorithms.” (Abstract, p. 292, PDF p. 1).

technique 1: resource-aware nondeterministic specifications

What it is. NREST is a nondeterministic-result monad whose result map associates each possible result with a resource bound. A computation can be fail (possibly nontermination) or res M; when several paths produce one result, their costs are combined by a supremum. A resource type must provide a complete lattice and a monoid, so costs can be ordered and added. The paper says: “We require the resources γ to have a complete lattice structure, such that we can form suprema over the (possibly infinitely many) paths that lead to the same result.” (Section 2.1, p. 294, PDF p. 3).

When to use it (Inference). Use NREST for an abstract algorithm with many legal outputs and a bound that should be independent of implementation details. Encode a sorting specification as any permutation that is sorted, with a symbolic currency for its cost.

How to apply it. Define spec Ω T as a computation returning results satisfying predicate Ω, charging resource function T; use return, bind, assert, and resource-aware ifc, recc, and whilec. Give each abstract operation a currency. Example 1 specifies a quadratic-time sort with $q |xs|^2 + $c; later refinement exchanges these abstract coins for concrete operation coins. The paper warns: “Abstract currencies like q and c only ‘have value’ if they can be exchanged to meaningful other currencies, and finally pay for the resource costs of a concrete implementation.” (Section 2.1, p. 295, PDF p. 4).

Verus form (Inference): represent the abstract result relation with a spec fn, and put the resource bound in an ensures clause over a ghost cost value or cost map. Use executable Rust only for the algorithm; use proof functions and lemmas to establish result predicates and arithmetic bounds. Verus does not provide this NREST monad or Isabelle’s lattice automation, so the developer must define the result/cost representation and prove supremum or aggregation lemmas. ensures belongs on executable or proof functions, not on a spec fn.

Evidence and limits. The evidence is the formal refinement theorems, not a runtime measurement of NREST. NREST’s supremum can be unattained: the paper’s Example 2 has infinitely many paths to one result, so the aggregate is infinite even though no path takes infinite cost (Section 2.2, p. 296, PDF p. 5). This is an assumption users must understand when composing resource bounds.

technique 2: currency refinement and lockstep decomposition

What it is. Currency refinement maps each abstract coin to a finite combination of concrete coins. For example, one abstract comparison coin becomes two list-lookups and one less-than operation. The exchange rate E :: ηa → ηc → Îł says, for each abstract currency, how many coins of each concrete currency it is worth; the refined cost of a concrete coin is the sum over abstract coins of (abstract amount times exchange rate). The paper gives the rule: “one abstract coin may be exchanged into multiple coins of different currencies.” (Section 2.3, p. 297, PDF p. 6; it reads “Note that, in general, one abstract coin may be exchanged into multiple coins of different currencies.”).

When to use it (Inference). Introduce a currency at every algorithmic boundary whose implementation is not yet fixed (sortc, partitionc, lookup, and so on). Refine the algorithmic proof first, then instantiate each abstract currency with the cost of its implementation.

How to apply it. (1) Write the abstract operation and its symbolic currency. (2) Write the concrete implementation with its own costs. (3) Define exchange rate E; in Example 5, idxs cmp maps to two $lookup and one $less. (4) Prove the concrete computation is below the exchanged abstract computation. (5) Compose such lemmas by lockstep refinement: traverse structurally equal bind, ifc, and recc programs and discharge generated preconditions. The paper notes that the exchange rate must be the same for all components and is currently aligned by manually deriving specialized refinement lemmas (Section 2.4, p. 299, PDF p. 8).

Verus form (Inference): use a ghost cost struct or spec fn mapping operation tags to natural counts. Each implementation lemma has ensures that its cost is bounded by the exchanged abstract budget; composition is a proof lemma over sequence or tuple costs. A Verus proof can encode lockstep recursion and arithmetic, but an automatic Sepref-style synthesizer is outside ordinary Verus and would need a separate generator.

Evidence and limits. Currency refinement lets the resource analysis be structured along the refinement chain (Section 5.3, “Refining Resources”, p. 312). The authors explicitly leave automatic exchange-rate unification (collecting constraints during lockstep refinement) as future work (Section 2.4, p. 299, PDF p. 8). Amortized data structures are supported through time credits in hnr (Section 4.1), not through currency refinement. The hnr predicate later cannot generally perform currency refinement, so basic operations use LLVM currencies and are wrapped in costed data-structure specifications (Section 4.2, p. 307, PDF p. 16).

technique 3: LLVM instruction cost semantics with separation logic credits

What it is. The LLVM model is a deterministic state monad returning result, cost, and state. Each LLVM instruction consumes a cost token; costs are maps from instruction names to natural numbers. For example ll_load consumes $load before invoking raw memory load (Section 3.2, pp. 300–301, PDF pp. 9–10). The paper states: “As a cost model for running time, we chose to count how often each instruction is executed.” (Section 3.3, p. 302, PDF p. 11).

When to use it (Inference). Use this level when the theorem must cover generated low-level code and pointer/memory operations. Define the model before proving the algorithm, then attach one cost to each instruction and function call.

How to apply it. (1) Define a cost monoid, state, and consume. (2) Layer shallow LLVM instructions over a raw memory model. (3) Add instruction costs at the top layer while reusing raw-memory proofs. (4) Define concrete state as memory plus available credits. (5) Prove primitive Hoare triples, then let the VCG compose them. (6) Treat malloc as proportional to allocated elements; leave tuple construction free because the authors expect register allocation. The weakest-precondition condition requires actual cost to be no greater than available credit and subtracts it from the post-state (Section 3.4, pp. 302–304, PDF pp. 11–13; Section 3.3, p. 302, for malloc and tuples).

Verus form (Inference): model credits as a ghost natural or map carried through contracts, and make each counted wrapper consume the corresponding ghost amount. Use requires for available budget, ensures for remaining budget and memory facts, and loop invariant clauses for repeated instructions. Verus can prove a source-level event-count bound; it does not automatically prove that LLVM optimization, cache misses, pipeline effects, or malloc implementation costs follow the abstract model.

Evidence and limits. The paper says that, without factoring in optimization passes and target architecture, “our cost model can, at best, be a rough approximation of the actual running time” (Section 3.3, p. 302, PDF p. 11). It argues that, assuming one LLVM instruction becomes at most a small constant number of machine instructions each with constant worst-case time, “the steps counted by our model linearly correlate to an upper bound of the actual execution time”, so the model “cannot be used for precise statements about execution time” but “can be used to prove worst-case complexity”. Inference: the proved statement is an LLVM instruction-count theorem, not a hardware-time theorem.

technique 4: heap nondeterminism refinement and Sepref synthesis

What it is. The hnr predicate connects an abstract NREST computation to a concrete heap program, preserving a refinement assertion and paying concrete costs with abstract advertised costs plus amortized credits. Sepref symbolically executes the abstract program, maintains representation relations, emits LLVM code, and emits an hnr proof.

How to apply it. (1) Give each abstract value a heap representation assertion. (2) Supply synthesis rules for bind, recursion, conditionals, loops, and atomic operations. (3) For bind, synthesize the first component, then the second, then free the intermediate representation; MK FREE states that free is a deallocator for the representation, and free can only use time credits stored in that representation, typically paid for at creation, which hides the cost of freeing in the abstract program (Section 4.2, p. 306, PDF p. 15). (4) Supply concrete rules such as array lookup (array_nth), whose cost is array_getcost = $ofs_ptr + $load (Section 4.2, p. 307). (5) Extract a final Hoare triple when the abstract cost does not depend on the result.

The paper’s rule is explicit: “The Sepref tool [14,15] automatically synthesizes a concrete program in the LLVM-monad from an abstract algorithm in the NREST-monad.” (Section 4.2, p. 306, PDF p. 15). Verus form (Inference): write representation predicates as spec fns over Rust values and ghost heap state, prove each operation’s representation-preservation lemma, and keep deallocation obligations in ownership/permission proofs. A separate code generator would be needed to synthesize Rust from abstract specs; Verus itself checks the supplied Rust.

Evidence and limits. hnr can discard time credits but must explicitly free memory (Section 4.1, p. 306, PDF p. 15). Composition has a supremum problem when a data relation is non-single-valued; the authors write that they have “not yet found a way to define hnr or ⇓D in a form that does not exhibit this effect” and instead explicitly require that the supremum of the data refinement has a witness (attains_sup). It “trivially holds” if the relation is single-valued or the abstract program is one-time (same bound for all results), and the authors say the non-single-valued relations they met occurred only as intermediate results (Section 4.4, pp. 307–308, PDF pp. 16–17). Inference: this is a concrete limitation on generic composition.

technique 5: introsort proof by phased refinement

The algorithm is refined from a slice-sorting specification into two phases (Sections 5.1–5.2, pp. 308–309). First, quicksort-style partitioning recurses on slices longer than the threshold τ and, when the depth limit reaches 0, sorts the slice with a fallback sorting algorithm (heapsort); second, one final insertion sort pass finishes the almost-sorted list. The recursion cost introsort_reccost ÎŒ (n,d) is $sortc (ÎŒ n) + $partitionc d * n plus an overhead of (d+1)*n times a constant set of control-flow coins (Section 5.3, p. 310). The authors prove the bound by induction over recursion: “The correctness of the running time bound is proved by induction over the recursion of introsort rec.” (Section 5.3, p. 310, PDF p. 19). Choose ÎŒ(n)=n log n, require superadditivity, and set depth (2\lfloor\log n\rfloor); the paper states that introsort_reccost ÎŒ_nlogn is in O(n log n) for such d and that any d in O(log n) would do (Section 5.3, p. 311). The final insertion pass is (O(τn)=O(n)) for constant threshold (τ) (Section 5.2, p. 309).

Steps. (1) Prove the slice permutation/sortedness specification. (2) Define abstract almost-sort and final-sort phases. (3) Prove the recursive cost recurrence and depth computation. (4) Refine fallback to heapsort and partition to Hoare median-of-three partition. (5) Refine final insertion sort, including the unguarded optimization justified by the almost-sorted invariant. (6) Separate functional and resource proofs for the optimized sift_down2 and combine the two statements (Section 5.5, p. 313). (7) Instantiate arrays, synthesize introsort_impl with Sepref, convert to a Hoare triple, sum all currencies, then instantiate elements to 64-bit unsigned integers with icmp ult (Section 5.6, pp. 313–314).

The concrete total is 4693 + 5 * log n + 231 * n + 455 * (n * log n) (Section 5.6, p. 314, PDF p. 23), which the paper says is “routinely proved to be in O(n log n)”. Verus form (Inference): encode sortedness/permutation and slice bounds as predicates, use recursive proof lemmas with decreases, and maintain loop invariants for partition, heap, and insertion loops. A resource ghost variable can mirror the recurrence; the LLVM-specific refinement and automatic synthesis do not carry over directly.

Evidence. Figure 1 (p. 315) sorts arrays of (10^8) uint64s with various distributions, displays the smallest time of 10 runs, compiles with clang-10 -O3, and runs on an Intel XEON E5-2699 with 128GiB RAM and 256K/55M L2/L3 cache. The three implementations (new Isabelle-LLVM, earlier no-time extraction, and libstdc++) have similar times; Section 5.7 (p. 314) says “the small differences are well within the noise of the measurements”. The paper reports no confidence interval and defers the benchmarking method to [17]. I did not read numeric values off the figure.

what is proved, measured, and trusted

Proved: refinement from abstract sorting to LLVM, functional sorting correctness, instruction-count resource bounds, and (O(n\log n)) worst-case complexity. Measured: smallest runtime among ten runs for the stated machine and input distributions. Trusted or assumed, as the paper says: for the final Hoare triple, “one has to only check the formalization of Hoare triples, separation logic, and the LLVM semantics” (Section 5.6, p. 314); the code generator forwards memory management to the target libc (calloc) (Section 3.3, p. 302); a verified compiler down to machine code “would further reduce the trusted code base” (Section 6.2, p. 316). Inference: this also leaves Isabelle/HOL, the cost-model assumptions about lowering, the allocator cost behavior, compiler correctness, and the sorting specification itself as trusted. The measured parity does not validate hardware-time upper bounds.

techniques for the catalog

  • resource-aware nondeterministic specifications: associate each legal result with a supremum-composable resource bound in a monadic specification
  • currency refinement: exchange abstract operation coins for concrete instruction or data-structure currencies along stepwise refinement
  • LLVM cost semantics: charge each modeled instruction and prove Hoare triples with separation-logic time credits
  • heap nondeterminism refinement: connect abstract computations to heap implementations while preserving representations and amortized credits
  • phased introsort complexity proof: prove recursive partition/fallback and final insertion costs separately, then compose them into an (O(n\log n)) bound

Last edited: