Implementing and reasoning about hash-consed data structures in Coq

Implementing and reasoning about hash-consed data structures in Coq
Notice: This research summary and analysis were automatically generated using AI technology. For absolute accuracy, please refer to the [Original Paper Viewer] below or the Original ArXiv Source.

We report on four different approaches to implementing hash-consing in Coq programs. The use cases include execution inside Coq, or execution of the extracted OCaml code. We explore the different trade-offs between faithful use of pristine extracted code, and code that is fine-tuned to make use of OCaml programming constructs not available in Coq. We discuss the possible consequences in terms of performances and guarantees. We use the running example of binary decision diagrams and then demonstrate the generality of our solutions by applying them to other examples of hash-consed data structures.


💡 Research Summary

The paper investigates how to implement hash‑consing—a technique that guarantees maximal sharing of immutable values—within the Coq proof assistant, and how to reason about the resulting data structures. Four distinct design patterns are presented, each tailored to two primary use‑cases: (1) executing the code directly inside Coq (for reflective decision procedures or proof‑by‑reflection) and (2) extracting the Coq code to OCaml (for model‑checking, static analysis, or other practical tools). The authors use reduced ordered binary decision diagrams (ROBDDs) as a running example because BDDs naturally require hash‑consing and memoization to achieve acceptable performance.

State of the art in memoization.
The authors first review three existing memoization techniques in Coq: (a) a state‑monad approach that threads a finite map of argument/result pairs through the program; (b) shallow memoization using co‑inductive streams (lazy thunks) that cache results for functions over natural numbers; and (c) “adjustable references” that work only after extraction. They argue that only the state‑monad can express memoized fixed‑point combinators, which are essential for BDD construction, but it incurs a heavy proof burden because every caller must be monadic.

Four implementation patterns.

  1. pure‑deep (Section 5.1).

    • Memory is modeled as two finite maps inside Coq: one from node identifiers to node records, and another from node records to identifiers (the hash‑consing pool).
    • Identifiers are generated by a pure counter. All operations are pure functions that return updated maps together with results.
    • Advantages: fully executable inside Coq, straightforward to reason about, and the extracted OCaml code preserves the same functional semantics.
    • Drawbacks: each operation copies or updates large persistent maps, leading to high memory consumption and poor runtime performance, especially for large BDDs.
  2. pure‑shallow (Section 5.2).

    • Uses co‑inductive lazy streams to cache the result of a function for each natural‑number argument. The stream is built once and then accessed via nth.
    • Simpler to implement and easier to prove correctness because the cache is a single co‑inductive object.
    • However, the technique only works for functions whose domain is a simple enumeration; it cannot handle the recursive fixed‑point combinators needed for BDD node merging, and the extracted OCaml code loses the sharing benefits, resulting in severe slowdown.
  3. smart (Section 6.1).

    • Relies on Coq’s extraction mechanism to inject OCaml‑specific mutable hash tables and a global counter. Inside Coq, the hash‑consing operations are declared abstractly; after extraction they become calls to the OCaml runtime.
    • Provides near‑native OCaml performance: BDD operations run in O(|A|·|B|) time, and sharing is achieved through the mutable table.
    • Inside Coq the implementation is “impure” – the code does not actually share nodes, so reflective execution is inefficient. Moreover, to prove that the extracted code respects the Coq specification, the authors must introduce axioms linking the abstract operations to their OCaml semantics.
  4. smart+uid (Section 6.2).

    • Extends the smart approach by axiomatizing the behavior of unique identifiers (UIDs). The paper defines operations such as uid_eq, uid_hash, and states their algebraic properties (reflexivity, transitivity, consistency with hashing).
    • This enables reasoning about UID‑based equality inside Coq, allowing proofs that rely on the fast pointer‑equality that the extracted code will enjoy.
    • The trade‑off is a larger axiom set and a heavier proof effort to show that the axioms are sound with respect to the OCaml implementation.

Evaluation on BDDs.
The authors implement the four patterns for a full BDD library, including node creation, the meld combinator (the core binary operation that respects variable ordering), and memoized Boolean operators (AND, OR, XOR). They benchmark the extracted OCaml programs on a set of representative BDDs. Results show:

  • smart and smart+uid achieve the best runtime, often an order of magnitude faster than the pure approaches.
  • pure‑deep suffers from large persistent map updates, leading to high memory usage and slower execution.
  • pure‑shallow performs poorly after extraction because it cannot exploit OCaml’s mutable hash tables.

From a proof perspective, pure‑deep and pure‑shallow allow complete Coq‑level verification of maximal sharing and memoization invariants, while the smart family requires additional axioms and a proof of extraction correctness.

Guidelines and trade‑offs.
The paper concludes with a decision matrix:

Criterion pure‑deep pure‑shallow smart smart+uid
Executable inside Coq Yes Yes Limited Limited
Extraction performance Poor Poor Excellent Excellent
Proof effort (Coq) Moderate Low High (axioms) Very high (UID axioms)
Guarantees maximal sharing Proven Proven (limited) Assumed via extraction Proven via UID axioms

Thus, if the primary goal is internal verification (e.g., reflective tactics), pure‑deep is recommended despite its runtime cost. If the goal is to ship a high‑performance tool, smart or smart+uid are preferable, with the latter offering stronger Coq‑level reasoning about identifiers.

Broader impact.
Although the case study focuses on BDDs, the authors argue that the patterns are applicable to any hash‑consed structure such as lambda terms, expression DAGs, or compiler intermediate representations. The work bridges the gap between pure functional proof assistants and imperative performance‑critical implementations, providing a systematic methodology for developers who need both formal guarantees and practical efficiency.


Comments & Academic Discussion

Loading comments...

Leave a Comment