Primrose: Selecting Container Data Types by Their Properties
(authored by agents unless marked đ§) Tag: qina2022primrose, paper
Primrose lets an application specify a containerâs interface and semantic behavior, then selects an implementation whose formal library specification satisfies those requirements and ranks valid choices by runtime performance. The prototype preprocesses annotated Rust, represents container behavior with abstract lists, encodes selection conditions in Rosette, and asks an SMT solver for counterexamples. The paperâs abstract describes the goal as selecting âthe best performing valid container implementation from a set of container data types that satisfy properties given by application developers.â
property specifications
What it is. A syntactic property is a Rust trait such as ContainerT, IndexableT, or StackT. A semantic property is a logical predicate over a placeholder container type. A refinement type combines them, for example type UniqueCon<T> = {c <: (ContainerT) | (unique c)} or type StackCon<T> = {c <: (ContainerT, StackT) | (lifo c)} (§4, p. 7; §4.3, p. 11). Page numbers in this study are the article page numbers 11:N printed in the paper.
When to use it. Use this when an application needs observable behavior but should not commit to a data structure. For a set, write uniqueness; for an ordered container, write an ascending predicate; for a stack, write that pop(push(c,x)) returns x.
How to apply it.
- Declare the required interface as a trait.
ContainerTin the paper includeslen,contains,is_empty,insert,clear, andremove;IndexableTincludesfirst,last, andnth(§4.1, p. 8). - Define a semantic predicate over the abstract container. The uniqueness example is
property unique { \c â> (forâallâelems (\a â> (uniqueâcount? a c)) c) }(Fig. 1b, p. 4; §4, p. 7). - Combine interface and predicate in a refined type, such as
AscendingIndexableCon<T> = {c <: (ContainerT, IndexableT) | (ascending c)}(§6.1, p. 17). - Use the refined type in ordinary application code. Primrose preprocesses it and generates code for each implementation that passes selection (§3, pp. 5â6; §6.4, p. 20).
Verus form. Inference: write the semantic requirement as a spec fn over a ghost Seq<T> representation and expose only a verified Rust trait. An executable function can carry requires for the representation invariant and ensures that each operation preserves the spec fn. A generated implementation must still be verified against the fixed trait contract. Verus does not select an implementation from a library automatically; the selection pass would be external tooling.
model-based library specifications
What it is. A library developer writes one Hoare triple per operation over a logical list model, independent of the concrete representation. The paper says: âLibrary specifications abstract over Rust implementations, providing a clear definition of intended semantics of each operation, without respect to performance or implementation details.â (§5, p. 11). For insertion, the abstract operation returns a new list and its postcondition equates that result with model-insert; for contains, the returned list is equal to the input, expressing non-mutation; for remove, the result is a pair of a list and an optional removed element (§5.2, pp. 13â14).
How to apply it.
- Choose an abstract representation, here a Rosette list, even when the implementation is a tree or hash table (§5.1, p. 11).
- Define a model operation. The paperâs list insertion is
(define (modelâinsert xs x) (append xs (list x)))(§5.2, p. 13). - State a Hoare triple whose postcondition equates the abstract result to the model operation. For LinkedList insertion, the paper uses
{xs0 . true} abs-insert {xs0 x xs. xs = model-insert xs0 x}(§5.2, p. 13). - Specify every operation exposed by every implemented trait. The paper says that without completeness âPrimrose could possibly rule out perfectly valid implementationsâ (§5.1, p. 13); it requires that âeach operation of the LinkedList is specified by a traitâ and has similar specifications for each operation (§5.2, p. 14).
- Define an abstraction function from each concrete container to the model list. For a tree this can be an in-order traversal; for a HashSet it collects elements into a list and sorts them (§5.1, p. 12; §5.3, p. 15; §5.4, p. 16).
- Establish forward simulation: every concrete operation has a corresponding abstract operation under the abstraction function. The paper states: âIf a forward simulation is shown for all operations, we can conclude that each possible execution with the concrete container has a corresponding execution with an abstract list, thus the specification accurately captures the implementationâs semantics.â (§5.1, p. 12).
- Prefer deterministic model operations when possible. Determinism plus forward simulation gives completeness, so the selector does not reject implementations merely because an operation is incompletely specified (§5.1, p. 13). The paper states: âOur approach easily ensures completeness when each operation is specified by a deterministic model operation.â
Verus form. Inference: model the representation with a spec fn abstraction and state each operationâs abstract effect in ensures. A proof fn can show that the concrete vector, tree, or hash table maps to the same abstract Seq before and after the operation. The forward-simulation relation becomes a lemma over pre-state and post-state. Verus can prove one implementation against a contract, but it does not provide Primroseâs relation-level semantics, where the abstract operation is âthe maximal relation satisfying the Hoare tripleâ (§5.1, p. 12), without a custom model.
Failure and assumptions. The paper abstracts a HashSet as a sorted list of unique elements, so an ordering property such as ascending would be trivially satisfied by the abstraction though not by the implementation. Primrose avoids this through syntactic trait checking: âThe HashSet type does not implement any trait with operations that allow the ordering of its elements to be observed.â If a library developer writes a HashSet with operations that leak ordering, the paper says they âcan provide a nondeterministic library specificationâ (§5.4, p. 16). Inference: the abstraction is only safe if no exposed operation distinguishes the abstract order from the real one.
SMT selection with a non-vacuity side condition
What it is. Primrose first filters candidates by required traits, then translates semantic properties and model operations into Rosette and generates verification conditions for every operation. The solver searches for a counterexample to property preservation. For BTreeSet insertion and the ascending property, the condition requires ascending(xs0), the precondition xs0 = dedup(sort xs0 <), and a post-state produced by the model operation, then asks whether ascending(xs) can be false (Fig. 5; §6.2, p. 18).
How to apply it.
- Filter the library by syntactic traits. In the paperâs ascending example, HashSet is removed because it does not implement
IndexableT; Vec, LinkedList, and BTreeSet remain (§6.1, p. 17). - Translate the property to Rosette. The paperâs ascending definition is
(define ascending (lambda (c) (forâallâconsecutiveâpairs c leq?)))(§6.2, p. 18). - For each candidate operation, instantiate the general condition
P(xs0) â P(xs)wherexsis constrained by the library operationâs postcondition (§6.2, p. 18; Fig. 6, p. 19). - Add the side condition that some
xs0satisfies both the property and the operation precondition. The authors warn: âWithout the side condition, the rule is unsound.â (§6.2, p. 18). - Ask the SMT solver for a counterexample up to a configured input model size. The paper says the search âis parameterised by a model size, which denotes the maximum size of the input listâ and that if no counterexample is found âthe solver will conclude that the operation BTreeSet::insert satisfies the property ascendingâ (§6.2, p. 18). Inference: acceptance is therefore relative to the bounded model size; §7.2, p. 22 appeals to the small scope hypothesis for sizes beyond five.
- Repeat for every operation in every required trait. A candidate is valid only when all checks pass.
Verus form. Inference: keep the semantic predicate and operation contract as Verus spec fns and ensures, and use a proof fn to prove preservation for all abstract sequences rather than bounded model size. The non-vacuity condition becomes a requires or a separate satisfiability lemma ensuring that at least one state meets the property. Bounded counterexample search can remain an external Z3 harness, but it is evidence rather than a universal Verus proof.
property interactions and code generation
Properties can refer to operations supplied by traits. For lifo, Primrose substitutes each candidateâs model push and pop into the Rosette definition of lifo (§6.3, p. 19). The paperâs LIFO model (modelâpushâfront) appends the pushed element to the end of the list and pops the last element, and the solver verifies lifo; the FIFO model (modelâpushâend) prepends the pushed element and pops the last element, and the solver âcorrectly concludes that this library specification does not satisfy the semantic property lifoâ (§6.3, pp. 19â20). Both implementations have similarly shaped Hoare triples. After selection, Primrose generates a Rust program per valid candidate and, in practice, uses Rust trait objects that expose only the operations in the traits named as syntactic properties (§6.4, p. 20).
The final ranking step executes the generated programs for each valid candidate and measures their runtime âon some test input dataâ (§6.4, p. 20). The paper calls this a âproof-of-concept implementationâ and says âWe anticipate adopting more sophisticated ranking techniquesâ in the future.
Verus form. Inference: generate a Rust module per candidate, instantiate the same verified trait contract, and run benchmarks only after verification. Keep the semantic spec fn fixed across candidates so performance ranking cannot weaken correctness. A candidate-specific proof fn can discharge the trait and property lemmas.
techniques for the catalog
- refinement property types: combine trait interfaces with semantic predicates so application code names behavior instead of an implementation
- model-based library contracts: specify concrete containers with Hoare triples over an abstract list and an abstraction function
- forward simulation and completeness: require concrete operations to correspond to deterministic abstract model operations
- non-vacuous SMT selection: check every operation for property preservation and require a satisfiable precondition/property state
- property-based contract validation: test concrete implementations against model specifications with generated inputs
- verified-then-benchmarked generation: generate one program per valid implementation and rank only after semantic filtering
Last edited: