Theory
We open the box. First-order logic gives us the language for talking about cooperation. Nelson-Oppen gives us the protocol.
Where We Left Off in Practice
Three demos. Two theories agreeing on a counterexample (Act 1, the swap bug). A formula where each piece is satisfiable but the combination is unsatisfiable (Act 2, with the disjunction hanging in the air). A live propagation chain (Act 3, three notes passed).
Now we make the cooperation precise. Five questions to answer:
- What is a theory, formally? Practice talked about reasoners "knowing things." We need a precise account.
- What's the contract for a theory solver? L03 said each one "decides a conjunction." We sharpen that.
- When can two theory solvers be combined honestly? Three restrictions.
- How does the combination work? Purification, plus equality propagation.
- Why doesn't simple equality propagation suffice for the Act 2 formula? Convexity, and what to do without it.
The catchphrase still applies: theories pass notes through equality. Today we say what a note is, who can send one, and what makes one valid.
1. First-Order Logic Semantics
L03 said informally that a theory is a signature and some axioms. We now make this formal. The work pays off in the next section, where the theory-solver interface drops out as a one-liner.
A concrete structure
Take a universe with two elements: . Two elements, nothing more.
Now pin down the meaning of a small vocabulary.
- Two constants: , .
- One unary function , which swaps the two elements: .
- One binary predicate , which holds on two of the four possible pairs: .
That data is what we mean by an interpretation . Together with the universe , it is a first-order structure, written . The structure has just enough furniture for any formula in this vocabulary to have a definite truth value.
Structures, in general
The general shape: a first-order structure is a pair where
- is any non-empty set. Anything goes: integers, reals, planets, arbitrary marks. The structure does not care.
- fixes the meaning of every symbol in our vocabulary:
- For every constant symbol , . The constant denotes some element.
- For every -ary function symbol , .
- For every -ary predicate symbol , . The predicate is a set of -tuples, those for which the predicate holds.
The data above instantiates each clause: a finite , explicit constants, an explicit function table, an explicit relation.
Evaluation rules
Terms denote elements of . Atoms and formulas evaluate to true or false. Both are defined inductively over the syntax.
Terms. A constant is given by directly. A function application is the function applied to its arguments:
Atoms. A predicate application is true exactly when the tuple of arguments lies in the relation:
Formulas. The connectives:
When , we say the structure is a model of , or that is true under .
We restrict ourselves to quantifier-free, ground formulas: no or , and no free variables. Quantifiers are L06.
Evaluating a formula on the structure
Back to the structure from above. Question: does ?
Compute the terms first.
- , so .
- , so , then .
So the question is whether is in . It is.
. The structure is a model of the atom.
This example is small enough to evaluate by hand. Studio Exercise 1 is exactly this evaluator written in Python. Read the rules, type the missing case, predict the truth values, run, check.
Satisfiability and validity, modulo nothing yet
The duality from L01 carries over.
- is satisfiable iff some structure is a model.
- is valid iff every structure is a model.
- is unsatisfiable iff no structure is a model.
is valid iff is unsatisfiable. The same counter-example search you have been doing all along; the trick scales up.
2. Theories as Restricted Structures
So far the universe and interpretation are arbitrary. can do whatever it likes; too. But when we write , we don't want structures where means something weird. We want structures where means actual addition.
You have already met four theories that pin down what their symbols mean:
| Theory | What gets fixed | |
|---|---|---|
| (equality + UF) | = plus arbitrary functions and constants |
reflexivity, symmetry, transitivity, function and predicate congruence |
| (linear real arithmetic) | rational constants, +, * (by constants), <= |
the standard reals |
| (linear integer arithmetic) | integer constants, +, * (by constants), <= |
the standard integers |
| (arrays) | select, store, = |
read-over-write axioms |
Each row is a restriction on which structures count.
admits only structures whose universe is the reals and whose
act normally. admits any universe but
forces equality to act like equality. admits any element
type but forces select/store to satisfy the array axioms.
Now name the abstraction. A theory is a pair
- is the signature: a set of constant, function, and predicate symbols that cares about.
- is the class of -models: the structures whose interpretation of -symbols agrees with the theory's axioms. A structure is a -model iff it satisfies all of them.
Each row of the table picks a and a class . The four theories above are the four you have already used.
Modulo a theory
Once we have a theory, satisfiability and validity modulo the theory are the obvious thing. Drop "every structure" and say "every -model."
- is -satisfiable iff some -model satisfies .
- is -valid iff every -model satisfies .
is -valid iff is -unsatisfiable. The
counter-example search still works: assert the negation, check
-satisfiability, recover a witness if there is one. Z3 has
been doing exactly this every time you wrote Not(P) and called
check().
Uninterpreted symbols
A formula can contain symbols outside . These are uninterpreted. The theory does not constrain them; -models agree with the theory on but assign whatever they like to symbols outside.
This is what Function('f', IntSort(), IntSort()) and
DeclareSort('Point') were doing all along. Z3 declares to
be a function symbol whose interpretation is unconstrained except
by the equality axioms (which are part of , the always-on
core theory). The L03 sq/sqabs trick depended on this exactly:
declare umul uninterpreted, give it a single axiom, watch the
solver succeed without bit-blasting.
3. The Theory-Solver Interface
A theory solver for is a procedure that
- takes a finite conjunction of literals over ;
- returns either sat (with a -model satisfying ) or unsat.
Three things to notice.
- Conjunction only. Not arbitrary boolean combinations. The solver gets a list of literals it must satisfy simultaneously. Disjunctions, if there are any, are someone else's job.
- -literals only. No symbols from outside the theory's signature. Mixed-theory atoms have to be split apart first; that's purification.
- Decides -satisfiability. The procedure terminates and gives the right answer.
This is the contract you have been seeing since L03. We just made it precise. Z3 has theory solvers for , , , , bitvectors, strings, datatypes, and a few more; each one satisfies this interface in its own theory.
4. Nelson-Oppen Restrictions
You have two theory solvers, one for and one for , each meeting the interface above. You want a solver for the combined theory : conjunctions over , looking for a single -model.
We only ever combine two theories at a time. If you have three
or more, say , you fold: combine
with first, then combine the result with .
Nelson-Oppen is the binary operation; the n-ary case is just
reduce.
Nelson-Oppen does this combination, but only under three restrictions. Each will feel a little abstract on first read; §8 walks through an algorithm that uses all three, and at that point you can look back and see exactly what each restriction is buying.
Why not just merge and into one big theory and call its solver? Two answers. First, the union of decidable theories is often undecidable (a classic result). Second, even when it stays decidable, a monolithic solver tends to be much slower than two specialists exchanging notes. Cooperation lets each theory solver keep its specialized data structures and algorithms.
Restriction 1: each is decidable, quantifier-free, conjunctive
Each already has a decision procedure for conjunctions of -literals. This is the §3 interface. It is the prerequisite for everything that follows.
Restriction 2: signatures share only equality
The two theories have disjoint vocabularies except for =. No
symbol other than equality is shared.
This restriction makes the cooperation tractable. When 's solver wants to tell 's solver something about a shared constant, the only language they have in common is equality. The note has to be of the form . Equality is the postal service from Practice; here we are saying it is the only postal service.
Restriction 3: each is stably infinite
is stably infinite iff every -satisfiable formula has a -model with infinite universe.
It is the metagame restriction: the proof would not go through without it. Stably-infinite was added in 1980 to fix a hole in Nelson and Oppen's original 1979 paper. The fix was real; non-stably-infinite theories can be combined unsoundly without it.
The intuition is that the cooperation procedure sometimes needs
room to invent fresh witnesses. If you have ever used gensym
in Lisp or Symbol() in JavaScript, you know the affordance:
conjure a fresh name nobody else is using. That is what
stably-infinite buys the proof. The procedure may need to gensym
a fresh element into the universe to make the cooperation go
through. If a theory's models are forced to be finite, the
procedure can run out of room and report SAT when the right
answer is UNSAT.
A concrete counterexample makes the failure tangible. Combine (one-bit bitvectors, only two values) with :
This is unsatisfiable by pigeonhole. forces , so needs three distinct values, but has only two. Nelson-Oppen does not see this. (the bitvector half) is satisfiable on its own, (the equality half) is satisfiable on its own, and there are no equalities to propagate, so the procedure returns SAT. Wrong. 's finite universe was the gap that the stably-infinite restriction is closing off.
Theories that are stably infinite: , , , . Most theories of interest.
Theories that are not stably infinite: fixed-width bitvectors
(only elements per BV_n sort), the toy theory with
axiom (universe forced to size
at most 2). For non-stably-infinite theories, Nelson-Oppen does
not apply out of the box; specialized cooperation procedures
handle them.
5. Purification
Restriction 2 says no shared symbols except =. But a typical
mixed-theory formula has terms that mix vocabularies. Take
The function symbols and are in . The
operators + and <= are in . But
applies a operator to a term;
does the reverse. Neither term lives purely in one
signature.
Purification is the rewrite that splits a mixed formula into two pure conjunctions, one per theory. It is structurally identical to Tseitin from L01: introduce fresh names for cross-theory subterms, and pin those names with equalities.
The rewrite rule
Apply this rule to fixpoint:
Whenever a subterm appears inside a context that belongs to a different theory's signature, introduce a fresh constant , replace with in place, and conjoin to the formula.
The rule covers three syntactic shapes uniformly: a foreign subterm inside a function application (), inside a predicate application (), or sitting on the other side of a cross-theory equality ( where and belong to different signatures). All three abstract to a fresh and conjoin . The worked example below exercises only the function case; predicates and bare equalities work identically.
Each rewrite shortens the depth of theory-mixing. The rewrite terminates because the formula's syntax tree has finite depth and each step strictly reduces the mixing.
The fresh constants are called shared constants and act as the mailbox for cross-theory facts. Original constants from the formula (here , , , ) may also be shared if they appear in both halves after the rewrite. Purification is the post office being installed; the equality propagation phase will run the mail.
A point that trips up first-time readers: shared constants are not shared terms. After purification every term belongs to one theory's signature only. That is the whole point. What gets shared is the constants that appear in both halves' literals, and those constants are how the two solvers will end up referring to the same objects. The next phase exchanges equalities over those constants, never over compound terms.
Warmup: a one-step purification
Before the bigger example, see the rule on a tiny formula. Take over . The subterm mixes signatures: a term sitting inside , which is in . One application of the rule:
Now separate.
Shared constant: . Local: in , in . One mechanical step, one fresh name, two pure conjunctions joined by an equality. That's the whole rule. The worked example below repeats it five times on a denser formula.
Worked example
The whole transformation in one picture: one mixed formula goes in, two pure conjunctions come out, joined only by the shared constants that purification introduces.
graph TD
accTitle: Purification splits a mixed formula into two pure halves
accDescr: Input formula f(x + g(y)) ≤ g(a) + f(b) flows into a Purification box, which produces two outputs labeled Σ_R and Σ_=, joined by shared constants u_1 through u_5.
F["f(x + g(y)) ≤ g(a) + f(b)"]
P["Purification
(introduce u_1, …, u_5)"]
R["Σ_R: u_4 = x + u_1 ∧ u_5 ≤ u_2 + u_3"]
E["Σ_=: u_1 = g(y) ∧ u_2 = g(a) ∧ u_3 = f(b) ∧ u_5 = f(u_4)"]
M["shared mailbox
{ u_1, u_2, u_3, u_4, u_5 }"]
F --> P
P --> R
P --> E
R -.- M
E -.- MNow the steps. Start with:
The mixed subterms are (a term inside +),
(similar), (similar), (a
term inside ), and (a
term inside <=). Five abstractions, in any order.
Here is one pass:
- Abstract with :
- Abstract with :
- Abstract with :
- Abstract with :
- Abstract with :
Now every atom belongs to exactly one signature. Separate:
Shared constants: (all introduced by purification, all appearing in both halves via their defining equation). Original also live somewhere but each appears in only one half here.
Tseitin escaped exponential CNF blowup by naming subformulas. Purification escapes mixed-theory atoms by naming cross-theory subterms. Same trick, different target. Same correctness story too: the result is not equivalent to the input (we added fresh constants the original didn't mention), but it is equi-satisfiable. Every model of the original lifts to a model of the purified pair by reading off the value of each from its definition, and every model of the purified pair projects back to a model of the original by ignoring those fresh constants.
6. Convexity
A theory is convex iff: whenever a conjunction of -literals implies a finite disjunction of equalities, implies one of those equalities individually.
If the theory can prove the disjunction, it can pick a specific disjunct. No genuine ambiguity.
Examples
Predict before reading on. Of , , and , which are convex? Two are; one is not.
(linear real arithmetic) is convex. Geometric reason: the set of solutions to a conjunction of linear constraints over the reals is a convex polytope. If the polytope is contained in a union of finitely many hyperplanes, it is contained in one of them. The name "convex" comes from this geometry: a convex theory is one whose satisfying assignments form a convex set in the natural ambient space, and convex sets have the property that finitely many hyperplanes covering them must include one that already covers them. and inherit the name even when no obvious geometry is in play.
A common trap: doesn't imply in the reals, with neither disjunct alone implied, making non-convex? Yes, but (variable times variable) is not an LRA term. LRA permits multiplication only by rational constants. Once is in your formula, you have left LRA for nonlinear arithmetic, which is in fact non-convex. The convexity claim depends precisely on the signature being linear.
(equality with uninterpreted functions) is convex. Combinatorial reason: a conjunction of equalities and disequalities partitions the set of terms into equivalence classes. The implied equalities are exactly the equalities within a class. There is no "either-or" the theory can prove.
(linear integer arithmetic) is not convex. The canonical witness is from Practice Act 2:
The conjunction implies the disjunction. But neither equality alone is implied. could be 1; could be 2. The integer lattice has gaps that allow this kind of split.
The theory of bitvectors is also not convex (similar reason: finite domain forces disjunctive consequences).
Why this matters
A convex theory propagates single equalities. A non-convex theory can imply genuine disjunctions of equalities, with no individual equality implied. The cooperation algorithm has to handle these two cases differently. The convex case is simpler; the non-convex case needs a search step.
We start with the convex case.
7. Equality Propagation: The Convex Algorithm
The full algorithm in pseudocode:
def NO_convex(F):
F1, F2 = purify(F) # split into pure conjunctions
while True:
if not T1_solver.sat(F1):
return UNSAT
if not T2_solver.sat(F2):
return UNSAT
new_eq = find_implied_equality(F1, F2)
if new_eq is None:
return SAT
F1.append(new_eq)
F2.append(new_eq)
find_implied_equality(F1, F2) looks for a pair of shared
constants such that one half implies but the
other half does not yet have it. If both halves already agree on
all implied equalities, the algorithm returns SAT.
Three things to verify.
Soundness. If the algorithm returns UNSAT, the original is genuinely unsatisfiable. Short argument: if a sub-solver returns unsat, that pure conjunction has no -model, so the combined formula has no -model.
Termination. The number of shared constants is finite. The number of equalities between them is at most where is that count. Each iteration either returns or adds a new equality, so the loop runs at most times. Complexity falls out: if both sub-solvers run in polynomial time, the convex Nelson-Oppen combination runs in polynomial time. If both are NP, the combination is NP. The non-convex extension in §9 stays in the same broad class but pays an additional factor for case-split branching.
Completeness, for convex theories. If the algorithm returns SAT, the original formula is genuinely satisfiable. The argument needs convexity: when no more equalities are implied by either side, the union of models for and (modulo agreement on shared constants) yields a -model. Stably infinite makes the universe-size accounting work; convexity makes the equalities-on-shared-constants accounting work.
Without convexity, the procedure is sound and terminates but is incomplete. It may return SAT when the formula is unsatisfiable. That gap is what §9 fills.
8. Worked Example
The same formula students saw in Practice Act 3:
Over the reals, with uninterpreted. Convex theories on both sides; the convex algorithm should decide it.
Purification
Three abstractions: , , . Then becomes . Split:
Shared constants: .
The chain
The big picture: four notes pass between the two reasoners. Three equalities cross the boundary; the fourth note is a contradiction.
sequenceDiagram
accTitle: Nelson-Oppen propagation chain on the worked example
accDescr: Σ_R and Σ_= exchange three equalities, then Σ_= reports unsat.
autonumber
participant R as Σ_R (LRA)
participant E as Σ_= (EUF)
Note over R,E: each side satisfiable on its own
R->>E: x = y
Note right of E: f(x) = f(y) by congruence, so u = v
E->>R: u = v
Note left of R: w = u − v = 0, and z = 0, so w = z
R->>E: w = z
Note right of E: f(w) = f(z) by congruence, contradicts f(w) ≠ f(z)
Note over R,E: UNSATSanity. Each side alone is satisfiable. Neither solver bails.
Step 1. implies . From and , we get , so . With , we have . Then , so .
derives . Pass to .
Step 2. , now with , implies . Function congruence: , , , so .
derives . Pass to .
Step 3. , now with , implies . We have and (from Step 1's reasoning) , so .
derives . Pass to .
The third note arrives. Theories pass notes through equality.
Step 4. , now with , is unsatisfiable. The constraint plus plus function congruence gives , contradiction.
is unsatisfiable. The original formula is UNSAT.
Running state
A scratchpad view: each row is one step of the sequence diagram above. The ★ marks the side that derived the equality on that step; the middle column shows which way the note flowed; the other side records the same equality via algorithm step 3.a.
| Step | dir | ||
|---|---|---|---|
| 0 | |||
| 1 | ★ | → | |
| 2 | ← | ★ | |
| 3 | ★ | → | |
| 4 | ⊥: by congruence contradicts |
After step 3 each side's full conjunction has the original literals from step 0 plus all three propagated equalities . Step 4 is then noticing that plus is unsatisfiable.
The two columns stay in sync without ever sharing a non-equality literal. Theories pass notes through equality.
03-no-trace.py from Practice runs each step as a Z3 query of the
form "does this conjunction with the negated conclusion become
unsatisfiable?" and prints a check mark per step. The chain is
mechanically verified: the page does not have to trust the prose.
A predicate-congruence cameo
The main worked example exercises function congruence (the chain). Predicate congruence is the dual axiom: equal inputs produce equal truth values. Tiny example, pure :
Function congruence on gives . Predicate congruence on gives . But the formula says and , contradiction. UNSAT.
No purification needed; the formula is already pure equality. Quick demonstration of the axiom that the main worked example never gets to use.
9. Beyond Convex: Case Splits on Disjunctions
Now back to Practice Act 2. The formula:
with ranging over the integers. Try the convex algorithm.
Purification
Introduce shared constants and so the and terms become and .
Shared constants: .
The convex algorithm gets stuck
is satisfiable on its own (any ).
is satisfiable on its own. Neither implies any
single equality between shared constants, so
find_implied_equality returns None. The algorithm returns SAT.
But this is wrong. The formula is unsatisfiable: if then function congruence forces , contradicting ; the same goes for . Both possible values close.
The algorithm got stuck because implied a disjunction:
But is non-convex, so neither disjunct is implied alone. The convex algorithm has no way to communicate the disjunction across the boundary.
The fix: case split
When a non-convex theory implies a disjunction of equalities, try each disjunct in turn. The combined formula is unsat iff every branch is unsat.
Pseudocode for the extended algorithm:
def NO(F):
F1, F2 = purify(F)
if convex_loop_unsat(F1, F2):
return UNSAT
# Convex loop done. Look for a disjunction of equalities.
disjunction = find_implied_disjunction(F1, F2)
if disjunction is None:
return SAT
for x_eq_y in disjunction:
if NO(F + [x_eq_y]) == SAT:
return SAT
return UNSAT
The recursion bottoms out because each branch fixes one more equality, and the number of equalities between shared constants is finite.
Tracing both branches
Back to our formula. Branch on .
Branch A: .
Add to both halves. now has and . Function congruence gives , contradicting . UNSAT.
Branch B: .
Add . now has and . Same congruence argument: , contradiction. UNSAT.
Both branches close. The original formula is UNSAT.
This is the resolution we promised at the end of Practice Act 2. The reasoner could not pass any single equality to the reasoner; it could only pass the disjunction. The non-convex extension says: try each disjunct, all branches must close. Both did.
10. Closing Bridge: What's Left
We have an algorithm. Three restrictions met (decidable conjunctive, signatures share only equality, stably infinite). Purification. Equality propagation, convex case. Case split on disjunctions of equalities, non-convex case. End to end, conjunctions over are decidable.
Today's piece sits in the middle of a larger SMT picture:
graph TD
accTitle: Where Nelson-Oppen sits inside an SMT solver
accDescr: A CDCL/DPLL(T) layer hands conjunctions to a Nelson-Oppen layer, which dispatches to per-theory solvers for T_=, T_R, T_Z, and T_A.
Top["Boolean envelope: CDCL / DPLL(T)
(L06)"]
NO["Nelson-Oppen
(today)"]
EQ["T_= solver
(L03)"]
LR["T_R / T_Z solvers
(L04)"]
AR["T_A solver
(L04)"]
Top -- "conjunction of T-literals" --> NO
NO -- "Σ_= conjunction" --> EQ
NO -- "arith conjunction" --> LR
NO -- "array conjunction" --> ARBut there are two pieces L04 promised that we have not delivered.
The boolean envelope
Nelson-Oppen takes a conjunction. Real formulas are not always conjunctions. Take
Each disjunct is unsatisfiable: function congruence kills the left, arithmetic kills the right. But Nelson-Oppen cannot see the OR. The missing piece is DPLL(T): the CDCL engine from Week 2 manages the boolean structure on top, dispatching conjunctions of theory literals to Nelson-Oppen (or directly to a single theory solver) at the leaves. That is L06.
ForAll returns
You have already used universal quantifiers, possibly without noticing. The EUF axioms from L03:
are all universally quantified. Z3 has been instantiating them behind the scenes every time you wrote a formula in . Today's worked example used the function-congruence axiom three times (Steps 2, 3, 4). We just did not name it.
Quantifiers come back explicitly in L06. The mechanisms are E-matching (instantiate at terms that already appear in the formula's congruence closure) and MBQI (model-based quantifier instantiation: try to build a model where the quantified formula holds, and instantiate where the model fails). A small foreshadowing example, the kind of thing Z3 just handles:
from z3 import Function, IntSort, Ints, ForAll, Solver, Not
f = Function('f', IntSort(), IntSort())
x, y = Ints('x y')
s = Solver()
s.add(ForAll([x], f(x) >= 0)) # universal axiom about f
s.add(Not(f(y) >= 0)) # negation of an instance
print(s.check()) # unsat (by E-matching)
Z3 returns UNSAT. The instantiation of the universal axiom contradicts the negation. E-matching is the mechanism that finds the right . Next week, in detail.
What we built today
Two notes for your future self.
- Theories pass notes through equality. The catchphrase is literal. Equality is the only shared symbol; everything else stays in its own world. The mailbox is the set of shared constants.
- A reduction is happening here too. The course catch phrase still applies. Purification is a reduction from a mixed-theory formula to a pair of pure formulas linked by shared equalities. Get the purification wrong and the solver gives you a correct answer to the wrong question.
Studio after the break: implement the FOL evaluator, predict truth values, work through some mixed-theory predict-then-check formulas, and purify by hand.
Source
This page follows Aaron Bradley and Zohar Manna, The Calculus of Computation (Springer, 2007), Chapter 10. Purification is §10.2 (p. 265–269); the convex equality-propagation algorithm is §10.3 (p. 272–276); the worked example is Example 10.13 with Figure 10.1 (p. 279–280); the non-convex case-split extension is §10.5 (p. 285–287). Convexity intuition and the stably-infinite restriction are presented in this page's own framing; both are covered in Bradley and Manna §10.4 and §10.6.