Array extensionality¶
STP reasons about array elements (select/store) always, and
about arrays as whole values with the --array-equality option: it
then decides the quantifier-free extensional theory of arrays, in
which equality and distinct between array terms are first-class
atoms. Without the option an equality between two array terms is
refused, with an error, at the point the term is built – except a
syntactically reflexive one, which the simplifying node factory folds to
true before the rejection is reached.
The implementation is an STP-specific integration of the lemmas-on-demand procedure of
Robert Brummayer and Armin Biere, Lemmas on Demand for the Extensional Theory of Arrays, Journal on Satisfiability, Boolean Modeling and Computation 6 (2009), 165–201.
Usage¶
Command line:
stp --array-equality file.smt2
C API: call vc_setFlag(vc, 'x') before constructing any equality
between whole arrays.
The option controls whether construction of the dedicated opaque
ARRAY_EQ node is permitted; its abstraction is deferred until the
completed query reaches the solver.
Python API: create the solver with stp.Solver(array_equality=True).
Arrays are built with Solver.array(name, index_width, value_width),
read with a[i], written with a.store(i, v), and compared whole
with ==/!=; ArrayExpr.model() returns an array’s entries in
a satisfying assignment, in ascending index order. Comparing whole
arrays on a solver created without the option raises RuntimeError.
With the option enabled:
(= a b)and(distinct a b)over array terms are decided, as is equality involvingstorechains and array-valuedite;(get-model)prints each array as a valid nullarydefine-funwhose body is a constant default cell with the observed writes stored on top, in ascending index order — the model replays in any conforming SMT-LIB2 solver. This form is used whenever the option is on, even for a query containing no array equality; only with the option off does the pre-feature array printer run;vc_getCounterExampleArrayreturns one entry per concrete index in ascending index order;array-valued
(get-value ...)is rejected as unsupported (use(get-model)).
Nullary array-sorted define-fun is accepted by the SMT-LIB2 parser
whether or not the option is on: such a definition is a pure name for its
body, and benchmarks use them without any whole-array equality in sight.
Without the option, STP decides exactly what it decided before the
feature existed, with one deliberate exception: an equality between whole
array terms is now refused rather than warned about. It was never
decided – nothing eliminates the array-sorted operands, so the atom
reached the solver unconstrained and the verdict could be wrong, and a
build with assertions aborted instead of answering. Both behaviours
reproduce on STP releases predating this feature. The documented C API
surface is pinned by an opt-in test
(default-off-capi-baseline-differential, enabled with
-DTEST_BASELINE_DIFFERENTIAL=ON), which builds the upstream commit
this branch was last merged with from git history and compares every
observation of an identical C API driver — verdicts, model values, every
counterexample-array entry, stdout, stderr and exit status — across the
two builds. Holding upstream fixed on both sides is what makes the
difference attributable to this feature; a baseline frozen further back
would also collect every unrelated upstream change made since. The
driver canonicalizes the entry order before comparison: the default-off
API does not specify one, and the legacy order comes from an unordered
map keyed by internal node-creation IDs. One diagnostic text
deliberately differs from the baseline and sits outside that comparison:
whole-array equality is refused with an error naming the option, where
the baseline warned and continued (pinned by a lit test). One
behavioral corner also deliberately differs: when the
soft timeout expires while lazy refinement is still undecided, the
solve now reports a timeout where it previously aborted with an
internal error.
Limitations: arrays of arrays are not supported (STP’s sort system has
no nested array sorts), and there is no constant-array
(as const) input syntax.
How it works¶
The procedure is an abstraction-refinement loop. As a whole it plays
the paper’s DP_A, the decision procedure for the theory of arrays;
STP’s existing bit-blaster and SAT solver provide DP_B, the solver
for each abstracted candidate formula:
Opaque construction and solve-boundary lowering. Construction of an equality between whole arrays produces a dedicated, near-opaque
ARRAY_EQnode whose operands remain visible. The node factory folds the shapes that are decidable on sight – reflexivity, two writes to the same index, and an if-then-else against one of its own branches – and leaves everything else alone. Function,letand query substitution therefore specialize the operands normally. Once the complete query has been assembled, a DAG traversal replaces every reachableARRAY_EQwith a fresh Boolean abstraction variable and creates its witness record. Repeated or operand-swapped pairs reuse one record within that solve, and a reflexive equality folds to true. Records, proxies and witnesses are solve-local and are rebuilt for the next query; only the opaque public AST persists. NoARRAY_EQreaches the array transformer or the bit-blaster.Two preprocessing passes deliberately run before that lowering, and are the only points in the solve that see an
ARRAY_EQat all. Equality propagation goes first, so that a definitional equality substitutes its symbol operand away and never reaches abstraction. Unconstrained-variable elimination goes second, because an equality with an unconstrained operand is a free Boolean and settles there rather than costing a record, a witness pair and a refinement loop. Afterwards the operands sit under witness reads, and the unconstrained array rules turn themselves off.One shape skips abstraction entirely: a chain of writes equated with its own base array (the frame condition
store(a,i,v) = aand its nestings) is solved by rewriting into read equalities over the base, each guarded by disequality with the indices of the writes that shadow it. Such an equality contributes nothing to the refinement loop at all.Array-valued
ite(c, a, b)is not abstracted at all: it is built as an ordinary node and stays one, and the consistency checker reasons about it directly (see stage 3).Solve-time preparation. Each current-root equality
a = blowered in this solve carries a fresh witness index λ and two virtual readsa[λ],b[λ], with the constrainta = b ∨ a[λ] ≠ b[λ]: if the SAT solver makes the equality false, the two arrays must visibly differ at λ (axiom A4′, the paper’s preprocessing step 1). These are conjoined onto the formula before STP’s ordinary simplification, and the equality operands are recovered from them afterwards in their simplified form. Each anchor remains a plain read even when its operand is an array-valued if-then-else, because read distribution is suppressed while the procedure is active. Every index and value that could appear in a future lemma is given a named variable inside the initial formula, so refinement lemmas can later be encoded over SAT variables that already exist.Once any equality is active, the checker owns the complete array graph reachable from the prepared formula, rather than a syntactic cone around the equality operands. Every surviving array-valued
ite(c, a, b)in that graph has its condition reified: a fresh Boolean symboln_cwithn_c ↔ c. The checker decides which branch of an if-then-else is live fromσ(n_c), and that has to be the value the bit-blasted circuit took — re-deriving it from the counterexample is how a scalar name comes to disagree with the term it stands for, and here it would make the wrong branch live and certify a model that does not satisfy the if-then-else axiom. The same symbol is what a lemma premise names, since encoding needs one fully encoded literal per Boolean atom. Reads are not distributed over these if-then-elses, by the simplifier or by the array transformer: the structure and its read-abstraction variables have to reach the checker intact.These two stages realize the transformations of the paper’s §4 and §5 in an STP-specific order: construction preserves a durable opaque equality; the fully expanded solve root is then lowered to solve-local proxies and witnesses; ordinary preprocessing rewrites the result; and operand recovery plus complete-graph freezing happen immediately before array transformation. Opacity permits function specialization to finish before lowering while still ensuring that ordinary STP processing never sees an array equality.
Consistency checking (paper §7,
lib/Extensionality/). When the SAT solver produces a satisfying assignment σ, a pure checker decides whether σ can be extended to a real array model. It seeds every access at its own array (rule I; writes are treated as reads of themselves per §11.4, so write congruence needs no extra constraints), then propagates accesses to a fixed point: down through and up over writes whose index differs from the access index under σ (rules D/U, axiom A3), across array equalities that σ makes true (rules R/L), and between an array-valued if-then-else and whichever branch σ selects (rules T-down/T-up). Two accesses meeting at one array with equal concrete indices but different values violate read congruence (rule C, axiom A1).The T rules are the direct integration §4.1 mentions but declines to present (“in principle, our approach supports a direct integration of if-then-else on terms of sort Array without rewriting it up front”). They are R/L with the equality proxy replaced by the condition literal and the destination chosen by σ rather than fixed by the edge, and their soundness case is Lemma 8.1’s R case with axiom A4 replaced by the if-then-else axiom. The model construction of §9 needs one new obligation, the analogue of Proposition 9.4 — and only its positive half, because the if-then-else axiom is a pair of implications rather than a biconditional: nothing ever requires an if-then-else to differ from a branch, so no witness index and no virtual reads are needed for one. Exactly one branch is live per candidate, so unlike an array equality there is no proxy left unconstrained for the SAT solver to guess and the checker to refute.
The alternative §4.1 does present — rewriting
ite(c,a,b)up front into a fresh arraydwithc → d = aand¬c → d = b— is what this replaced. It charges two array equalities, two witness indices and four virtual reads per if-then-else where the direct rules charge one Boolean literal, so it is worse on the paper’s own bound (Proposition 10.1), and each of its two proxies is unconstrained under one of the two assignments of the condition. Its cost also grew with nesting depth on array if-then-else under an equality, where the direct rules are flat.Per §11.2 the checker keeps one representative access per concrete index of each array, keyed by its index value: congruence is a single lookup, and an access arriving with the same index and value as its representative is dropped without further propagation.
Lemmas on demand (paper §8). A conflict yields the lemma
index(x) = index(y) ∧ ⋀ path write-index disequalities ∧ ⋀ crossed array equalities ∧ ⋀ crossed if-then-else conditions → value(x) = value(y)which is false in the candidate σ. Propagation paths are as short as the pass can make them (the minimization of §11.1): seeding every access before the fixed point starts makes the FIFO work list breadth-first per access, so the arrival that fires a conflict is the earliest — shortest — one. Because the pass continues past a conflict rather than stopping at it, that is exact for the first conflict of a pass and best-effort afterwards: an arrival that conflicts is not queued onward, so a later conflict uses the shortest route still open to it, and a pair that could only have met at an earlier conflict site waits for a later refinement round. The lemma is encoded as clauses over the SAT variables of the already-encoded names (equalities reified through fresh definitional literals); an atom the simplifier can decide from its defining terms — write indices that are distinct constant offsets from one pointer are the common case — is dropped at encoding time instead of becoming an equality circuit the SAT solver would have to search through, on whichever side of the atom the structural verdict permits. The clause is added to the incremental SAT solver; the loop re-solves. Each lemma permanently excludes the assignment that produced it, so the loop terminates.
Models. When the checker finds no conflict, the fixed point of its propagation defines each array’s observed contents; unobserved indices take a single default cell, which is zero except for an array of
RoundingModeelements, where zero is not a mode at all and the default isRNE. These observations — including the witness indices of false equalities — feed the model printer and the programmatic model APIs.
When at least one equality is active, the consistency checker owns the complete reachable array graph and directly abstracts every read in it. STP’s legacy lazy read refinement is not entered during such a solve. This single ownership boundary avoids candidates whose scalar dependencies crossed the former checker/legacy partition. When no equality is active, the extensionality checker remains dormant and STP’s legacy array path runs unchanged. An active candidate is reported satisfiable only when both the array consistency check and STP’s ordinary model evaluation pass on the same assignment.