diff --git a/Library-SMTLIB/src/de/uni_freiburg/informatik/ultimate/logic/PrintTerm.java b/Library-SMTLIB/src/de/uni_freiburg/informatik/ultimate/logic/PrintTerm.java index b54b59c77..87713de3b 100644 --- a/Library-SMTLIB/src/de/uni_freiburg/informatik/ultimate/logic/PrintTerm.java +++ b/Library-SMTLIB/src/de/uni_freiburg/informatik/ultimate/logic/PrintTerm.java @@ -144,6 +144,16 @@ private void run(final Appendable appender) throws IOException { } } appender.append('('); + } else if (next instanceof int[]) { + final int[] arr = (int[]) next; + mTodo.addLast(")"); + for (int i = arr.length - 1; i >= 0; i--) { + mTodo.addLast(arr[i]); + if (i > 0) { + mTodo.addLast(" "); + } + } + appender.append('('); } else { appender.append(next.toString()); } diff --git a/SMTInterpol/doc/offset-equality-plan.md b/SMTInterpol/doc/offset-equality-plan.md new file mode 100644 index 000000000..6c280f5cc --- /dev/null +++ b/SMTInterpol/doc/offset-equality-plan.md @@ -0,0 +1,1467 @@ +# Offset Equality Extension for Congruence Closure + +## Overview + +Standard congruence closure (CC) maintains equality classes: all members of a +class are equal. With offset equalities, a class becomes an *affine class*: +every member `t` has a known rational offset from the representative, meaning +`t = rep + t.mOffsetToRep`. Plain equality is the special case offset = 0. + +The main motivation is tighter integration with linear arithmetic (LA): instead +of LA posting only zero-offset equalities to CC via the Nelson-Oppen shared-term +mechanism, LA can post `a = b + k` for any rational `k`, allowing CC to merge +the two terms and potentially fire new congruences immediately. + +--- + +## CCTerm — weighted union-find + +Add one field: + +```java +Rational mOffsetToRep; // this == mRepStar + mOffsetToRep +``` + +`mRepStar` continues to point directly to the representative; no path +compression is used (merges already update all `mRepStar` pointers), so +`mOffsetToRep` is always exact. + +### Merge with offset k (`a = b + k`) + +``` +repA = a.mRepStar, offsetA = a.mOffsetToRep → a = repA + offsetA +repB = b.mRepStar, offsetB = b.mOffsetToRep → b = repB + offsetB + +a = b + k ⟹ repA + offsetA = repB + offsetB + k + ⟹ repA = repB + (offsetB + k − offsetA) +``` + +Let `delta = offsetB + k − offsetA`. When merging the smaller class (say +`srcRep = repA`) into the larger (`destRep = repB`), all members of `srcRep`'s +class get their `mOffsetToRep` shifted: + +```java +Rational delta = offsetB.add(k).sub(offsetA); +for (CCTerm t : src.mMembers) { + t.mRepStar = destRep; + t.mOffsetToRep = t.mOffsetToRep.add(delta); +} +``` + +`undoMerge` restores both `mRepStar` and `mOffsetToRep` for all moved members, +which it already iterates over — no structural change needed there. + +### Consistency check (already same class) + +If `repA == repB`, require `offsetA == offsetB + k`; otherwise it is an +arithmetic conflict and a conflict clause must be generated. + +### invertEqualEdges + +Must accumulate and negate offsets as it reverses the equality-edge chain. + +### Proof trail — no mEqualEdgeOffset needed + +`mEqualEdge` from term `t` already points to one of the two specific CCTerms in +`mReasonLiteral` (either `getLhs()` or `getRhs()`). Path reconstruction can +recover the edge offset without any extra field: + +- If `t == eq.getLhs()`: `t = mEqualEdge + eq.mOffset`, contributing `+mOffset` +- If `t == eq.getRhs()`: `mEqualEdge = t + eq.mOffset`, contributing `−mOffset` + +A pointer comparison against `eq.getLhs()` determines the direction at +reconstruction time. `CongruencePath` therefore only needs to accumulate these +signed offsets as it walks the chain. + +--- + +## CCEquality — offset equality atoms + +Add one field: + +```java +Rational mOffset; // lhs == rhs + mOffset (currently always 0) +``` + +All existing code that constructs `CCEquality` passes offset 0 and is +unaffected. LA creates `CCEquality` objects with non-zero offsets when it +derives `a = b + k`. + +--- + +## Offset arguments to function applications + +For an argument of the shape `ccterm + constant`, e.g. `f(x+5)`, there are two +ways to represent the `+5`. + +### Option A — reify a CCTerm for `x+5` (rejected) + +Create a CCTerm for `x+5` and place it in `x`'s affine class at offset 5. +`CCAppTerm` and `SignatureTrigger` stay structurally unchanged; the argument is +an ordinary CCTerm whose `mOffsetToRep` happens to be 5. + +The problem: to put `x+5` into `x`'s class, CC must assert the offset equality +`(+ x 5) = x + 5`. This is a definitional **tautology**, and the proof +generator has to justify it as a leaf ("by definition of +"). Every +offset-shaped argument then costs one extra CCTerm **and** one tautological +offset equality that the proof machinery must discharge. + +(Note: a *standalone* `(= y (+ x 5))` is not affected — it is simply the offset +equality `y = x + 5`, a genuine asserted fact, and creates no `x+5` CCTerm.) + +### Option B — offset inside CCAppTerm (chosen) + +Add a parallel offset array to `CCAppTerm`: + +```java +Rational[] mArgOffsets; // arg i represents mArgs[i] + mArgOffsets[i]; null if all zero +``` + +No CCTerm is created for `x+5`. The effective offset of argument `i` for +signature purposes is `mArgs[i].mOffsetToRep + mArgOffsets[i]`. Keeping the +field `null` when every offset is zero means the common (plain-CC) case costs +nothing. + +The `+5` is now **intrinsic** to the term: the proof checker sees `f(x+5)` and +the offset is part of what the term is — there is no tautology to discharge. + +`SignatureTrigger` carries the same structural offsets. They are constants +fixed at term-creation time (they never change), so they need no backref +tracking; only `mArgs[i].mOffsetToRep` varies, so `recomputeHashCode`'s delta +logic is unchanged. + +`CongruenceTrigger` handles both forms transparently. + +### Scope + +Option B only helps arguments of the shape `ccterm + constant`. A genuine +linear-combination argument such as `f(x+y)` or `f(2x+1)` still needs a shared +CCTerm and obtains its offset relationship through real LA propagation — an +honest fact, not a tautology. + +## SignatureTrigger — congruence detection + +For uninterpreted functions, `f(a) = f(b)` only holds when `a = b + 0`, i.e. +same representative **and** same offset. The signature hash and equality check +must therefore include the effective offset of each argument — that is +`mArgs[i].mOffsetToRep` plus the structural `mArgOffsets[i]` from the app term +(see Option B above). + +Only `SignatureTrigger.computeHashCode()` and `equals()` need to change. +`CongruenceTrigger` extends `SignatureTrigger` and inherits both methods without +overriding them, so it picks up the fix for free. `ReverseTrigger` is +independent (extends `SimpleListable`, not `SignatureTrigger`) and is +unaffected. + +### computeHashCode + +```java +public int computeHashCode() { + int h = mId.hashCode(); + for (int i = 0; i < mTerms.length; i++) { + CCTerm t = mTerms[i]; + h ^= HashUtils.hashJenkins(i, t.getRepresentative()); + h ^= HashUtils.hashJenkins(i, t.mOffsetToRep); // add offset + } + return h; +} +``` + +`recomputeHashCode` needs a corresponding update to XOR out the old offset +contribution and XOR in the new one when a representative (and therefore its +offset assignment) changes. + +### equals + +```java +for (int i = 0; i < mTerms.length; i++) { + CCTerm a = mTerms[i], b = other.mTerms[i]; + if (a.getRepresentative() != b.getRepresentative()) return false; + if (!a.mOffsetToRep.equals(b.mOffsetToRep)) return false; +} +``` + +--- + +## CCTermPairHash — keyed by (lhs, rhs, offset) + +### Design + +Instead of a `Map` inside `Info`, change the *key* of the +hash table to include the offset. Each `Info` entry now represents one specific +relationship `lhs = rhs + offset`. `mDiseq` and `mEqlits` remain a single +disequality and a flat list of equalities, all pertaining to that one offset. + +The symmetry condition + +``` +hash(lhs, rhs, offset) == hash(rhs, lhs, −offset) +``` + +reflects that `lhs = rhs + k` and `rhs = lhs + (−k)` are the same fact. + +### Canonicalization + +Normalize at `Info` construction time: if +`System.identityHashCode(lhs) > System.identityHashCode(rhs)`, swap lhs↔rhs and +negate the offset. The stored triple `(lhs, rhs, offset)` is then always in +canonical form, so `hashCode` is a straightforward function of the three fields +and `equals` is a direct comparison (no need to check both orientations at query +time). + +`Info` gains one field: + +```java +Rational mOffset; // lhs == rhs + mOffset (canonical form) +``` + +`getInfo(lhs, rhs)` becomes `getInfo(lhs, rhs, offset)`. + +### Merge rehashing + +When `srcRep` merges into `destRep` with offset `delta` (`srcRep = destRep + delta`), +each pair-info entry `(srcRep, other, k)` migrates to `(destRep, other, k − delta)`: + +``` +srcRep = other + k, srcRep = destRep + delta + → destRep = other + (k − delta) +``` + +If an entry `(destRep, other, k − delta)` already exists, the equality and +disequality lists are joined and any conflict is checked — exactly the same +logic as today, just with the adjusted offset in the key. Conflict detection +remains O(1): `getInfo(srcRep, destRep, k)` directly returns the relevant entry. + +The zero-offset case is unchanged (offset = 0 throughout). + +--- + +## CongruencePath / proof reconstruction + +Walking the `mEqualEdge` chain accumulates signed offsets (see CCTerm section +above). The final proof term asserts `a = b + Σoffsets` rather than `a = b`. +For the hyperresolution intermediate representation, offset-equality steps need +an offset parameter added to the existing equality step type (or a new step +type). + +--- + +## LA interface + +When LA derives an equality `a − b = k`, it currently posts to CC only if +`k = 0`. With offset CC, LA can post it as a `CCEquality` with `mOffset = k` +for any `k`, allowing CC to merge the two terms immediately and fire congruences +that Nelson-Oppen would only discover through an additional round-trip. + +The existing `share()` mechanism propagates to LA when two shared terms end up +in the same affine class with offset `k`: LA should be told `a − b = k` rather +than the bare equality `a = b`. + +--- + +## Implementation strategy: one engine, one flag + +Rather than maintaining a separate "no offset" code path, the engine carries +offsets **unconditionally**. A single flag (`mCreateOffsetEqualities` on +`CClosure`) gates only the two places where non-zero offsets are *created*: + +- `CCTermBuilder` / `addTermAxioms` — flag on: build the offset-free CCTerm + (`factor·affine`) and remember the constant offset; flag off: build the + whole-term CCTerm as today (offset 0). +- `createCCEquality` — flag on: `mOffset = o_b − o_a` from the `LASharedTerm` + offsets; flag off: `mOffset = 0`. + +Everywhere else (union-find, pair hash, signatures, proofs) is offset-uniform. +With every offset `ZERO` the arithmetic degenerates exactly to plain congruence +closure, so the existing test suite continuously exercises the offset plumbing +in its degenerate case. The flag also doubles as the proof guard: default it +**off when proof generation is enabled** until offset-aware proof production +lands, then flip. + +### CC↔LA sharing of offset-free terms + +When offset equalities are enabled, a numeric term `t = affine + c` is +represented by the offset-free CCTerm for `affine`, and the **`LASharedTerm` for +`t` has offset zero** (it represents `affine`, not `t`). Only this offset-free +CCTerm is `share()`d with the offset-free `LASharedTerm`, so the CC↔LA +shared-term equality has equal values on both sides and stays sound. The full +term `t` gets no separate CC↔LA share — the constant `c` (recomputed from the +polynomial on demand) bridges CC (`affine + c`) and LA (`affine + c`), both +anchored on the shared `affine`. When the feature is off, `LASharedTerm` keeps +the constant as its offset and the whole term is shared, exactly as before. + +### Entry pathway via LASharedTerm + +The offset machinery already exists in `LASharedTerm` (it stores summands and a +`Rational` offset). For `2x+4y+1` the normalized form is `factor·affine + +offset` with factor 2, affine `x+2y`, offset 1. The CCTerm represents the +offset-free `factor·affine` (`2x+4y`, shared with an `LASharedTerm` of offset +0); the factor stays in CC's term (so `2x+4y` and `x+2y` are *distinct* +CCTerms — the factor is LA's concern, not CC's). A `CCEquality` between two +LA-shared terms gets its offset from the difference of their `LASharedTerm` +offsets. + +## Increment plan + +1. **Weighted union-find foundation (done).** Added `CCTerm.mOffsetToRep`, + `CCEquality.mOffset`, `CCAppTerm.mArgOffsets`; threaded the offset delta + through `merge`/`mergeInternal`/`undoMerge` and added the same-class offset + consistency check. Behavior-preserving (all offsets `ZERO`); no creation site + emits non-zero offsets yet. +2. **Signatures + pair hash structure.** + - **2a (done).** Offset in `SignatureTrigger` hash/equals/recompute (the + effective offset is the term's offset to its representative plus the + structural `CCAppTerm.mArgOffsets`); the offset delta is threaded through + `rehashSignatures` and the merge/undo callers. + - **2b (done).** `CCTermPairHash.Info` carries an offset; the cuckoo-hash key + and `equals`/`getInfo` are offset-aware (offset hash invariant under + negation, so `(A,B,off)` mirrors `(B,A,-off)`). Zero-defaulting overloads + keep every existing call site querying offset 0, so it is behavior- + preserving. The actual offset-threading through the call sites is deferred + to increment 3, where it is testable. +3. **Creation sites + flag + call-site offset threading.** Implement the two + flag-gated creation sites (offset-free CCTerms from `LASharedTerm`; + `CCEquality.mOffset` from the offset difference) and, in the same increment, + thread the context-derived offsets through `insertEqualityEntry`, + `isDiseqSet`, `separate`/`undoSep`, `createCCEquality`, `backtrackComplete`, + `removeCCEquality`, the merge-time propagation in `CCTerm.mergeInternal`/ + `undoMerge` (propagate the matching-offset eqlits as true, the others as + negated), and `CompareTrigger.getOffset`. Then turn the flag on (off under + proof generation). This is the first end-to-end testable increment. +4. **Proof production.** Offset-aware `CongruencePath` / `CCProofGenerator` + (and proof checker), and the offset-conflict explanation stubbed in + increment 1; then enable offsets under proof generation. + +## Summary of files to change + +| File | Change | +|---|---| +| `CCTerm.java` | Add `mOffsetToRep`; update `merge()`, `invertEqualEdges()`, `undoMerge()` | +| `CCAppTerm.java` | Add `mArgOffsets: Rational[]` (null when all zero) for offset-shaped arguments | +| `CCEquality.java` | Add `mOffset: Rational` | +| `CCTermPairHash.java` | Add `mOffset` to `Info`; canonicalize key; update `getInfo()` and merge rehashing | +| `SignatureTrigger.java` | Include `mOffsetToRep` in `computeHashCode()`, `recomputeHashCode()`, `equals()` | +| `CongruencePath.java` | Accumulate signed offsets along equality-edge chain | +| `CCProofGenerator.java` | Emit offset-equality proof steps | +| `CClosure.java` | Update `addEquality()` entry point; update `share()` to propagate offset to LA | +| `LinArSolve.java` | Post offset equalities (`k ≠ 0`) to CC, not only zero-offset equalities | + +## Resume point + +Branch `offsetequality`. Done and committed: increments 1, 2a, 2b, 3; the +deterministic pair-hash offset; the shared-term polynomial-flattening fix +(`test04`); the quantifier gate (`quanttest001`); the `CCParameter` / +`OffsettedCCTerm` abstraction with `getValueKey()`; and the `checkCongruence` +migration. **Also done (this session, gap 1, the array migration):** +`ArrayTheory` and `WeakCongruencePath` are now offset-aware — every array index +is read as a `CCParameter` (`getIndexParamFromSelect`/`getIndexParamFromStore`) +and all index-keyed maps/sets (`mSelects`, `seenStores`, `nodeMapping`, +`storeIndices`, `seenIndices`, `mArrayModels`, the weakeq-ext `inverse` map) key +on the value identity `getValueKey()` instead of the bare representative; index +comparisons use `sameValueAs`/`.equals(valueKey)`; index disequality literals in +array lemmas are offset-aware (`createIndexEquality` / `computeIndexDiseq` via +`createEquality(t1, t2, offset, …)`, dropping the always-false disjunct when the +two indices share a CCTerm but differ by a constant); and `ModelBuilder` gained a +`getModelValue(CCParameter)` overload so array models store at the true index +value (rep value + offset), not the representative's value. This relies on the +array theory rebuilding from scratch each `buildWeakEq`/`computeWeakeqExt` pass, +so value keys are a stable snapshot. `WeakCongruencePath` navigates by value key; +its `computePath` calls only collect reason atoms (sound clause) — the net offset +matters only for the proof object, which is disabled while offsets are on. +`WeakSubPath.mIdx` stays a bare CCTerm for the (offset-disabled) proof annotation. + +Results: `array/difftest004` now SAT with a **correct** model (previously a wrong +model — the index offset was dropped); `nia/divaxiom2` and `abv/ext02` no longer +crash (`ext02` correctly UNSAT). All `array/` benchmarks pass; no crashes in +`abv/`,`bv/`. + +**Done (commit `858013d5`):** gaps 2 and 3 — LA→CC offset-equality propagation, +eager offset (dis)equality propagation at merge time, and the offset-aware +conflict explanation (`computeAntiCycle`/`CongruencePath.computeAntiCycle`, +`setLiteral` polarity, eager disequality explanation in `getPropagatedLiteral`, +`checkPending`/`separate` allowances). `bv/test01` fixed; `abv/ext01` no longer +crashes. + +**Done (commit `0c94d305`) — the CC↔LA sharing redesign:** the `LASharedTerm` now +carries the term's **full value** (its constant), instead of being offset-free. +The offset-free design handed LA the wrong value (`(a+255) mod 256` shared as +`−255` not `0`), so `mbtc` (which groups shared terms by value) never matched it — +the fingerprint bucketing was papering over this and over-collided. Sound because +the offset-free CCTerm and full `LASharedTerm` share the same LinVars (constant +only affects the `LAEquality` bound), and the CC-level offset equality still comes +from `EqualityProxy`'s term constants. `share()` decoupled so each distinct +full-value `LASharedTerm` reaches LA while the offset-free CCTerm is shared with +CC once. The non-constant fingerprint bucketing is **removed**; `mbtc`/ +`propagateSharedEqualities` are value-based again. **Fixes `abv/indexInRange01` +and `ufbv/ufbv01`.** + +**SystemTest: 6 → 1.** The lone remaining failure `model/buggy001` is a +pre-existing array-model assertion (`assert mArrayModels != null` in +`ArrayTheory.fillInModel`), unrelated to offsets. All other unit tests green. +`array/difftest004`, `abv/ext01/02`, `bv/test01`, `nia/divaxiom2`, +`abv/indexInRange01`, `ufbv/ufbv01` all fixed this round. + +**Next:** (1) the still-open *completeness* item from the design discussion — +share **every** numeric `CCAppTerm` argument with LA (a plain variable used only +as a function argument never enters LA today; create a LinVar for it), so an +argument that becomes CC-equal to a shared term can't be missed. No current +benchmark exercises it, but it's the textbook-complete shape. (2) **DONE — offset-less +checkpoint propagation.** `fingerprintSharedVar` now drops the constant (the `null` +key, holding the offset and any fixed-variable contributions) when +`createOffsetEqualities()`, so two shared terms collide when their *non-constant* +parts agree, i.e. they are provably equal up to a constant. `propagateSharedEqualities` +recovers the exact offset from the (model-independent) value difference and calls the +4-arg `propagateSharedEquality`, which now propagates implied **offset** equalities to +CC at checkpoint (the LA→CC dual of `CCTerm.mSharedTerm`), not just zero-offset ones. +The early-out guard is offset-aware: same affine class *and* matching offset → already +known (skip); otherwise the `propagated` hash dedups (the same-class/different-offset +self-pair `(rep, rep)` lets CC raise the offset conflict exactly once). The shared-var +loops iterate a snapshot, because propagating an offset equality synthesizes and shares +a `rhs + offset` term (appends to `mSharedVars`); it is offset-free-equivalent to an +existing term and the guard recognizes it as known on the next call. No-op when offsets +are disabled (full-value fingerprint, every offset 0). SystemTest unchanged at 1 +failure (`model/buggy001`, pre-existing). (3) the offset-aware **proof +object** to enable offsets under proofs; offset-aware e-matching; deferred +`DataTypeTheory` numeric-field handling. (4) consider removing the +`CCEquality`↔`LAEquality` linkage (bigger; has proof-generation consequences). + +**Remaining system-benchmark failures** (with proofs/interpolants disabled so +offsets are exercised): `bv/test01`, `abv/indexInRange01` (both unsat → **sat**, +*unsound*, but pre-existing before the array migration — they stem from gap 2, +LA knowing e.g. `k mod 256 = 1` but never telling CC). These are the motivation +for gap 2 and should be the next target. + +**Temporary working-tree change (uncommitted):** +`SMTInterpolTest/src/system/SystemTest.java` has `:proof-check-mode`, +`:proof-level` and `:interpolant-check-mode` commented out so the system +benchmarks exercise offsets (offsets are forced off under proof generation). +Revert this before merging. + +## Status after increment 3 + +Increments 1, 2a, 2b and 3 are implemented and committed; offsets are live for +quantifier-free, non-proof problems and the full unit suite passes. Two further +fixes landed: +- the shared-term equality must use a *flattened* polynomial term + (`Clausifier.addConstantToTerm`), otherwise a nested `(+ t offset)` is + re-parsed with the inner sum collapsed to one monomial (was unsound, e.g. + `bv/test04`); +- offsets are disabled in the presence of quantifiers (`mQuantTheory != null`), + because e-matching binds variables to offset-free CCTerms and would lose the + offset (was unsound, e.g. `quanttest001`). + +Running the system benchmarks with proofs/interpolants disabled (so offsets are +exercised) leaves these failures, all in offset-heavy machinery and **none of +them unsound** (crashes or incompleteness): `array/difftest004` (crash), +`nia/divaxiom2` (crash), `bv/test01`, `abv/indexInRange01`, `abv/ext02` +(unsat → unknown). They stem from the gaps below. + +## Remaining gaps + +1. **Every `CCAppTerm` consumer must treat argument `i` as `getArgument(i) + + getArgOffset(i)`** (the most important gap). Consumers that currently drop the + offset: `ArrayTheory` (select/store indices and array arguments), + `DataTypeTheory`, `CClosure.getCCTermRep`/`getAllFuncApps`/`checkCongruence`, + and the e-matching (`GetArgCode`/`EMReverseTrigger`, currently moot because + offsets are off under quantifiers). Already fixed: `ModelBuilder.addApp`. This + is behind the array crash and the BV/ABV incompleteness. +2. **LA → CC offset-equality propagation (DONE, commit `858013d5`).** In + `LinArSolve.propagateSharedEqualities`, when `createOffsetEqualities()` the + shared-term fingerprint is keyed on its *non-constant* part (the `null` entry + holds the constant from `addToFingerprint`/`fpr.add(shared.getOffset())`), and a + collision propagates `value(a) == value(b) + k` (`k` = constant difference) via a + generalized `propagateSharedEquality(lhs, rhs, offset, …)` that builds `rhs + k` + with `addConstantToTerm` and reuses `EqualityProxy.createCCEquality` (which + derives the CCEquality offset from the term constants and links the + `LAEquality`). A true-proxy guard skips the tautology between two distinct + constant terms. Off when offsets are off (constant stays in the key). Fixed + `bv/test01`. +3. **Eager offset (dis)equality propagation + offset-aware conflict explanation + (DONE, commit `858013d5`).** `CCTerm.mergeInternal` now propagates the + *disequality* for eqlits whose offset differs from the merge offset (mirroring + the existing matching-offset *true* propagation). The conflict/explanation side + is offset-aware: `CClosure.computeAntiCycle` + `CongruencePath.computeAntiCycle` + explain an offset disequality whose two sides are in the *same* class at a + different offset via the path between them (`{¬eq, ¬path}`), since the offset-0 + temporary-merge trick cannot work once the classes are merged; `setLiteral`'s + same-class conflict uses the same form; and — the key fix — `getPropagatedLiteral` + computes a propagated disequality's explanation **eagerly** (offsets only), + while the congruence graph still matches the trail, storing it on the atom so + `DPLLEngine` does not recompute it later from a state where a subsequent offset + merge (undone only at decision-level backtrack) has joined the two sides. + `checkPending`/`separate` allow the new same-class / decided-false cases. Fixed + `abv/ext01` (was crashing) and `abv/ext02`. **SystemTest 5 → 4 failures, no new + failures, all other unit tests green.** + + *Insight (Jochen):* the offset-differs disequality propagation is not the + problem — the old `mDiseqReason` anti-cycle is, because you cannot temporarily + insert the equal-edge when the classes are already equal at a different offset; + computing the explanation eagerly at propagation time sidesteps it. `CongruencePath` + itself needs **no** offset tracking for the clause: it only collects the path + edges; δ vs k lives in `CClosure`. (Net offsets are only needed for the eventual + *proof object*, still deferred and moot while offsets are off under proofs.) + +## CCParameter: a value `CCTerm + Rational` + +To stop every consumer from re-deriving `getArgument(i) + getArgOffset(i)` / +`mOffsetToRep` arithmetic, introduce a small abstraction for "a value up to a +constant" (array index, congruence argument, shared-term comparison): + +```java +public interface CCParameter { // value == getCCTerm() + getOffset() + CCTerm getCCTerm(); + Rational getOffset(); // structural offset; ZERO for a bare CCTerm + default CCTerm getRepresentative() { return getCCTerm().getRepresentative(); } + default Rational getOffsetToRep() { return getCCTerm().getOffsetToRep().add(getOffset()); } +} +``` + +- It must be an **interface** (not a base class): `CCTerm` already + `extends SimpleListable`, so it `implements CCParameter` with `getCCTerm() = + this`, `getOffset() = ZERO` — no extra object for the common offset-0 case. +- `OffsettedCCTerm implements CCParameter` is an immutable `(mTerm, mOffset)` with + `mOffset != ZERO`. Factory `CCParameter.of(t, off)` returns the bare `t` when + `off` is zero, so an offset-0 value is *always* a bare `CCTerm` (canonical + representation; no ambiguous `OffsettedCCTerm(t, 0)`). +- `SignatureTrigger` can use `CCParameter[]` instead of the parallel + `CCTerm[] mTerms` + `Rational[] mArgOffsets`, with the rehash-on-rep-change it + already performs. + +**Array index keys.** A `CCParameter`'s value identity +`(getRepresentative(), getOffsetToRep())` *changes on merge* (rep and offset both +shift), so it cannot be a naive persistent `HashMap` key. However, `ArrayTheory` +does **not** keep persistent index-keyed maps across merges: it **rebuilds its +weak-equivalence structures from scratch on every index merge** (the class shape, +primary/secondary store edges can change completely — e.g. when a secondary-edge +index becomes equal to the primary-edge index a different secondary edge must be +chosen). So **no rehashing is needed**: within one rebuild the representatives and +offsets are a fixed snapshot, making a value-identity key stable for that rebuild. + +The adaptation is therefore: where the array theory keys an index on +`index.getRepresentative()` today, key it on the value identity +`(getRepresentative(), getOffsetToRep())` instead — a small value-identity key +wrapper built fresh during the rebuild. The index value itself is read as +`CCParameter.of(app.getArgument(idxPos), app.getArgOffset(idxPos))`, which +supplies those accessors uniformly (a bare `CCTerm` when the index has no +offset). Note this composite key cannot be the bare `CCParameter`/`CCTerm` +directly, because `CCTerm`'s `hashCode`/`equals` are object identity, not the +value identity `(rep, offsetToRep)` the array theory needs. + +## Gap-fix order + +1. `CCParameter` + `OffsettedCCTerm`; wire the `CClosure` consumers + (`getCCTermRep`, `getAllFuncApps`, `checkCongruence`). **(done)** — + `DataTypeTheory` still deferred (only matters for numeric datatype fields). +2. `ArrayTheory` offset-aware index handling (gap 1, the substantial one). + **(done — incl. `WeakCongruencePath` and `ModelBuilder.getModelValue`.)** +3. LA → CC offset-equality propagation (gap 2) + eager offset (dis)equality + propagation (gap 3) + offset-aware conflict explanation. **(done, commit + `858013d5`; SystemTest 5 → 4 failures.)** +4. Eager negated-equality propagation (gap 3). +5. Proof production (increment 4) and offset-aware e-matching (re-enable offsets + under quantifiers) — both deferred to the quantifier-theory rework. + +--- + +# Redesign: offset-free sharing + clash-slot MBTC (planned) + +This redesign supersedes the full-value `LASharedTerm` approach (commit `0c94d305`, +documented under *Resume point*). It returns to **offset-free** CC↔LA sharing — +which is the sound Nelson-Oppen shape — and fixes the reason full-value was +adopted in the first place by changing *what* model-based theory combination +(MBTC) iterates over. + +## Why full-value sharing is the wrong lever + +The shared object and its CCTerm must denote the **same value** for the +Nelson-Oppen shared-term equality to be sound. Full-value `LASharedTerm` violates +that: the shared CCTerm is offset-free (value `2x+4y`) while the `LASharedTerm` +carries the constant (value `2x+4y+1`). Every LA→CC step then has to un-bend the +mismatch with `getTermConstant`, and that bridging is where bugs live (e.g. the +"already known" guard in `LinArSolve.propagateSharedEquality` compares an +offset-free CC offset against a full-value LA offset — mismatched units). + +Full-value was adopted only because MBTC groups *whole shared terms by their LA +value* (`getSharedCongruences`), and offset-free whole terms carry the wrong +canonical value (`(a+255) mod 256` shows up as `−255`, not `0`), so the value +buckets never matched. The real defect is that **MBTC is asking the wrong +question**: the equalities CC needs are not between whole numeric terms, they are +between the argument `CCParameter`s that sit at congruence (and reverse-trigger) +positions. Those carry their constant in the structural offset, so comparing +*them* by value is correct even when the shared terms are offset-free. + +## The three sharing jobs, and where offsets land + +- **CC → LA (merge propagation).** Unchanged. `CCTerm.mSharedTerm` + + `CCTerm.propagateSharedEquality` already work entirely in offset-free space + (the `LAEquality` is built from offset-free flat terms and the `mOffsetToRep` + difference). Sound and untouched. +- **LA → CC, entailed (checkpoint).** `propagateSharedEqualities` over + offset-free `LASharedTerm`s. It computes the exact value difference from the + model after a fingerprint collision, so it never relied on a (wrong) canonical + value; with offset-free shares it is correct *by construction*, and the + unit-mismatch guard disappears (no constant to bridge). +- **LA → CC, model-based (MBTC).** Redesigned to iterate over **numeric clash + slots** (see below) instead of whole `LASharedTerm`s. Equalities only. + +## `LASharedTerm` becomes offset-free + +When offset equalities are on, the `LASharedTerm` for a numeric term shares the +**offset-free** value (constant dropped), 1:1 with the offset-free CCTerm and +value-consistent with it. Terms that differ only by a constant (`2x+4y+1`, +`2x+4y+5`) collapse to the same offset-free shared entity; their distinctness +lives only in the SMT term layer and surfaces as the structural offset of a +`CCParameter` at the use site (a function argument). So `LASharedTerm`'s role +narrows to the offset-free value handle that the checkpoint propagation reads. + +## The clash slots (enumerated on demand, not a persistent index) + +A *clash slot* is a key `(FunctionSymbol, argPosition)`; its members are the +numeric `CCParameter`s occupying that argument position. Two members in the same +slot whose values coincide but whose CC classes differ are exactly the equalities +MBTC should propose — merging them is what fires the trigger CC cares about +(congruence, or a reverse trigger). + +**No persistent `Map>` is maintained.** MBTC runs +only in `finalCheck` (`LinArSolve.finalCheck`, after bounds / integrality / +`mutate`), which is rare, so the slots are **enumerated on demand** at that point. +This dissolves the earlier "how/when slots are added and undone on +merge/backtrack" question entirely — there is no lifecycle to mirror. The two +sources: + +- **Congruence source (covers EUF / QF_UFLIA).** Each numeric argument position + `(sym, pos)` is a slot; its members are `app.getArgParam(pos)` over + `getAllFuncApps(sym)`, restricted to numeric argument sorts. Two arguments at + the same `(sym, pos)` with equal value may make their applications congruent. + This source is **necessary**: plain congruence runs through `CongruenceTrigger` + (a `SignatureTrigger`) and the per-app `FindTriggerTrigger`, never a + `ReverseTrigger`, so the reverse-trigger source below yields *nothing* for a + pure `f(a)`/`f(b)` numeric clash. The old whole-term `mbtc` handled QF_UFLIA, so + dropping these positions would regress completeness. Enumerated on demand from + `getAllFuncApps`. +- **Reverse-trigger source (covers e-matching, and datatypes via a feed).** + Reverse triggers already carry `getFunctionSymbol()` and `getArgPosition()`, and + the `MasterReverseTrigger` / `ReverseTriggerTrigger` machinery already groups the + n-th argument of every application of that symbol at that key — so this source + is **self-maintaining and free**. The `-1` convention is the discriminator: + - A *find* trigger (watches the whole symbol for new apps) returns + `getArgPosition() == -1` — `MasterReverseTrigger.getArgPosition()` returns + `-1` by contract; e-matching installs these via `insertFindTrigger(func, …)` + with `argPos = -1`. **LA ignores these** — they are not clash positions. + - A *real* reverse trigger returns `getArgPosition() != -1` (e.g. + `EMReverseTrigger → mArgPos`, installed via `insertReverseTrigger(func, arg, + argPos, …)`). This **induces a clash slot** keyed `(getFunctionSymbol(), + getArgPosition())`. Restrict to **numeric** argument sorts on top of this. + +So clash slots = +`{(sym,pos) : reverse trigger with pos != -1 and numeric arg}` ∪ +`{numeric arg positions via getAllFuncApps(sym)}`. + +### Datatypes: a numeric-position reverse-trigger feed + +The datatype "result-vs-argument" clash — `sel_i(d)` against the i-th argument of +`cons(x)`, the speculative complement of the existing DT_PROJECT loop in +`DataTypeTheory` that only fires once `d` is already known equal to a `cons` — does +**not** fall out of the existing reverse triggers. `DataTypeTheory` installs its +reverse triggers at `(selectorFunc, 0)` / `(isFunc, 0)`, i.e. on the +**datatype-typed** argument, which the numeric-sort filter discards. The numeric +clash is between the *selector result* `sel_i(d)` and the *constructor argument* +`cons(x).getArgParam(i)`. + +`DataTypeTheory` already computes the selector↔(constructor, position `i`) +correspondence (it walks `constructor.getSelectors()` and reads +`consTerm.getArgParam(i)` in the DT_PROJECT path). The hookup is therefore a +**light, dedicated feed**: register a reverse trigger at the constructor's numeric +argument position `(cons, i)` (or equivalently inject the offset-free selector +result into that slot). Expressed in the reverse-trigger vocabulary it becomes +uniform — the `(cons, i)` slot then holds both the constructor arguments and the +selector results, and the standard clash machinery proposes `sel_i(d) = x_i`. This +is the still-deferred "numeric datatype field" handling; until it lands, datatype +numeric fields are simply not clashed (no soundness impact). **Also decide** +whether `ArrayTheory` needs to contribute slots for numeric `select` results / +`store` values, or already covers those value equalities through +weak-equivalence. + +## MBTC over clash slots + +For each slot: + +1. **Filter to shared reps.** Keep only members whose + `getRepresentative().mSharedTerm != null` — their class has an LA value. A + member whose class is LA-free cannot be forced to clash (model construction + gives it a non-clashing value), so it is ignored. This removes the + "force a LinVar on every numeric argument" prerequisite entirely. +2. **Value each member.** The class's LA value is carried by `rep.mSharedTerm`, + which need not *be* the rep and has its own `getOffsetToRep()`. So the member + value is **not** simply `value(rep) + param.getOffsetToRep()`; it is + + ``` + value(param) = sharedTermValue(rep.mSharedTerm) // LA value of the offset-free shared term + − rep.mSharedTerm.getOffsetToRep() // shift back to the rep + + param.getOffsetToRep() // shift out to the member + ``` + + (sign convention from `CCParameter.sameValueAs`: `value(p) = value(rep) + + p.getOffsetToRep()`). `rep.mSharedTerm` is a `CCTerm` and already carries + `getOffsetToRep()`, so **no new field is needed** — just fold the lookup in. +3. **Propose offset equalities** for members that are equal-valued but in + distinct CC classes: `u = v + (c_v − c_u)`. For a reverse-trigger slot this + merge then activates the existing trigger (e.g. the datatype propagation). + +**Equalities only.** LA never sends CC a disequality literal; the other theories +read "not provably equal" as disequal. The only residual obligation is implicit: +at model construction, shared `CCParameter`s in *distinct* CC classes must get +*distinct* values. The existing `sharedPoints` / `choose` / `hasSharing` +collision-avoidance handles this, but it must become **offset-aware**: today it +de-collides *raw* `LASharedTerm` values, whereas a clash-slot member's value is +`repValue + offset`, so the quantity kept distinct must be the offset-shifted +member value, not the raw shared value. + +## Model construction consequence + +LA-free numeric CC classes (those whose rep has no shared term) get no value from +LA. This is **already supported**: `ModelBuilder.fillInTermValues` routes a numeric +representative with `getSharedTerm() == null` into a `delayed` set and assigns it a +fresh value via `mModel.extendFresh(...)` after all LA-derived values are placed. +So "the bit of extra model-construction work" is mostly pre-existing. **Confirm** +only that `extendFresh` avoids colliding with the LA-assigned values, so distinct +classes (one LA-valued, one LA-free) stay distinct. + +## Net effect + +The value-mismatch bug class is removed by construction (shares are +value-consistent; MBTC computes correct per-argument values), offsets become +intrinsic to the argument positions where they matter, and the clash slots unify +the congruence and reverse-trigger sources behind one `(sym, pos)` key — with no +persistent structure to maintain. + +## Open items to settle in code + +1. **Datatype numeric-field feed.** Implement the constructor-numeric-position + reverse-trigger feed described above; confirm it produces exactly the + `sel_i(d)` vs `cons` i-th-argument clash. Decide array `select`/`store` value + contributions (or confirm weak-equivalence already covers them). +2. **Offset-aware collision avoidance.** Make `sharedPoints` / `choose` / + `hasSharing` de-collide offset-shifted clash-slot member values; confirm + `extendFresh` for LA-free classes avoids the LA-assigned values. +3. **Per-position completeness** is complete for SAT/UNSAT congruence; it drops + equalities between shared terms sharing no signature position, which are + irrelevant to solving. + - *Interpolation* is only used in the UNSAT case and is **not** affected by + MBTC (MBTC only feeds the SAT/model search). The single obligation is that an + MBTC equality which is *propagated* (not merely suggested) and lands in an + UNSAT core be proof-producible — that is exactly the deferred offset-aware + proof-object work, not a separate interpolation concern. Defer. +4. **Flag interaction.** When offsets are off, `LASharedTerm` stays full-value and + MBTC stays whole-term, exactly as today. + +## Status: implementable slice DONE (uncommitted) + +Steps 1–4 below are implemented (`Clausifier`, `CClosure`, `LinArSolve`, plus an +`EqualityProxy` fix) and validated under the temp proof-gate (SystemTest 22→21). +One additional fix was required beyond the four steps: a numeric disequality whose +`CCEquality` had no linked `LAEquality` never reached linear arithmetic, so model +construction could build an invalid model (e.g. `lia/divdiv5`). Whole-term mbtc +created that `LAEquality` as a side effect; clash-slot MBTC does not (the terms are +not function arguments). Fix: `EqualityProxy.createAtom` eagerly creates and links +the `LAEquality` for numeric equalities when `createOffsetEqualities()`. + +Two SystemTest regressions remained, both in the offset proof/interpolation track: +- `abv/ext01`: a clash-MBTC offset equality reached, via congruence, the stub + `CCTerm.merge` "offset equality conflict explanation not yet implemented" (the + congruence-merge analogue of the `computeAntiCycle` case `setLiteral` already + handles). **FIXED** — see *Congruence-merge offset conflict* below; `ext01`/`ext02` + now solve `unsat` and proof-check. +- `interpolation/uflratest004`: the eager `LAEquality` changes the proof so the + offset-unaware interpolant checker rejects the interpolant. The eager `LAEquality` + is kept (needed for SAT model soundness); offset interpolation is deferred. + +## Congruence-merge offset conflict (the `CCTerm.merge` stub) — DONE (uncommitted) + +The stub `CCTerm.merge` threw `UnsupportedOperationException("offset equality +conflict explanation not yet implemented")` in the `src == dest` branch (the two +merged terms are already in one class) when the merge offset disagreed with the +offset they already have. It is reachable **only** from `buildCongruence` +(`lhs.merge(this, rhs, null)`, `reason == null`): two congruent `CCAppTerm`s +`f(x)`, `f(y)` — congruence implies value difference `0` — are already in the same +class at a non-zero offset `existingDiff` (e.g. the class already records +`f(x) = f(y) + k`). `setLiteral` is the only other `merge` caller and it guards +`src != dest`, so the `reason != null` case never reaches this branch (asserted). + +Why `computeAntiCycle` could not be reused directly: it is keyed on a real +`CCEquality eq` — it negates `eq` in the clause and reads `eq.getOffset()` as the +deviating offset, and in the proof its prepended leading edge is discharged by +resolving against the `eq` literal. The congruence merge has `reason == null`: +there is no literal to negate, the deviating "edge" is a congruence justified by +the *argument* equalities (so the clause carries `{¬argEq…, ¬path}` with no positive +literal, like `computeCycle(const, const)`), and the leading proof edge must be +discharged by a congruence sub-proof, not a literal. + +Implementation: `CClosure.computeCongruenceAntiCycle(CCAppTerm, CCAppTerm)` → +`CongruencePath.computeCongruenceAntiCycle`. +- **Clause:** collect `mAllLiterals` from `computePath(second@0, first@0)` (the + existing class path, establishing `existingDiff`) plus `computePath` over each + argument pair `getArgParam(i)` (justifying the congruence); negate all. The + contradiction is intrinsic to arithmetic (`0 != existingDiff`), so no positive + literal is needed. +- **Proof object** (mirrors `computeAntiCycle`'s leading-node trick — a `SubPath` + cannot carry one CCTerm at two offsets): explicit + `mainPath = [first@0, second@0, …, first@existingDiff]` via the + `CCAnnotation(diseq, mainPath, otherPaths, CONG)` constructor (the one + `computeAntiCycleDiffClass` uses). The leading step `first@0 → second@0` has no + clause literal and is not a registered subpath, so + `CCProofGenerator.collectEquality → findCongruencePaths` auto-resolves it from the + argument subpaths in `otherPaths` (= `mAllPaths` minus the inlined existing path, + retrieved via `mVisited.get(SymmetricPair(second, first))`). The two endpoints are + the same term `first` at offsets `0` and `existingDiff` — a trivially-false offset + disequality discharged by an EQ lemma. + +Validated (clean build, assertions on): `abv/ext01`, `abv/ext02` now `unsat` (were +`unsupported`); `BitvectorTest` 89/89 with FULL proofs + `proof-check-mode` (the +proof object verifies); `test/proof` 97/98 (only the pre-existing +`trivialdiseqarray` array+offset blocker, identical at baseline); +`CongruentAddTest`/`PairHashTest`/`ProofSimplifierTest`/`ProofUtilsTest`/`RPITest` +green. The change is gated only by the structural shape of the merge, not by +`createOffsetEqualities()` directly, but it is dead code when offsets are off (every +offset is then `0`, so `existingDiff` is always `0`). Files: `CCTerm.java`, +`CClosure.java`, `CongruencePath.java`. + +Still-deferred gate-flip blockers (confirmed pre-existing, identical at baseline): +`ProofSimplifier.checkProof` on offset lowlevel proofs (e.g. `bv/bvand0*`) and +`QuantClause.collectVarInfos` on quantified datatype matching (e.g. +`datatype/quantified/match_test`). + +## Shared-term clash during a merge (the `computeCycle` constant case) — DONE (uncommitted) + +`uflira/uflira_001.smt2` (`to_real a = f a`, `f b = 1/2`, `a = b`) was `unsat` +correctly but the **proof object** failed: `Cannot explain term pair +((f b)+-1/2,(f a))`. Same root cause as `computeAntiCycleDiffClass`: an equal edge +was added to the CC graph but the nodes were not yet really merged. + +Mechanism: asserting `a = b` makes `f a ≡ f b` by congruence. `CCTerm.mergeInternal` +tries to merge `f a`'s class (shared term `to_real a`) with `f b`'s class (shared +term `0.0`, with `f b` at offset `-1/2`). The merged shared-term equality +`to_real a == 0.0 + (-1/2)` is integer-impossible (`a` is `Int`), so +`createEquality` returns the false proxy → `sharedTermConflict`, and the conflict was +built by `computeCycle(0.0, (to_real a)+(-1/2))`. But the conflict is detected +**before** the union-find update: the equal edge `f a — f b` is set while +`mOffsetToRep` is still relative to each node's own representative. `computePath` +walks across the bridge fine for the *clause*, but `SubPath.getParams()` reads the +congruence bridge step as `(f b)+(-1/2) → (f a)+0` (two reference frames) instead of +offset-free → the proof generator cannot explain it. + +Fix: new `CClosure.computeSharedConflictCycle` → +`CongruencePath.computeSharedConflictCycle(lshared, rshared, lhs, rhsTerm, reason, +bridgeOff, …)`, a hybrid of `computeAntiCycleDiffClass` (two single-class halves +stitched by hand) and `computeCongruenceAntiCycle` (the bridge may be a congruence, +`reason == null`): +- **Clause:** `computePath(lshared@0, lhs@0)` (source half) + `computePath(rhsTerm@0, + rshared@0)` (dest half), each single-class and offset-correct. The bridge edge + `lhs — rhsTerm` is justified by the merge `reason` (added as a literal) when + `reason != null`, or — for a congruence merge — by `computePath` over each argument + pair `getArgParam(i)` (which also builds the subpaths the proof needs). Negate all; + no positive literal (the contradiction is the trivially distinct shared values). +- **Proof object:** build an explicit `mainPath = paramsSrc ++ paramsDest` via the + `CCAnnotation(diseq, mainPath, otherPaths, CONG)` constructor. The destination half + is computed **already shifted** into the source frame: instead of anchoring at + `rhsTerm@0` and shifting every node afterwards, the dest `computePath` is anchored at + `rhsTerm@shift` with `shift = (lshared.mOffsetToRep − lhs.mOffsetToRep) + bridgeOff` + (`bridgeOff = reasonDiff(reason, lhs, rhsTerm)`, `0` for a congruence). Since + `SubPath.getParams()` offsets every node by the anchor's `mStartOffset`, the dest + params come out in the source frame and `mainPath` is a plain two-`arraycopy` + concatenation — no post-hoc per-node shift. For a congruence bridge the step + `lhs → rhsTerm` (offset 0) is auto-resolved from the argument subpaths in + `otherPaths`, exactly like `computeCongruenceAntiCycle`'s leading edge; for a + real-equality bridge the step matches the `reason` literal. The diseq is taken from + the stitched path endpoints (`mainPath[0]`, `mainPath[last]`), so its offset is the + true value difference `value(lshared) − value(rshared)` (= `sharedOffset`), fixing a + latent bug where the old call passed `delta` instead — these coincide only when both + shared terms are their class reps. Discharged by an EQ/LA lemma. + +`reason == null` is the key complication the user flagged: there is no separating +disequality literal to resolve the bridge on. It is handled exactly as in +`computeCongruenceAntiCycle` (argument subpaths discharge the congruence step), so the +hybrid covers both bridge kinds uniformly. + +**CongruencePath refactor (same change).** Three cleanups landed alongside the fix: +1. `computeCCPath` renamed to **`computeCongruence`** (it pairs the arguments of two + congruent app terms); both cross-class builders (`computeCongruenceAntiCycle`, + `computeSharedConflictCycle`) now call it instead of hand-rolling the argument loop. +2. **`computePath` no longer drains** the work list — it only enqueues. Each top-level + `compute*Cycle`/`computeDTLemma`/`computeDecideLevel` calls the extracted + `drainTodo()` **once** after all `computePath`/`computeCongruence` calls are queued, + so a single shared drain builds them (and dedups subpaths shared between several + queued ends). `WeakCongruencePath` drains once per weak/main path; the strong paths + it inlines into a weak path are built via `computePathNonRecursive` (see the + *drainTodo / mAllPaths cleanup* section below — the original `computePath(...); + mAllPaths.removeFirst()` per-call-drain idiom is superseded). +3. **`computeAntiCycleDiffClass` gets the same pre-shift simplification** as + `computeSharedConflictCycle`: its right half is anchored at `right@shift` + (`shift = dLeft.mOffsetToRep − left.mOffsetToRep + eq.getOffset()`), so `getParams()` + returns it already in the left frame and `mainPath` is a plain concatenation. The + `assert mainPath[last].getOffset() == dOff` still holds. + +Validated (clean build, `-ea`): `uflira_001` proof now `unsat` with +`proof-check-mode` (was `Cannot explain term pair`); `test/proof` 97/98 (only the +pre-existing `trivialdiseqarray`, confirmed identical at baseline; `match_rewrite` +re-checks its full cross-class anti-cycle proof); +`test/uflira` 5/5, `test/lia` 32/32, `test/abv` 4/4 clean; `BitvectorTest` 89/89 +(FULL proof + proof-check), `ProofSimplifierTest` 14/14, `ProofUtilsTest` 4/4, +`CongruentAddTest` 5/5, `PairHashTest` 1/1. Pre-existing deferred crashes unchanged +(`datatype/quantified/*match*` = `QuantClause.collectVarInfos`). Files: +`CCTerm.java`, `CClosure.java`, `CongruencePath.java`, `WeakCongruencePath.java`. + +## `SubPath.getParams(anchor)` + the cross-class guard, and the merge-conflict-diseq stitch — DONE (uncommitted) + +A `SubPath` is a list of offset-free CCTerms; `getParams` renders them as offsetted +`CCParameter`s. It used to store the anchor (`mStart`/`mStartOffset`) on the SubPath. +Now `getParams(CCParameter anchor)` takes the anchor at render time (the relative +offsets are intrinsic — `getOffsetToRep` differences — so the anchor only fixes the +absolute base). `mStart`/`mStartOffset` are gone; the no-arg `getParams()` self-anchors +at the path's first node. This also subsumes the per-element "pre-shift" in +`computeAntiCycleDiffClass`/`computeMergeConflictCycle`: the destination half is rendered +with `getParams(rhsTerm@shift)`, so the main path is a plain concatenation. + +**Direction fix (Jochen caught it).** `mVisited` keys paths by an undirected +`SymmetricPair`, so a retrieved `SubPath`'s stored term list may run either way; the +stitch builders assumed `[start … end]`. `getParams(anchor)` now treats the anchor as the +intended start (it must be one of the two endpoints) and renders the term list in +reverse when the anchor is the stored last node, so callers always get `[anchor … other]`. +The three stitch sites anchor explicitly at their start endpoint +(`segSrc.getParams(srcEnd)`, `segB.getParams(right@shift)`, +`existing.getParams(second@0)`) rather than self-anchoring, and assert the main path runs +`srcEnd … destEnd` (comparing `getCCTerm()`, not the offsetted parameter). Validated by +temporarily reversing the build order of both halves: `abv/ext01` still proof-checks +`unsat`. + +**The cross-class assertion (Jochen's idea).** `getParams` asserts the anchor shares the +representative of every *numeric* node. `getOffsetToRep` is class-relative, so a path that +spans two classes mixes reference frames and yields garbage offsets — the bug behind every +cross-class offset conflict. Non-numeric terms can never carry an offset, so the legitimate +cross-class paths (weak-array paths over distinct strong classes) are exempt. + +**The lingering bug the assertion found.** With the strict guard, `computeCycle(diseq)` +called from `CCTerm.mergeInternal` (the `diseq != null` branch: a disequality forbids a +merge at exactly the merge offset) fired the assertion — it walks a single path across the +freshly added, not-yet-united merge bridge, exactly like the shared-term clash. It is the +eager-conflict twin of the lazy `computeAntiCycle → computeAntiCycleDiffClass` path, and was +never converted; it was silently wrong in the proof object whenever the bridge offset is +non-zero (the clause is always sound, so only proof-checking catches it). Fix: generalize +`computeSharedConflictCycle` into `computeMergeConflictCycle`, which takes an optional +concrete `diseqLit` (the shared-term clash passes `null`; the merge-conflict passes the +disequality, which becomes the cycle's positive literal). `CClosure.computeMergeDiseqCycle` +wraps it; `mergeInternal` orients the diseq's two sides into the source/destination class +(via pre-merge `mRepStar`) and calls it. Now both clash kinds build the two halves +separately — no cross-class numeric `getParams` remains. + +**Real trigger:** `abv/ext01.smt2` hits `computeMergeDiseqCycle` with `bridgeOff = -1` on +`(ubv_to_int (select d (@diff d e))) == (ubv_to_int (@diff d d))` — the abv/ext case where +this was first suspected. It now proof-checks `unsat`. + +Also removed dead code orphaned by the shared-term-clash change: `SubPath.getTerms()`, the +`CCTerm[]` `prepend` overload and the unused `terms`/`mainTerms` locals in `CCAnnotation`, +and the two-arg `computeCycle(CCParameter,CCParameter)` (CongruencePath impl + CClosure +wrapper). + +Validated (clean build, `-ea`; note `ant clean` is required after a stash/`cp` restore — a +stale `.class` newer than the restored `.java` is not rebuilt): `test/proof` 97/98, +`test/abv` 4/4, `test/bv` 35/35, `test/regression` 53/55, `test/uflira` 5/5, `test/lia` +32/32 — only the two documented pre-existing failures (`trivialdiseqarray` array+offset; +`Script_simple` `CCInterpolator occur==null` offset interpolation). `BitvectorTest` 89/89, +`ProofSimplifierTest` 14/14, `ProofUtilsTest` 4/4, `CongruentAddTest` 5/5, `PairHashTest` +1/1. Files: `CongruencePath.java`, `CCAnnotation.java`, `CClosure.java`, `CCTerm.java`. + +## All offset-cycle explainers consolidated into `computeMergeConflictCycle` — DONE (uncommitted) + +Jochen's observation: the three anti-cycle builders are all special cases of +`computeMergeConflictCycle`. The same-class ones are the degenerate case where the two +endpoints coincide (`srcEnd == destEnd == lhs`): the source half is empty and the +destination half is the existing class path from `rhsTerm` back to `lhs`, so the trivial +diseq `(lhs@0, lhs@(bridgeOff − pathOffset))` falls out of the same two-half stitch. The +three knobs span every case: + +- `srcEnd == destEnd` ⇒ same-class (anti-cycle), else cross-class (merge / diff-class). +- `reason == null` ⇒ congruence bridge (justified by argument equalities), else an equality + literal bridge. +- `diseqLit == null` ⇒ trivial/shared diseq (EQ/LA discharged), else a concrete disequality + literal carried positively in the clause. + +Mapping (all now `CClosure` → `CongruencePath.computeMergeConflictCycle`): +- `computeAntiCycle` same-class: `(eq.lhs, eq.lhs, eq.lhs, eq.rhs, eq, eq.getOffset(), null)`. +- `computeAntiCycle` diff-class: `(dLeft, dRight, eq.lhs, eq.rhs, eq, eq.getOffset(), diseq)` + with `dLeft`/`dRight` oriented via `eq.mDiseqOrientation` at the call site. +- `computeCongruenceAntiCycle`: `(first, first, first, second, null, ZERO, null)`. +- `computeMergeDiseqCycle` / `computeSharedConflictCycle` unchanged (eager merge conflict). + +Deleted `CongruencePath.computeAntiCycle`, `computeAntiCycleDiffClass`, +`computeCongruenceAntiCycle` (5 explainers → 1 + `computeCycle`, the bridgeless +positive-eq cycle which stays separate). `CClosure.setLiteral`'s same-class assert-true +case routes through a new `computeSameClassAntiCycle` helper (must force the same-class +shape — `eq` can carry a stale `mDiseqReason`). Cosmetic: the unified same-class anti-cycle +anchors its trivial diseq on `lhs` (`(lhs@0, lhs@dev)`) instead of the old `rhs` anchoring — +both are valid EQ-discharged trivial diseqs, proof-check unaffected. `CongruencePath` +−170 lines net. + +Validated (clean build, `-ea`): `test/proof` 97/98, `test/abv` 4/4, `test/bv` 35/35, +`test/regression` 54/55, `test/uflira` 5/5, `test/lia` 32/32, `test/datatype` non-quantified +clean — only the documented pre-existing failures (`trivialdiseqarray`, `Script_simple`, +`datatype/quantified/*match*` = `QuantClause.collectVarInfos`). `BitvectorTest` 89/89, +`ProofSimplifierTest` 14/14, `ProofUtilsTest` 4/4, `CongruentAddTest` 5/5, `PairHashTest` +1/1. Files: `CongruencePath.java`, `CClosure.java`, `CCAnnotation.java`. + +## Merge conflicts reported before mutating the graph — DONE (uncommitted) + +Since the conflict explainers build each half within its own class and never walk the +merge bridge, `CCTerm.mergeInternal` no longer needs the equal edge for a conflict. It now +detects the conflict (disequality forbidding the merge, or shared-term clash) and returns it +before `invertEqualEdges`/`mEqualEdge`/`mOldRep`/`mReasonLiteral` are set — dropping +the old add-the-edge-then-undo-it dance, so the conflict path mutates nothing. The diseq +orientation (`diseq.getLhs().mRepStar == src`) now reads a pristine union-find. **Bug fixed +while reviewing:** the first cut treated any non-null `diseqInfo` as a disequality conflict, +but a `CCTermPairHash.Info` also holds equality literals / compare triggers, so +`diseqInfo.mDiseq` can be null — the guard must be `diseqInfo != null && diseqInfo.mDiseq != +null` (otherwise `diseq.getLhs()` NPEs on essentially every benchmark). Validated: test/proof +97/98, abv 4/4, bv 35/35, uflira/lia/datatype-nonquant clean (only the pre-existing failures); +unit suites green. File: `CCTerm.java`. + +## `drainTodo` / `mAllPaths` cleanup — DONE (uncommitted) + +`mAllPaths` was overloaded: it is the final, topologically-ordered annotation output +(the producer must emit paths so that *later paths explain congruences on earlier* — see +`CCAnnotation.mParamPaths` and `CCProofGenerator.collectStrongEqualities`, which walks the +array backwards), **and** it was abused as a return channel — `WeakCongruencePath` read +`mAllPaths.removeFirst()` to retrieve the strong path it had just asked `computePath` to build +so it could inline it (`addSubPath`) into a weak path. That coupling caused two problems: + +- **Per-call dedup set.** `drainTodo`'s `added` set was local, while `mVisited` is a field. A + subpath built in one drain and re-enqueued in a later drain (`computeWeakeqExt` loops over + store indices; `computeCongruence` re-enqueues argument pairs unconditionally) was found in + `mVisited` but missing from the fresh `added` set → **re-appended to `mAllPaths` (duplicate)**. +- **`removeFirst` ordering reliance.** The graft assumed `mAllPaths`'s front was exactly the + path for the last `computePath` — fragile, since the shared drain also drains other pending + pairs (the index/select equalities), and it only stayed correct *because* the broken per-call + dedup kept re-adding already-built paths. + +**Fix (three changes):** +1. **Persistent dedup.** `drainTodo`'s local `added` set is promoted to the field `mCollected`, + so a subpath enters `mAllPaths` at most once across all drains of a lemma. (Per-conflict + instances are fresh from the constructor, so no clearing is needed.) +2. **Inline via `computePathNonRecursive`.** The two graft sites in `WeakCongruencePath` + (`collectPathOnePrimary`, `collectPathPrimary`) call `computePathNonRecursive(left, right)` + directly and `addSubPath` the returned `SubPath`. `computePathNonRecursive` returns the path + (cached or freshly built), enqueues its congruence dependencies on `mTodo`, and — crucially — + **never adds it to `mAllPaths`** (correct: an inlined path is not a standalone annotation + path). The dependencies are collected by the enclosing lemma's drain. This removes both the + `removeFirst` call and its ordering assumption; you get the right `SubPath` by return value + regardless of what else is pending in `mTodo`. Strong paths that *are* standalone annotation + subpaths (the index/select equalities in `computeWeakCongruencePath`) keep using `computePath`. +3. **Explicit drain in `computeConstOverWeakEQ`.** It was the one lemma method with no + `drainTodo()` of its own — it worked only because `collectPathPrimary` drained internally. + With the internal graft-drains gone, it now calls `drainTodo()` before `mAllPaths.addFirst(path)` + (the drain must precede the prepend so the main path lands ahead of its dependencies). The + other lemma methods already drain at the right spot before each manual `addFirst`. +4. **`computeMergeConflictCycle` builds its two halves the same way.** It used to + `computePath(srcEnd, lhs)` / `computePath(rhsTerm, destEnd)`, drain, then re-fetch the halves via + `mVisited.get(SymmetricPair(...))` and filter them back out of `mAllPaths` + (`for (p : mAllPaths) if (p != segSrc && p != segDest)`). Now it calls + `computePathNonRecursive` for each half directly: the `SubPath`s come back by return value (no + `mVisited` lookup), they never enter `mAllPaths` (no filter), and only their congruence + dependencies plus the bridge's argument subpaths are drained — so `mAllPaths` *is* the + other-paths list and is passed straight to the `CCAnnotation` constructor (which only iterates + it, so no copy). The two halves must be built before the drain regardless of `produceProofs`, + since that is where their literals reach `mAllLiterals` for the clause. + +Validated (clean build, `-ea`): `test/proof` **97/98** (only the pre-existing +`trivialdiseqarray`, identical to baseline); `array/` + interpolation `.smt2` under +`proof-check-mode` have an **identical pass/fail set** to a reverted baseline (the +`constarr00{4,5,11,12,14}` interpolation failures are pre-existing in the deferred +offset-interpolation track); `BitvectorTest`, `ProofSimplifierTest`, `ProofUtilsTest`, +`RPITest`, `CongruentAddTest` green (117 tests); `abv`/`uflira`/`lia` sweeps clean. +Files: `CongruencePath.java`, `WeakCongruencePath.java`. + +## Offset-aware array weak-eq propagation (the `trivialdiseqarray` blocker) — DONE (uncommitted) + +`trivialdiseqarray` (`a = (store (const ((select a j) + 1)) i 1)`, `i != j`) returned **sat** +for an unsat problem — the lone remaining `test/proof` failure. The unsat reasoning is: with +`i != j`, `a` and the const array agree at `j`, so `select(a, j) = const value = (select a j) + +1`, i.e. `x = x + 1`, a (same-class, different-offset) offset conflict. Two array-side spots +dropped the offset and suppressed it (an instance of gap 1, the `ArrayTheory` part): + +1. **Offset-blind propagation guards** (`ArrayTheory`, four sites: `READ_OVER_WEAKEQ` / + `READ_CONST_WEAKEQ` in both the primary-merge `mergeWith` path and the secondary-edge path). + They skipped propagation with `select.getRepresentative() != other.getRepresentative()`. Two + selects (or a select and a const value) in the *same* class but at *different* offsets are + **not** already equal — that is exactly the conflict — so the guard threw the conflicting + lemma away. Fixed to the offset-aware `!select.sameValueAs(other)` + (`(getRepresentative(), getOffsetToRep())` identity). The sibling `CONST_WEAKEQ` guard already + compared offsets; these four now match it. With offsets off, `sameValueAs` reduces to a + representative comparison, so behaviour is unchanged in the classic mode. +2. **Offset dropped building the lemma equality** (`WeakCongruencePath.generateClause`): it called + `createEquality(diseq.getFirst().getCCTerm(), diseq.getSecond().getCCTerm(), …)`, stripping to + bare `CCTerm`s → the degenerate `select(a,j) = select(a,j)` (which trips the `createEquality` + `t1 != t2 || offset != 0` assertion once the guard above fires). Fixed to pass the + `CCParameter`s to the offset-aware overload, which builds `select(a,j) = select(a,j)+1`, + recognizes the false proxy (`eq == null`, already handled), and lets the lemma become the + conflict clause over its premises. + +`trivialdiseqarray` is now **unsat**, with a low-level proof that proof-checks (`read-const-weakeq` +derives `select(a,j) = select(a,j)+1`; an `EQ`/farkas lemma refutes it). + +*Debug-output note:* the LA `Shared Vars` / `Assignments` dump (`LinArSolve.getSharedCongruences`) +prints `sharedTermValue`, the **offset-free** LA value with no structural constant, so e.g. +`(+ (select a j) 1)` printed `= 2` alongside `(select a j) = 2` (the `+1` is a CC offset, not in +the LA value) — misleading when reading a model, but not the cause of the bug. The +equivalence-class dump (`CClosure.dumpModel`) does print offsets (`[+k]`). + +Validated (clean build, `-ea`): `test/proof` **98/98** (`trivialdiseqarray` fixed, no +regressions); array-relevant sweep (93 benchmarks under `proof-check`) has **0 status mismatches** +and only pre-existing errors (`constarr00{4,5,11,12,14}` offset-interpolation, `Script_simple`); +`BitvectorTest`/`ProofSimplifierTest`/`ProofUtilsTest`/`RPITest`/`CongruentAddTest` green (117 +tests). Files: `ArrayTheory.java`, `WeakCongruencePath.java`. + +## Offset-aware array extensionality fingerprint + model values — DONE (uncommitted) + +An audit of `ArrayTheory` for offset correctness found index handling solid (everything keys on +`CCParameter.getValueKey()` = `(representative, offsetToRep)`) and the value-propagation guards +fixed by the previous increment, but **two value spots still dropped the offset** — the more +serious one a soundness bug in extensionality: + +1. **`computeWeakeqExt` model fingerprint (soundness).** The per-array `nodeMapping` that drives + `weakeq-ext` stored element values as bare representatives (`getRepresentative()`), so two arrays + whose elements agree only **up to a constant offset** got the same fingerprint, collided in the + `inverse` map, and `weakeq-ext` propagated a spurious `a = b`. Minimal witness (genuinely sat): + `(not (= (store c i y) (store c i (+ y 1))))` was reported **unsat** (the spurious `a = b` + conflicts with the asserted disequality). Fixed to store value identities + (`getValueKey()`) and compare with `equals()` (offset value-keys are fresh `OffsettedCCTerm` + objects, so the old `!=` reference test would not even match equal offset values); `constRep` and + the `defaultValue` cache become `CCParameter`. Precision only improves — two arrays still match + iff they agree at every index *including* offsets, so no valid extensionality is lost. The + witness is now **sat**. +2. **`fillInModel` element values (wrong models).** The const value (`getValueFromConst(..) + .getRepresentative()`), the per-index select values (`.getRepresentative()`), the finite-elem + default (read back from `mArrayModels`), and the secondary-edge select (`getModelValue(ccValue)`) + all fed a bare representative to `getModelValue`, dropping `offsetToRep`. The fix passes + `CCParameter`s (`getValueFromConst(..)` directly, or `getValueKey()` for selects). Verified + with `model-check-mode`: `a = (store (const (+ y 3)) i (+ y 1))`, `i != 0` yields a consistent + model (`y=-2`, `select(a,i)=-1`, `select(a,0)=1`). + +Follow-up: the error-prone `getModelValue(CCTerm)` accessor (returns the representative's value, +silently dropping a member's offset) was **removed** in favour of the single offset-aware +`getModelValue(CCParameter)` — a bare `CCTerm` is an offset-free parameter, so all former callers +(array/datatype/boolean terms, all non-numeric or representatives) bind to it with identical +behaviour, while any numeric member now necessarily goes through the offset-shifting path. Removing +the overload is binary-incompatible, so callers in other files (e.g. `DataTypeTheory`) must be +recompiled — a clean build is required (an incremental rebuild leaves a stale `.class` referencing +the deleted method and throws `NoSuchMethodError`). + +Validated (clean build, `-ea`): `test/proof` **98/98**; the extensionality witness is `sat` (was +`unsat` on the pre-fix build, confirming the soundness bug); array-relevant sweep (93 benchmarks, +`proof-check`) **0 status mismatches**, only the pre-existing errors; `datatype`/`model` dirs 30/30; +117 JUnit tests green. Files: `ArrayTheory.java`, `ModelBuilder.java`. + +## Explicit select/const edge in weakeq-ext annotations — DONE (uncommitted) + +Preparation for offsets under proofs. In a `weakeq-ext` lemma, each weak-i path may +contain one *select/const edge*: the step where two arrays are weakly-i-equivalent +not by a store or a strong equality but because a select equality +`select(a1,j1) = select(a2,j2)` (or `select(a1,j1) = v` for `a2 = (const v)`) holds +with `j1, j2` equal to the path index. Until now the edge was *not* recorded — three +consumers (`CCProofGenerator.findSelectPath`, `ProofSimplifier.proveSelectPath`, and +`ArrayInterpolator`) each re-derived it by iterating the clause equalities. With +offset equalities this search becomes ambiguous: the justifying equality is +offset-rendered (`EqualityProxy.getLiteral` builds `(= mLhs (+ mRhs off))`, and the +`CCTermPairHash` identity-hash canonicalization can move the constant onto the +*select* side, e.g. `(= core (+ (select a1 j1) off))`), so neither atom side is the +bare select the searcher matches on, and among several candidates only one is right. + +Fix: record the edge in the annotation and let the consumers read it. + +- **`WeakCongruencePath.WeakSubPath`** gains `mSelectLeft`/`mSelectRight` + (`CCParameter`, with offset), set by `computeWeakCongruencePath` — the one place + the edge is known (`select1`/`select2`), ordered `path[0]`-side then + `path[last]`-side. Plain weak paths leave it null; it is *always* null for + read-over-weakeq / read-const-weakeq (their weak paths have no select step). +- **`CCAnnotation`** carries a parallel `mSelectEdges[i]` (`{left, right}` or null) + populated from the `WeakSubPath`, with a getter. +- **Term format:** the `:weakpath` value stays `{index, subs}` when there is no edge + and becomes `{index, subs, {leftTerm, rightTerm}}` when there is one + (`CCProofGenerator.buildLemma`). Backward-tolerant: `ArrayInterpolator.ProofPath` + reads only `[0]`/`[1]` and ignores the third element. +- **`CCProofGenerator.collectWeakPath`** uses the annotated edge (oriented against the + step's two arrays via `isSelect`/`isConst` in the new `orientSelectEdge`); it only + falls back to `findSelectPath` if no edge is present. +- **`ProofSimplifier`** parses the edge from `:weakpath[2]` and threads it through + `proveSelectOverPath` → `proveSelectOverPathStep`, feeding the edge's + `getFlatTerm()` (a bare select, or the const's full stored value) directly to + `proveSelectConst`. This sidesteps the affine-extraction problem: those terms match + `proveSelectConst` as-is, and the offset-rendered clause equality is bridged by + `resolveNeededEqualities` (`OffsetEqKey` + `proveEqWithMultiplier`). Match is decided + by `proveSelectConst` succeeding on both sides, *not* by the returned proof — + `proveSelectPathTrans` legitimately returns `null` for a trivial step (the edge + selects coincide with the path-end selects), which must not be read as "no match". + `proveSelectPath` is kept only as a fallback for an absent edge. + +The change is validated with offsets *off* (the format machinery is exercised; +select edges are offset-0): the array + interpolation `proof-check-mode` + +`proof-level lowlevel` sweep is unchanged versus baseline (only the pre-existing +deferred offset-interpolation and datatype failures remain — `CCInterpolator` throws +on offset lemmas). Instrumentation on `constarr014` (68 select edges) confirms the +annotated edge is used for every select step and the search fallback is never hit in +any passing test. Files: `WeakCongruencePath.java`, `CCAnnotation.java`, +`CCProofGenerator.java`, `ProofSimplifier.java`. + +**Still open (the reason this is preparation):** `proveSelectConst` does not yet +handle a select/const edge whose select sits at an LA/offset-equal index, or a const +value carrying a non-zero offset — i.e. offsets *under proofs*. The edge is now +delivered explicitly (bare select + full const value), so this handling no longer has +to contend with the search ambiguity; it is the natural next step. + +## Syntactic `OffsetEqKey` (constant-last canonic sums) — DONE (uncommitted) + +`ProofSimplifier.OffsetEqKey` no longer parses its sides with `Polynomial` into +summand hashmaps; a new `OffsetTerm` splits a side purely syntactically — a trailing +constant summand of a `+` application is the offset, dropping it yields the +offset-free part (a plain `Term`, compared by identity). A side that is entirely +constant splits into the base `0` term and its value as offset, mirroring the `0` +base CCTerm of a plain-numeral parameter; correspondingly `CCParameter.addConstant` +folds a constant base into a plain constant (`5`, not `(+ 0 5)`), agreeing with the +canonic term of that value (and with `Clausifier.addConstantToTerm`). Constants in +any *other* position stay inside the offset-free part (harmless when offsets are +off, since keys then only compare identical flat terms). + +This is sound because the split is now the exact inverse of term construction: +`TermCompiler.unifyPolynomial` canonicalizes a constant-carrying polynomial as +`CCParameter.addConstant(unifyPolynomial(constantFreePart), constant)`, so canonic +compiler terms, `Clausifier.addConstantToTerm` results and annotation flat terms +(`CCParameter.getFlatTerm`/`addConstant`) are all byte-identical per value, with the +constant last. `BvToIntUtils` was the one producer bypassing the unifier (direct +`Polynomial.toTerm`, e.g. `normalizeMod` emitted `(+ x 255 (* -256 (div …)))` with +the constant mid-sum, which made `resolveNeededEqualities` miss the clause literal +and emit an invalid trichotomy farkas on `abv/indexInRange01`, `ufbv/ufbv01`); all +its polynomial-term exits now go through `mPolyUnifier`. Full unit suite matches the +baseline (only the known pre-existing SystemTest failures). Files: +`ProofSimplifier.java`, `TermCompiler.java`, `BvToIntUtils.java`, `CCParameter.java`. + +## Implementable slice (ready to start) + +1. `LASharedTerm` becomes offset-free under `createOffsetEqualities()` (revert the + full-value part of `0c94d305`; the checkpoint path is already offset-aware). +2. On-demand clash-slot enumeration in `finalCheck`: congruence positions via + `getAllFuncApps` (numeric arg sorts) ∪ reverse-trigger positions + (`getArgPosition() != -1`, numeric arg sorts). +3. MBTC over slots with the corrected value formula (step 2 above). +4. Offset-aware `hasSharing` / `choose`. + +Datatype numeric-field feed, array contributions, proof object (and thus offsets +under proofs/interpolation) remain deferred. + +--- + +# Quantifier support (offset-aware e-matching) — planned + +Offsets are currently disabled whenever a `QuantifierTheory` exists +(`Clausifier.createOffsetEqualities()` requires `mQuantTheory == null`), because +e-matching binds variables to offset-free CCTerms and loses the constant (the +recorded unsoundness in `quanttest001`: match `a(x)` against `a(l+1)` but +instantiate `x := l`). This section maps every CC touch point in `theory/quant` +and plans the migration. The EPR theory is ignored (slated for deletion). + +## Inventory: where the quantifier theory touches CC values + +**A. The e-matching register is `CCTerm[]`** — the core representation gap. +`ICode.execute(CCTerm[], int)`, `EMatching.mTodoStack`/`mClauseCodes`/`addCode`, +`PatternCompiler.compile()` (builds the initial register; note +`TermInfo.mGroundTerm` is `(CCTerm) clausifier.createCCTerm(...)` — that cast +throws for a ground pattern subterm like `(+ a 2)` once offsets are on), +`EMReverseTrigger.mRegister`, `EMCompareTrigger.mRegister`. A register slot +holds a *value* (candidate for a pattern subterm or a variable binding), and +values are `CCParameter`s now. Whole-pattern candidates are always `CCAppTerm`s +(offset 0); only argument values / variable bindings carry offsets. + +**B. `GetArgCode` strips the offset** (`getArgParam(pos).getCCTerm()`, with an +explicit "offsets are off under quantifiers" comment). This is *the* unsound +line: binding a variable from `f(a+2)`'s argument must yield the value `a+2`. + +**C. Compare triggers — SETTLED, almost trivial (Jochen).** The trigger is keyed +on the base-term pair plus the *structural* offset δ = o2 − o1 (from +value(t1)+o1 == value(t2)+o2, i.e. value(base1) = value(base2) + δ; stable, so +`EMCompareTrigger` can store the two `CCParameter`s and recompute δ at remove +time). Verified against the code, nearly everything is already in place: +- **Merge-time activation is already offset-selective.** + `CCTerm.mergeInternal` fires `info.mCompareTriggers` only in the + `offFromSrc.equals(delta)` branch; the conflicting-offset branch leaves them + untouched — they ride along dormant in the removed Info and come back on + unmerge. Since there is no "definitely distinct" event in the machinery, + nothing needs to happen on a conflicting-offset merge, and nothing does. +- **`insertCompareTrigger`/`removeCompareTrigger` copy the + `insertEqualityEntry` walk** (CClosure.java:730): negate δ on the merge-time + swap, re-base when stepping `t1 = t1.mRep` (`offset − t1.offsetToRep + + t1.mRep.offsetToRep`), and match `pentry.getOffsetToOther()` when scanning + `mPairInfos`. Just a few CCTerm→CCParameter/offset changes. +- **Same rep, wrong offset at install time:** the trigger can never fire in the + current subtree, so per Jochen it need not be installed (drop the + continuation). Note the `insertEqualityEntry` walk would park it for free — + it has no different-reps assert; the walk terminates via `isLast = t1.mRep == + t2` and leaves the entry in the merge-boundary Info, which goes live again on + unmerge — which matters only when the conflicting merge is *younger* than the + register data (data at level 0, merge at level 3: backtrack to 1 keeps the + trigger, a later matching merge fires it; dropping misses that instance). + Either way it is the same walk; pick during implementation. +`CompareCode` itself becomes: `sameValueAs` → proceed; otherwise install (or +drop in the same-rep case, per the previous point). + +**D. Reverse triggers.** `ReverseCode` reads the arg from the register (now a +`CCParameter`) and `CClosure.insertReverseTrigger(sym, CCTerm arg, pos, trig)` +takes a bare CCTerm. The underlying machinery is already value-keyed +(`ReverseTrigger.getArgument()` returns `CCParameter`; +`ReverseTriggerTrigger`/`MasterReverseTrigger`/`SignatureTrigger` key on +`getOffsetToRep`), so this is mostly signature plumbing: make +`EMReverseTrigger.mArg` a `CCParameter` and let `insertReverseTrigger` accept +it. `EMReverseTrigger.activate` compares `mArg` with +`appTerm.getArgParam(pos)`: the *decide level* path +(`getDecideLevelForPath`) stays on the offset-free `getCCTerm()`s (the merge +path is the same for all offsets); the value match is guaranteed by the +signature. + +**E. Yield / SubstitutionInfo.** `SubstitutionInfo.mVarSubs` is +`List`, `mEquivalentCCTerms` is `Map`; both become +`CCParameter`. Dawg keys come from `varSubs.get(i).getFlatTerm()` — +`CCParameter.getFlatTerm()` already renders the flattened `(+ a 2)`, so the +Term-keyed dawg layer is unaffected once the values are right. + +**F. Canonicalization drops offsets.** +`QuantifierTheory.getRepresentativeTerm(term)` does +`getCCTerm(term).getRepresentative().getFlatTerm()`. Two problems: (i) +`Clausifier.getCCTerm` now *asserts* its argument is offset-free, and +interesting terms / substitutions are value terms like `(+ a 2)`; (ii) the rep +flat term drops the offset — the values `a` and `a+1` in one class would +canonicalize to the same key, conflating *different* substitutions in +`addAllInteresting` and `getRepresentativeSubsDawg` (the dedup dual of the +`quanttest001` unsoundness). Fix: split via +`getOffsetFreeTerm`/`getTermConstant`, then return +`CCParameter.addConstant(rep.getFlatTerm(), k + offsetToRep)` — the canonical +*value* term. + +**G. Ground-side CC lookups on possibly-offsetted terms** (all hit the +`getCCTerm` assert or silently miss): `InstantiationManager.getTermAge`, +`evaluateCCEquality`/`evaluateCCEqualityKnownShared` (ground lhs/rhs of a +`QuantEquality` can carry a constant, e.g. `x = t+3`), +`evaluateLitForPartialEMatchingSubsInfo` (`getCCTerm(clauseVarSubs.get(i))`), +`QuantifierTheory.addGroundCCTerms` (recurses into ground subterms like +`(+ a 1)`). Each needs the offset-free-part + constant split. The equality +evaluation itself needs value semantics: `isEqSet` → `sameValueAs` on +`CCParameter`s; `isDiseqSet` is already offset-aware internally but needs a +`CCParameter` entry point. Same-rep-different-offset evaluates to **FALSE** +(new case — today same rep means TRUE). + +**H. `CClosure.getCCTermRep(Term)`** (used by +`InstantiationManager.TermFinder` to find an existing congruent term for an +instance term) recurses with offset-free `CCTerm[]` argReps and an offset-free +`SignatureTrigger`. It must become `CCParameter`-based: parse each argument's +constant, build the signature on value keys, and return a `CCParameter` +(rep + offset) so `FindSharedAppTerm`/`FindSharedAffine` record the true value +term, not the rep's offset-free flat term. + +**I. `QuantClause`.** `updateInterestingTermsForFuncArgs` drops offsets: +`appTerm.getArgParam(i).getCCTerm().getFlatTerm()` → `getArgParam(i) +.getFlatTerm()`; same for the select-index branch (existing `TODO: add offset` +at the `ArrayTheory.getIndexFromSelect` site). Dedup via +`getRepresentativeTerm` is fixed by F. The `collectVarInfos:338` crash +(`datatype/quantified/*match*`, 4 tests) turned out to be **pre-existing with +offsets off and unrelated to this work**: the quantified equality has a +`MatchTerm` side (`(match l1 ...)` with free variables) and the assert +`rhs instanceof ApplicationTerm` rejects it — a missing quantified-`match` +feature in the quantifier theory, out of scope here (earlier notes +miscategorized it as an offset blocker because it was observed during +gate-flip validation). + +**J. Unaffected (checked):** `SubstitutionHelper`, `DestructiveEqualityReasoning`, +`QuantAuxEquality` (Term-level only); `Dawg` (Term-keyed); +`evaluateLAEquality*`/`evaluateBoundConstraint*` (Polynomial-level); +`checkCompleteness` (lambdas are fresh offset-free constants); +`getDecideLevelForPath` (paths are offset-free). + +**K. Follow-ups enabled/required by the flip:** +- The **reverse-trigger clash-slot source** in + `CClosure.getNumericClashSlots()` (documented deferred: e-matching reverse + triggers with `getArgPosition() != -1` and numeric arg sort) becomes *live* + once quantifiers run with offsets — MBTC must see e-matching positions. +- Proofs: instantiation lemmas substitute real SMT terms, and `(+ a 2)` is one; + the `:inst` rule should be term-level clean, but validate with + `proof-check-mode` once both gates (quantifier and proof) are open. + +## What offsets *buy* e-matching (the extension, separate step) + +Today the fragment forbids arithmetic below uninterpreted functions +(`QuantUtil.containsArithmeticOnQuantOnlyAtTopLevel`, +`isEssentiallyUninterpreted` rejects `f(x+3)`). With offset CC, a pattern +argument `x + k` (variable-plus-constant, and more generally `t + k`) is +matchable for free: `GetArgCode` yields the value `v`, the binding is +`x := v − k` (constant shift on a `CCParameter`); `CompareCode` compares +shifted values. That extends the almost-uninterpreted fragment with constant +offsets under function symbols (`f(x+3) = g(y)` etc.) — the actual quantifier +payoff of the offset project. Requires: `PatternCompiler` support for affine +arguments (constant part only), a shift field on GetArg/Compare codes, fragment +checks widened (`isEssentiallyUninterpreted`, +`containsArithmeticOnQuantOnlyAtTopLevel`, `containsAppTermsForEachVar`), and +`QuantClause.addVarArgInfo`/`VarInfo` positions that record the shift so +interesting-term collection proposes `groundArg − k`. Non-constant coefficients +and multi-variable arguments stay out. + +## Increments + +- **Q1 — `CCParameter` register. (DONE, uncommitted.)** Mechanical + thread-through of A/B/E: register `CCTerm[]` → `CCParameter[]` everywhere in + `ematching/`, `GetArgCode` keeps the offset, `SubstitutionInfo` holds + `CCParameter`s, `PatternCompiler` ground slots stop casting. Type change + propagates into `InstantiationManager` (equivalent-CCTerm maps, var subs; + asserts use `sameValueAs`). `CompareCode`: `sameValueAs` → proceed; same base + term at different structural offsets → drop (can never become equal); + otherwise install. Offset-free behavior verified byte-identical + (`test/quantified` + `test/datatype/quantified` outputs diff-identical to + baseline under `-ea`; full `ant runtests` 723/723). +- **Q2 — CClosure API. (DONE except `getCCParamRep`, uncommitted.)** + `insertCompareTrigger(CCParameter, CCParameter, trigger)` / + `removeCompareTrigger` mirror the `insertEqualityEntry` offset walk + (including the same-class park-at-boundary case — chosen over dropping since + it is the same walk anyway); merge-time activation was already + offset-selective; `insertReverseTrigger(CCParameter)`; `isEqSet`/`isDiseqSet` + on `CCParameter` (`isEqSet` = `sameValueAs`; all callers were quant-side and + want value semantics). `isDiseqSet` also returns true for same-rep pairs at + different offsets (provably distinct by a non-zero constant; Jochen) — the + same-rep case is now fully decided between `isEqSet`/`isDiseqSet`, and the + degenerate `getInfo(rep, rep, ·)` lookup is avoided. Still open: + `getCCTermRep` → `getCCParamRep` (needed in Q3 for `TermFinder`). +- **Q3 — evaluation & canonicalization. (DONE, uncommitted.)** New + `Clausifier.getCCParameter(Term)` (offset-free split + `CCParameter.of`); + `getRepresentativeTerm` returns the canonical *value* term + (`getValueKey().getFlatTerm()`); `getTermAge`/`evaluateCCEquality + (KnownShared)`/partial-EM var-subs lookup use `getCCParameter`; + `addGroundCCTerms` normalizes via `getOffsetFreeTerm`; `getCCTermRep` → + `getCCParamRep` (constant split, value-keyed signature lookup, returns + rep+offset; `TermFinder` and the QuantClause array lookup adapted); + `QuantClause` interesting terms keep offsets (`getArgParam(i).getFlatTerm()`, + select-index TODO resolved). Same-rep-wrong-offset evaluates FALSE via the + `isDiseqSet` same-class case. `collectVarInfos:338` needs no fix here (see I). +- **Q4 — flip the quantifier gate. (DONE, uncommitted.)** Dropped + `mQuantTheory == null` from `Clausifier.createOffsetEqualities()` — this also + dissolves the raw-vs-effective flag divergence (the `bitVecLearnConflict2` + class of bugs), since both predicates now coincide. Reverse-trigger + clash-slot source added to `getNumericClashSlots()` (watched values of + installed reverse triggers join their (symbol, position) slot; + `ReverseTriggerTrigger.getTriggers()` accessor). NOTE the proof gate was + already open at HEAD (`CClosure.createOffsetEqualities` no longer checks + `isProofGenerationEnabled`), so this flip enables offsets under + quantifiers+proofs+interpolation together. Validation: new + `test/quantified/offsetmatch001..003` (offset binding `x := a+3`; a/a+1 + dedup-distinctness; spurious-unsat guard — all pass with `-ea`); + `quanttest001` unsat; all `test/quantified` verdicts unchanged with + offset-shaped proofs/checked interpolants (`ArrayInitialization02` has + `interpolant-check-mode true`); full `ant runtests` **726/726** incl. + SystemTest (lowlevel proof-check + model-check + interpolant-check on the + whole corpus) and BitvectorTest. +- **Q5 — offset patterns** (the fragment extension above). Independent, + largest, do after Q4 is stable. + +Q1+Q2 are the substantial work; Q3 is broad but mechanical; the compare-trigger +offset semantics (C) is the one genuinely new CC mechanism. diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/CCTermBuilder.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/CCTermBuilder.java index 32037071b..ea8bca656 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/CCTermBuilder.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/CCTermBuilder.java @@ -22,8 +22,10 @@ import de.uni_freiburg.informatik.ultimate.logic.ApplicationTerm; import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.logic.Term; import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.SourceAnnotation; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CClosure; @@ -35,19 +37,29 @@ public class CCTermBuilder { private final SourceAnnotation mSource; private final ArrayDeque mOps = new ArrayDeque<>(); - private final ArrayDeque mConverted = new ArrayDeque<>(); + /** + * The converted results as {@link CCParameter}s: the term that produced {@code mConverted.peek()} has value + * {@code peek().getCCTerm() + peek().getOffset()}. The offset carries the {@code +5} of an offset-free term like + * {@code x+5} up to the enclosing application, where it ends up in that argument's {@link CCParameter}. Offsets are + * {@link Rational#ZERO} (i.e. a bare {@link CCTerm}) unless offset equalities are enabled. + */ + private final ArrayDeque mConverted = new ArrayDeque<>(); public CCTermBuilder(Clausifier clausifier, final SourceAnnotation source) { mClausifier = clausifier; mSource = source; } - public CCTerm convert(final Term t) { + private void pushResult(final CCParameter ccParam) { + mConverted.push(ccParam); + } + + public CCParameter convert(final Term t) { mOps.push(new BuildCCTerm(t)); while (!mOps.isEmpty()) { mOps.pop().perform(); } - final CCTerm res = mConverted.pop(); + final CCParameter res = mConverted.pop(); assert mConverted.isEmpty(); return res; } @@ -61,12 +73,19 @@ public BuildCCTerm(final Term term) { @Override public void perform() { - CCTerm ccTerm = mClausifier.getCCTerm(mTerm); + // mCCTerms is keyed by offset-free terms; probe (and below build) the offset-free part, then re-apply the + // constant as the CCParameter's offset (zero when the term is already offset-free). + final Term offsetFree = mClausifier.getOffsetFreeTerm(mTerm); + final CCTerm ccTerm = mClausifier.getCCTerm(offsetFree); if (ccTerm != null) { - mConverted.push(ccTerm); + pushResult(CCParameter.of(ccTerm, mClausifier.getTermConstant(mTerm))); } else { final CClosure cclosure = mClausifier.getCClosure(); - if (Clausifier.needCCTerm(mTerm) && ((ApplicationTerm) mTerm).getParameters().length > 0) { + if (offsetFree != mTerm) { + // Numeric term with a non-zero constant: build the offset-free CCTerm and remember the constant. + mOps.push(new AddOffsetToTerm(mClausifier.getTermConstant(mTerm))); + mOps.push(new BuildCCTerm(offsetFree)); + } else if (Clausifier.needCCTerm(mTerm) && ((ApplicationTerm) mTerm).getParameters().length > 0) { final FunctionSymbol fs = ((ApplicationTerm) mTerm).getFunction(); if (fs.isIntern() && fs.getName() == "select") { mClausifier.getArrayTheory().cleanCaches(); @@ -79,16 +98,34 @@ public void perform() { } } else { // We have an intern function symbol - ccTerm = cclosure.createAnonTerm(mTerm); - cclosure.addTerm(ccTerm, mTerm); - mClausifier.shareCCTerm(mTerm, ccTerm); + final CCTerm anonTerm = cclosure.createAnonTerm(mTerm); + cclosure.addTerm(anonTerm, mTerm); + mClausifier.shareCCTerm(mTerm, anonTerm); mClausifier.addTermAxioms(mTerm, mSource); - mConverted.push(ccTerm); + pushResult(anonTerm); } } } } + /** + * Adds an offset to the (already built) numeric CCTerm of its offset-free part, + * and pushes the CCParameter with the offset. + */ + private class AddOffsetToTerm implements Operation { + private final Rational mOffset; + + public AddOffsetToTerm(final Rational offset) { + mOffset = offset; + } + + @Override + public void perform() { + final CCTerm offsetFreeParam = (CCTerm) mConverted.pop(); + pushResult(CCParameter.of(offsetFreeParam, mOffset)); + } + } + /** * Helper class to build the intermediate CCAppTerms. Note that all these terms will be func terms. * @@ -103,16 +140,18 @@ public BuildCCAppTerm(ApplicationTerm appTerm) { @Override public void perform() { - final CCTerm[] args = new CCTerm[mAppTerm.getParameters().length]; + final CCParameter[] args = new CCParameter[mAppTerm.getParameters().length]; for (int i = args.length - 1; i >= 0; i--) { args[i] = mConverted.pop(); } assert mClausifier.getCCTerm(mAppTerm) == null; - final CCTerm ccTerm = mClausifier.getCClosure().createAppTerm(mAppTerm.getFunction(), args, mSource); + final CCTerm ccTerm = + mClausifier.getCClosure().createAppTerm(mAppTerm.getFunction(), args, mSource); mClausifier.getCClosure().addTerm(ccTerm, mAppTerm); mClausifier.shareCCTerm(mAppTerm, ccTerm); mClausifier.addTermAxioms(mAppTerm, mSource); - mConverted.push(ccTerm); + // the application term itself is not numeric-offset; its value is the application + pushResult(ccTerm); } } } \ No newline at end of file diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/Clausifier.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/Clausifier.java index b16c38307..cc0bda787 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/Clausifier.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/Clausifier.java @@ -71,6 +71,7 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.bitvector.BvToIntUtils; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.ArrayTheory; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCAppTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CClosure; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.DTReverseTrigger; @@ -221,10 +222,10 @@ public Clausifier(final Theory theory, final DPLLEngine engine, final ProofMode * The source annotation that is used for auxiliary axioms. * @return the ccterm. */ - public CCTerm createCCTerm(final Term term, final SourceAnnotation source) { + public CCParameter createCCTerm(final Term term, final SourceAnnotation source) { final boolean wasRunning = mIsRunning; mIsRunning = true; - final CCTerm ccterm = new CCTermBuilder(this, source).convert(term); + final CCParameter ccterm = new CCTermBuilder(this, source).convert(term); mIsRunning = wasRunning; if (!wasRunning) { run(); @@ -261,8 +262,13 @@ public void setTermFlags(final Term term, final int newFlags) { } public void share(final CCTerm ccTerm, final LASharedTerm laTerm) { + // With offset equalities several terms (e.g. 2x+4y, 2x+4y+1, 2x+4y+5) map to the same offset-free CCTerm. Each + // of them is a distinct value, so each full-value LASharedTerm must be registered with linear arithmetic (so + // mbtc sees every value); but the offset-free CCTerm is shared with congruence closure only once. getLASolver().addSharedTerm(laTerm); - getCClosure().addSharedTerm(ccTerm); + if (ccTerm.getSharedTerm() != ccTerm) { + getCClosure().addSharedTerm(ccTerm); + } } public void shareLATerm(final Term term, final LASharedTerm laTerm) { @@ -284,6 +290,10 @@ public void shareCCTerm(final Term term, final CCTerm ccTerm) { } public void addTermAxioms(final Term term, final SourceAnnotation source) { + // Axioms are added for offset-free terms only; an offseted term (x+5) carries its constant structurally and its + // offset-free part (x) gets the axioms. The only callers with user terms (EqualityProxy, createEqualityProxy) + // normalize via getOffsetFreeTerm before calling. + assert getTermConstant(term).equals(Rational.ZERO) : "addTermAxioms on offseted term " + term; final int termFlags = getTermFlags(term); if ((termFlags & Clausifier.AUX_AXIOM_ADDED) == 0) { final boolean wasRunning = mIsRunning; @@ -293,7 +303,7 @@ public void addTermAxioms(final Term term, final SourceAnnotation source) { CCTerm ccTerm = getCCTerm(term); if (ccTerm == null && (needCCTerm(term) || term.getSort().isArraySort())) { final CCTermBuilder cc = new CCTermBuilder(this, source); - ccTerm = cc.convert(term); + ccTerm = (CCTerm) cc.convert(term); } final ApplicationTerm at = (ApplicationTerm) term; @@ -329,7 +339,7 @@ public void addTermAxioms(final Term term, final SourceAnnotation source) { final String funcName = at.getFunction().getName(); final boolean isStore = funcName.equals("store"); final boolean isConst = funcName.equals(SMTLIBConstants.CONST); - mArrayTheory.notifyArray(getCCTerm(term), isStore, isConst); + mArrayTheory.notifyArray(ccTerm, isStore, isConst); } if (fs.isConstructor()) { @@ -353,6 +363,15 @@ public void addTermAxioms(final Term term, final SourceAnnotation source) { } if (term.getSort().isNumericSort()) { + // With offset equalities the CCTerm is offset-free (value 2x+4y for a term 2x+4y+1) and the + // constant is carried structurally at the use sites. The LASharedTerm must then be offset-free + // too, so it stays value-consistent with the offset-free CCTerm: clash-slot MBTC values a member + // as value(rep) + offsetToRep and must not double-count the constant. Terms differing only by a + // constant share one offset-free CCTerm; each still registers its own offset-free LASharedTerm + // (same value), which is harmless and recognized as already-known by the offset-aware guards. + // + // Without offset equalities the LASharedTerm carries the full value (its constant), as before, + // so whole-term mbtc groups shared terms by their true value. boolean needsLA = term instanceof ConstantTerm; if (term instanceof ApplicationTerm) { final String func = ((ApplicationTerm) term).getFunction().getName(); @@ -364,6 +383,9 @@ public void addTermAxioms(final Term term, final SourceAnnotation source) { final MutableAffineTerm mat = createMutableAffinTerm(new Polynomial(term), source); assert mat.getConstant().mEps == 0; if (!mLATerms.containsKey(term)) { + // The LASharedTerm shares the term's value with linear arithmetic. The term is offset-free (see + // the precondition), so this stays consistent with getCCTerm and the endScope unshare loop, which + // look up CC nodes by the (offset-free) key. shareLATerm(term, new LASharedTerm(term, mat.getSummands(), mat.getConstant().mReal)); } } @@ -570,13 +592,77 @@ public MutableAffineTerm toMutableAffineTerm(final Polynomial poly) { } public CCTerm getCCTerm(final Term term) { + // mCCTerms is keyed by offset-free terms only; an offseted term has no entry of its own (its CCParameter, with + // the constant as offset, is produced at build time by createCCTerm). Callers must normalize via + // getOffsetFreeTerm first, or use getCCParameter for a possibly offseted term. + assert getTermConstant(term).equals(Rational.ZERO) : "getCCTerm on offseted term " + term; return mCCTerms.get(term); } + /** + * Get the {@link CCParameter} denoting the value of the given term: the CCTerm of the term's offset-free part plus + * the term's constant. Unlike {@link #getCCTerm} this accepts terms with a constant summand. This function does not + * create new terms. + * + * @return the value of the term, or null if no CCTerm exists for the term's offset-free part. + */ + public CCParameter getCCParameter(final Term term) { + final Rational constant = getTermConstant(term); + final CCTerm ccTerm = getCCTerm(constant.equals(Rational.ZERO) ? term : getOffsetFreeTerm(term)); + return ccTerm == null ? null : CCParameter.of(ccTerm, constant); + } + public LASharedTerm getLATerm(final Term term) { return mLATerms.get(term); } + /** + * Whether offset equalities are enabled in the congruence closure. When enabled, numeric terms are represented by + * offset-free CCTerms with the constant carried as an offset. + */ + public boolean createOffsetEqualities() { + return getCClosure() != null && getCClosure().createOffsetEqualities(); + } + + /** + * The constant offset of a numeric term, i.e. the value such that {@code term == offsetFreeTerm + constant}. Returns + * {@link Rational#ZERO} when offset equalities are disabled or the term is not numeric. + */ + public Rational getTermConstant(final Term term) { + if (!createOffsetEqualities() || !term.getSort().isNumericSort()) { + return Rational.ZERO; + } + return new Polynomial(term).getConstant(); + } + + /** + * Build the normalized term for {@code term + constant}. The result is a single flattened polynomial term (not a + * nested {@code (+ term constant)}), so that re-parsing it as a Polynomial recovers all summands. + */ + public Term addConstantToTerm(final Term term, final Rational constant) { + return CCParameter.addConstant(term, constant); + } + + /** + * The offset-free part of a numeric term, i.e. the term with its constant + * summand removed. Two terms that differ only by a constant (e.g. + * {@code 2x+4y+1} and {@code 2x+4y+5}) yield the same canonical offset-free + * term, so they share a single CCTerm. Returns the term itself when it has no + * constant or offset equalities are disabled. + */ + public Term getOffsetFreeTerm(final Term term) { + if (!createOffsetEqualities() || !term.getSort().isNumericSort()) { + return term; + } + final Polynomial poly = new Polynomial(term); + final Rational constant = poly.getConstant(); + if (constant.equals(Rational.ZERO)) { + return term; + } + poly.add(constant.negate()); + return mCompiler.unifyPolynomial(poly, term.getSort()); + } + public ILiteral getILiteral(final Term term) { return mLiterals.get(term); } @@ -1296,8 +1382,13 @@ public EqualityProxy createEqualityProxy(final Term lhs, final Term rhs, final S if (sort.getName().equals("Int") && !diff.getConstant().isIntegral()) { return EqualityProxy.getFalseProxy(); } - addTermAxioms(lhs, source); - addTermAxioms(rhs, source); + // The proxy works on the offset-free sides plus the offset (value(offLhs) == value(offRhs) + offset). The + // offset is the difference of the two sides' constants. + final Term offLhs = getOffsetFreeTerm(lhs); + final Term offRhs = getOffsetFreeTerm(rhs); + final Rational offset = getTermConstant(rhs).sub(getTermConstant(lhs)); + addTermAxioms(offLhs, source); + addTermAxioms(offRhs, source); // we cannot really normalize the sign of the term. Try both signs. EqualityProxy eqForm = mEqualities.get(diff); if (eqForm != null) { @@ -1308,7 +1399,7 @@ public EqualityProxy createEqualityProxy(final Term lhs, final Term rhs, final S if (eqForm != null) { return eqForm; } - eqForm = new EqualityProxy(this, lhs, rhs); + eqForm = new EqualityProxy(this, offLhs, offRhs, offset); mEqualities.put(diff, eqForm); return eqForm; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/EqualityProxy.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/EqualityProxy.java index 921c2d4b8..ad248b5da 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/EqualityProxy.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/EqualityProxy.java @@ -36,7 +36,7 @@ public class EqualityProxy { */ public static final class TrueEqualityProxy extends EqualityProxy { private TrueEqualityProxy() { - super(null, null, null); + super(null, null, null, null); } @Override public DPLLAtom getLiteral(final SourceAnnotation source) { @@ -49,7 +49,7 @@ public DPLLAtom getLiteral(final SourceAnnotation source) { */ public static final class FalseEqualityProxy extends EqualityProxy { private FalseEqualityProxy() { - super(null, null, null); + super(null, null, null, null); } @Override public DPLLAtom getLiteral(final SourceAnnotation source) { @@ -69,21 +69,31 @@ public static FalseEqualityProxy getFalseProxy() { } final Clausifier mClausifier; + /** The offset-free sides and the constant offset: this proxy states {@code value(mLhs) == value(mRhs) + mOffset}. */ final Term mLhs, mRhs; + final Rational mOffset; - public EqualityProxy(final Clausifier clausifier, final Term lhs, final Term rhs) { + public EqualityProxy(final Clausifier clausifier, final Term lhs, final Term rhs, final Rational offset) { mClausifier = clausifier; mLhs = lhs; mRhs = rhs; + mOffset = offset; } public LAEquality createLAEquality() { - /* create la part */ + // The proxy's canonical equality as a linear constraint: mLhs - mRhs - mOffset == 0. final Polynomial affine = new Polynomial(mLhs); affine.add(Rational.MONE, mRhs); + affine.add(mOffset.negate()); return mClausifier.getLASolver().createEquality(mClausifier.createMutableAffinTerm(affine, null)); } + /** + * The norm factor for a CCEquality between {@code lhs} and {@code rhs}: it relates the CCEquality to the proxy's + * shared LAEquality via {@code factor * (lhs - rhs) == laeq.getVar()}. It is per CCEquality, not per proxy, since + * equivalent equalities can be scaled (e.g. {@code 2x = 2y+4} vs {@code x = y+2} share a proxy but need factors 1/2 + * and 1). The constant offset does not affect the gcd, so it is irrelevant here. + */ public Rational computeNormFactor(final Term lhs, final Term rhs) { final Polynomial affine = new Polynomial(lhs); affine.add(Rational.MONE, rhs); @@ -104,40 +114,55 @@ public Rational computeNormFactor(final Term lhs, final Term rhs) { */ public CCEquality createCCEquality(final Term lhs, final Term rhs) { assert lhs.getSort().isNumericSort(); - final CCTerm ccLhs = mClausifier.getCCTerm(lhs); - final CCTerm ccRhs = mClausifier.getCCTerm(rhs); + // Resolve the offset-free CC nodes and the offset (the difference of the sides' constants); both are per call, + // not the proxy's, since this lhs/rhs may be a scaled equivalent of the proxy's canonical equality. + return createCCEquality(mClausifier.getCCTerm(mClausifier.getOffsetFreeTerm(lhs)), + mClausifier.getCCTerm(mClausifier.getOffsetFreeTerm(rhs)), + mClausifier.getTermConstant(rhs).sub(mClausifier.getTermConstant(lhs))); + } + + /** + * Create a CCEquality {@code value(ccLhs) == value(ccRhs) + offset} linked to this proxy's LAEquality. The offset + * and norm factor are derived from {@code ccLhs}/{@code ccRhs}/{@code offset} (not from the proxy's canonical + * {@link #mOffset}), because equivalent equalities can be scaled and share this proxy at different offsets. + */ + public CCEquality createCCEquality(final CCTerm ccLhs, final CCTerm ccRhs, final Rational offset) { assert ccLhs != null && ccRhs != null; - final DPLLAtom eqAtom = getLiteral(null); + final DPLLAtom eqAtom = getLiteral(SourceAnnotation.EMPTY_SOURCE_ANNOT); LAEquality laeq; if (eqAtom instanceof CCEquality) { final CCEquality eq = (CCEquality) eqAtom; laeq = eq.getLASharedData(); if (laeq == null) { - final Rational normFactor = computeNormFactor(mLhs, mRhs); laeq = createLAEquality(); laeq.addDependentAtom(eq); - eq.setLASharedData(laeq, normFactor); + eq.setLASharedData(laeq, computeNormFactor(eq.getLhs().getFlatTerm(), eq.getRhs().getFlatTerm())); } } else { laeq = (LAEquality) eqAtom; } for (final CCEquality eq : laeq.getDependentEqualities()) { assert (eq.getLASharedData() == laeq); - if (eq.getLhs() == ccLhs && eq.getRhs() == ccRhs) { + // The offset must match too: two CCEqualities between the same pair of CCTerms but with different offsets + // (e.g. x == y and x == y + 3) are distinct facts and must not be conflated, otherwise propagating one + // would wrongly set the other. eq states getLhs() == getRhs() + eq.getOffset(). + if (eq.getLhs() == ccLhs && eq.getRhs() == ccRhs && eq.getOffset().equals(offset)) { return eq; } - if (eq.getRhs() == ccLhs && eq.getLhs() == ccRhs) { + if (eq.getLhs() == ccRhs && eq.getRhs() == ccLhs && eq.getOffset().equals(offset.negate())) { return eq; } } - final CCEquality eq = mClausifier.getCClosure().createCCEquality(mClausifier.getStackLevel(), ccLhs, ccRhs); - final Rational normFactor = computeNormFactor(lhs, rhs); + final CCEquality eq = + mClausifier.getCClosure().createCCEquality(mClausifier.getStackLevel(), ccLhs, ccRhs, offset); laeq.addDependentAtom(eq); - eq.setLASharedData(laeq, normFactor); + eq.setLASharedData(laeq, computeNormFactor(ccLhs.getFlatTerm(), ccRhs.getFlatTerm())); return eq; } private DPLLAtom createAtom(final Term eqTerm, final SourceAnnotation source) { + // mLhs/mRhs are offset-free, so axioms, existence probes and LinVar creation operate directly on them; the + // offset (mOffset) is reintroduced when the CCEquality is built below. mClausifier.addTermAxioms(mLhs, source); mClausifier.addTermAxioms(mRhs, source); @@ -166,8 +191,8 @@ private DPLLAtom createAtom(final Term eqTerm, final SourceAnnotation source) { return createLAEquality(); } else { /* let them share congruence closure */ - final CCTerm ccLhs = mClausifier.createCCTerm(mLhs, source); - final CCTerm ccRhs = mClausifier.createCCTerm(mRhs, source); + final CCTerm ccLhs = mClausifier.createCCTerm(mLhs, source).getCCTerm(); + final CCTerm ccRhs = mClausifier.createCCTerm(mRhs, source).getCCTerm(); /* Creating the CC terms could have created the equality */ final DPLLAtom atom = (DPLLAtom) mClausifier.getILiteral(eqTerm); @@ -176,12 +201,29 @@ private DPLLAtom createAtom(final Term eqTerm, final SourceAnnotation source) { } /* create CC equality */ - return mClausifier.getCClosure().createCCEquality(mClausifier.getStackLevel(), ccLhs, ccRhs); + final CCEquality cceq = + mClausifier.getCClosure().createCCEquality(mClausifier.getStackLevel(), ccLhs, ccRhs, mOffset); + if (mClausifier.createOffsetEqualities() && mLhs.getSort().isNumericSort()) { + // With offset equalities, clash-slot MBTC only creates LAEqualities for shared terms that occupy a + // function-argument position; a numeric (dis)equality between other shared terms (e.g. two selector or + // div results) would otherwise have no LAEquality, so linear arithmetic would never learn the + // disequality and model construction could pick a model that violates it. Create the LAEquality eagerly + // (it forces the necessary LinVars, here both sides are numeric) and link it to the CCEquality, exactly + // as createCCEquality does once a term is already shared. + final LAEquality laeq = createLAEquality(); + laeq.addDependentAtom(cceq); + cceq.setLASharedData(laeq, computeNormFactor(mLhs, mRhs)); + } + return cceq; } } public DPLLAtom getLiteral(final SourceAnnotation source) { - final Term eqTerm = mLhs.getTheory().term("=", mLhs, mRhs); + // Reconstruct the offset-aware equality term (mLhs == mRhs + mOffset) as the literal/flag key, so proxies that + // differ only by their offset get distinct atoms (mirrors CCEquality.getSMTFormula). + final Term rhsWithOffset = + mOffset.equals(Rational.ZERO) ? mRhs : mClausifier.addConstantToTerm(mRhs, mOffset); + final Term eqTerm = mLhs.getTheory().term("=", mLhs, rhsWithOffset); DPLLAtom lit = (DPLLAtom) mClausifier.getILiteral(eqTerm); if (lit == null) { lit = createAtom(eqTerm, source); @@ -201,6 +243,9 @@ public String toString() { pt.append(sb, mLhs); sb.append(" == "); pt.append(sb, mRhs); + if (!mOffset.equals(Rational.ZERO)) { + sb.append(" + ").append(mOffset); + } return sb.toString(); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/TermCompiler.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/TermCompiler.java index 0d207586e..9d0ea39da 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/TermCompiler.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/convert/TermCompiler.java @@ -47,6 +47,7 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.ProofConstants; import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.resolute.BitvectorRules; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.bitvector.BvToIntUtils; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.IPolynomialUnifier; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.Polynomial; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.TermUtils; @@ -783,24 +784,39 @@ public void postConvertAnnotation(final AnnotatedTerm old, final Annotation[] ne } /** - * Canonicalize a polynomial, i.e. check if we already created this term with the summands in a different order. + * Canonicalize a polynomial, i.e. check if we already created this term with + * the summands in a different order. * - * @param poly - * the polynomial to canonicalize and convert to a term. - * @param sort - * the Sort of the resulting term. + *

+ * A polynomial with a non-zero constant (and at least one other summand) is + * canonicalized as the canonic term of its constant-free part with the constant + * appended as last summand ({@link CCParameter#addConstant}). Thus a + * canonic term and its offset-free term (the flat term of the base CCTerm when + * offset equalities are enabled) agree summand-for-summand, and dropping the + * trailing constant from a canonic term syntactically recovers its canonic + * offset-free term. + * + * @param poly the polynomial to canonicalize and convert to a term. + * @param sort the Sort of the resulting term. * @return the canonic summation term. */ @Override public Term unifyPolynomial(final Polynomial poly, final Sort sort) { - final int hash = poly.hashCode() ^ sort.hashCode(); + Polynomial basePoly = poly; + final Rational offset = poly.getConstant(); + if (offset != Rational.ZERO) { + basePoly = new Polynomial(); + basePoly.add(Rational.ONE, poly); + basePoly.add(offset.negate()); + } + final int hash = basePoly.hashCode() ^ sort.hashCode(); for (final Term canonic : mCanonicalPolys.iterateHashCode(hash)) { - if (canonic.getSort() == sort && poly.equals(new Polynomial(canonic))) { - return canonic; + if (canonic.getSort() == sort && basePoly.equals(new Polynomial(canonic))) { + return CCParameter.addConstant(canonic, offset); } } - final Term canonic = poly.toTerm(sort); + final Term canonic = basePoly.toTerm(sort); mCanonicalPolys.put(hash, canonic); - return canonic; + return CCParameter.addConstant(canonic, offset); } -} \ No newline at end of file +} diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/ArrayInterpolator.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/ArrayInterpolator.java index c4faa13c5..b4b3c3855 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/ArrayInterpolator.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/ArrayInterpolator.java @@ -30,6 +30,7 @@ import de.uni_freiburg.informatik.ultimate.logic.ApplicationTerm; import de.uni_freiburg.informatik.ultimate.logic.FormulaUnLet; import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.logic.SMTLIBConstants; import de.uni_freiburg.informatik.ultimate.logic.Sort; import de.uni_freiburg.informatik.ultimate.logic.Term; @@ -38,7 +39,9 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.LitInfo; import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.Occurrence; import de.uni_freiburg.informatik.ultimate.smtinterpol.option.SMTInterpolConstants; -import de.uni_freiburg.informatik.ultimate.smtinterpol.util.SymmetricPair; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.OffsetEqKey; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.OffsetTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.Polynomial; /** * The interpolator for the theory of arrays. @@ -69,13 +72,16 @@ public class ArrayInterpolator { */ private LitInfo mDiseqInfo; /** - * The atoms of the equality literals in this lemma. Note that they appear negated in the clause. + * The atoms of the equality literals in this lemma, keyed by the fact they denote (offset-aware, so a lemma's + * shifted instance like {@code (+ i 1) = (+ j 2)} finds the canonicalized literal {@code i = (+ j 1)}). Note that + * they appear negated in the clause. */ - private Map, Term> mEqualities; + private Map mEqualities; /** - * The atoms of the disequality literals in this lemma. Note that they appear positively in the clause. + * The atoms of the disequality literals in this lemma, keyed like {@link #mEqualities}. Note that they appear + * positively in the clause. */ - private Map, Term> mDisequalities; + private Map mDisequalities; /** * The store path between the arrays of the main disequality for weak equivalence in a weakeq-ext lemma, and for * weak equivalence modulo i, where i is the path index, in a read-over-weakeq lemma. @@ -90,9 +96,10 @@ public class ArrayInterpolator { */ private Occurrence mABSwitchOccur; /** - * The strong path between the select indices of the main disequality for read-over-weakeq. + * The strong path between the select indices of the main disequality for read-over-weakeq. The projection index + * is the select index that is not the weakpath index. */ - private Term mIndexEquality; + private ProjectedIndexLit mIndexEquality; /** * This map contains the paths for weak congruence on index i in weakeq-ext. */ @@ -162,6 +169,79 @@ private ProofPath[] getPaths(final InterpolatorClauseInfo clauseInfo) { return paths; } + /** + * A clause literal found for a queried (dis)equality {@code left = right}, together with its occurrence info + * reoriented to the query: the literal may be a canonicalized form of the queried fact, differing by a constant + * shift and swapped sides. + */ + class ArrayLit { + final Term mAtom; + final OffsetLitInfo mInfo; + + ArrayLit(final Term atom, final OffsetEqKey queryKey) { + mAtom = atom; + final ApplicationTerm eq = mInterpolator.getAtomTermInfo(atom).getEquality(); + final OffsetEqKey litKey = new OffsetEqKey(eq.getParameters()[0], eq.getParameters()[1]); + mInfo = new OffsetLitInfo(mTheory, mInterpolator.getAtomOccurenceInfo(atom), litKey).reorient(queryKey); + } + } + + /** + * Find the clause equality denoting the fact {@code left = right} (modulo constant shift), or null. + */ + private ArrayLit lookupEquality(final Term left, final Term right) { + final OffsetEqKey key = new OffsetEqKey(left, right); + final Term atom = mEqualities.get(key); + return atom == null ? null : new ArrayLit(atom, key); + } + + /** + * Find the clause disequality denoting the fact {@code left != right} (modulo constant shift), or null. + */ + private ArrayLit lookupDisequality(final Term left, final Term right) { + final OffsetEqKey key = new OffsetEqKey(left, right); + final Term atom = mDisequalities.get(key); + return atom == null ? null : new ArrayLit(atom, key); + } + + /** + * Check that two numeric terms are trivially distinct, i.e. they have the same offset-free part but different + * constants (like {@code x} and {@code x + 1}, or two distinct numerals). + */ + private static boolean isTriviallyDistinct(final Term left, final Term right) { + final OffsetEqKey key = new OffsetEqKey(left, right); + return key.getLhs() == key.getRhs() && !key.getOffset().equals(Rational.ZERO); + } + + /** + * An index (dis)equality collected on a weak path: the clause atom with its info reoriented to the path-level + * fact (some index vs. the weakpath index), and the path-level term of the non-weakpath side, which is used to + * build the projections in the interpolant (the atom's own parameters may be shifted by a constant). + */ + class ProjectedIndexLit { + final Term mAtom; + final OffsetLitInfo mInfo; + final Term mProjIndex; + + ProjectedIndexLit(final ArrayLit lit, final Term projIndex) { + mAtom = lit.mAtom; + mInfo = lit.mInfo; + mProjIndex = projIndex; + } + } + + /** + * Build the term {@code term + offset} in canonical form (flattened sum, constant last). + */ + private Term addOffset(final Term term, final Rational offset) { + if (offset.equals(Rational.ZERO)) { + return term; + } + final Polynomial poly = new Polynomial(term); + poly.add(offset); + return poly.toTerm(term.getSort()); + } + /** * Compute interpolants for array lemmas of type read-over-weakeq and weakeq-ext. * @@ -179,13 +259,13 @@ public Term[] computeInterpolants(final InterpolatorClauseInfo lemmaInfo) { final InterpolatorAtomInfo atomTermInfo = mInterpolator.getAtomTermInfo(atom); final ApplicationTerm equality = atomTermInfo.getEquality(); // negated in clause means positive in conflict - final Map, Term> map = (atom != literal ? mEqualities : mDisequalities); - map.put(new SymmetricPair<>(equality.getParameters()[0], equality.getParameters()[1]), + final Map map = (atom != literal ? mEqualities : mDisequalities); + map.put(new OffsetEqKey(equality.getParameters()[0], equality.getParameters()[1]), atom); } final ApplicationTerm equality = getDiseq(mLemmaInfo); final Term[] eqParams = equality.getParameters(); - mDiseq = mDisequalities.get(new SymmetricPair<>(eqParams[0], eqParams[1])); + mDiseq = mDisequalities.get(new OffsetEqKey(eqParams[0], eqParams[1])); mDiseqInfo = mInterpolator.getAtomOccurenceInfo(mDiseq); Term[] interpolants = new Term[mNumInterpolants]; @@ -227,17 +307,21 @@ private Term[] computeReadOverWeakeqInterpolants() { mStorePath = paths[0]; assert mDiseq != null; - final ApplicationTerm selectEq = (ApplicationTerm) mDiseq; - assert selectEq.getFunction().getName().equals("=") - && selectEq.getParameters()[0] instanceof ApplicationTerm + // Read the select structure from the annotation's disequality: the clause atom may be an offset-canonicalized + // form of it, but the annotation is guaranteed to be an equality of two path-level select terms. + final ApplicationTerm selectEq = getDiseq(mLemmaInfo); + assert selectEq.getParameters()[0] instanceof ApplicationTerm && selectEq.getParameters()[1] instanceof ApplicationTerm && ((ApplicationTerm) selectEq.getParameters()[0]).getFunction().getName().equals("select") && ((ApplicationTerm) selectEq.getParameters()[1]).getFunction().getName().equals("select"); final Term leftIndex = ((ApplicationTerm) selectEq.getParameters()[0]).getParameters()[1]; final Term rightIndex = ((ApplicationTerm) selectEq.getParameters()[1]).getParameters()[1]; if (leftIndex != rightIndex) { - mIndexEquality = mEqualities.get(new SymmetricPair<>(leftIndex, rightIndex)); - assert mIndexEquality != null; + assert mStorePath.getIndex() == leftIndex || mStorePath.getIndex() == rightIndex; + final Term otherIndex = mStorePath.getIndex() == leftIndex ? rightIndex : leftIndex; + final ArrayLit indexEquality = lookupEquality(otherIndex, mStorePath.getIndex()); + assert indexEquality != null; + mIndexEquality = new ProjectedIndexLit(indexEquality, otherIndex); } final WeakPathInfo arrayPath = new WeakPathInfo(mStorePath); @@ -422,8 +506,8 @@ private void determineInterpolationColor() { } } else if (mLemmaInfo.getLemmaType().equals(":read-const-weakeq")) { // Compute the first partition where the select term (i.e. not the value "v" of "const(v)") is A-local. - final InterpolatorAtomInfo diseqInfo = mInterpolator.getAtomTermInfo(mDiseq); - final ApplicationTerm mainDiseqApp = diseqInfo.getEquality(); + // Use the annotation's disequality: the clause atom may be offset-canonicalized. + final ApplicationTerm mainDiseqApp = getDiseq(mLemmaInfo); final Term left = mainDiseqApp.getParameters()[0]; final Term right = mainDiseqApp.getParameters()[1]; final Term select; @@ -488,23 +572,38 @@ private Term[] findSharedTerms(final Term term) { if (sharedTermCounter == mNumInterpolants) { return sharedTerms; } - // If the term is not shared itself in all partitions, we go through the equalities - for (final SymmetricPair eq : mEqualities.keySet()) { + // If the term is not shared itself in all partitions, we go through the equalities. An equality relates the + // term if one side has the same offset-free part; the shared candidate is then the other side plus the + // difference of the constants (for the mixed case, the mixed variable plus that difference). + final OffsetTerm termSplit = new OffsetTerm(term); + for (final Map.Entry entry : mEqualities.entrySet()) { // Always check first if we still need to search. if (sharedTermCounter == mNumInterpolants) { return sharedTerms; } - if (eq.getFirst().equals(term) || eq.getSecond().equals(term)) { - final LitInfo eqInfo = mInterpolator.getAtomOccurenceInfo(mEqualities.get(eq)); - for (int color = 0; color < mNumInterpolants; color++) { - if (eqInfo.isMixed(color)) { // in those partitions, term is local - sharedTerms[color] = eqInfo.getMixedVar(); - } else if (sharedTerms[color] == null) { // term itself wasn't shared - final Term otherTerm = eq.getFirst().equals(term) ? eq.getSecond() : eq.getFirst(); - final Occurrence otherOccur = mInterpolator.getOccurrence(otherTerm); - if (otherOccur.isAB(color)) { - sharedTerms[color] = otherTerm; - } + final InterpolatorAtomInfo atomTermInfo = mInterpolator.getAtomTermInfo(entry.getValue()); + final Term[] eqParams = atomTermInfo.getEquality().getParameters(); + final OffsetTerm leftSplit = new OffsetTerm(eqParams[0]); + final OffsetTerm rightSplit = new OffsetTerm(eqParams[1]); + final Term otherTerm; + final Rational shift; + if (leftSplit.getTerm() == termSplit.getTerm()) { + shift = termSplit.getOffset().sub(leftSplit.getOffset()); + otherTerm = eqParams[1]; + } else if (rightSplit.getTerm() == termSplit.getTerm()) { + shift = termSplit.getOffset().sub(rightSplit.getOffset()); + otherTerm = eqParams[0]; + } else { + continue; + } + final LitInfo eqInfo = mInterpolator.getAtomOccurenceInfo(entry.getValue()); + for (int color = 0; color < mNumInterpolants; color++) { + if (eqInfo.isMixed(color)) { // in those partitions, term is local + sharedTerms[color] = addOffset(eqInfo.getMixedVar(), shift); + } else if (sharedTerms[color] == null) { // term itself wasn't shared + final Occurrence otherOccur = mInterpolator.getOccurrence(otherTerm); + if (otherOccur.isAB(color)) { + sharedTerms[color] = addOffset(otherTerm, shift); } } } @@ -518,20 +617,16 @@ private Term[] findSharedTerms(final Term term) { * is B-local and the main diseq is A -> it is a premise for the path summaries */ private void addIndexEqualityReadOverWeakeq(final WeakPathInfo mainPath) { - final LitInfo indexEqInfo = mInterpolator.getAtomOccurenceInfo(mIndexEquality); - final InterpolatorAtomInfo diseqInfo = mInterpolator.getAtomTermInfo(mDiseq); - final ApplicationTerm mainDiseqApp = diseqInfo.getEquality(); - final Term otherIndex = getIndexFromSelect(mainDiseqApp.getParameters()[0]).equals(mStorePath.getIndex()) - ? getIndexFromSelect(mainDiseqApp.getParameters()[1]) - : getIndexFromSelect(mainDiseqApp.getParameters()[0]); + final OffsetLitInfo indexEqInfo = mIndexEquality.mInfo; + final Term otherIndex = mIndexEquality.mProjIndex; final Occurrence otherIndexOccur = mInterpolator.getOccurrence(otherIndex); for (int color = 0; color < mNumInterpolants; color++) { if (mainPath.mSharedIndex[color] != null && mainPath.mSharedIndex[color] == mStorePath.getIndex()) { if (mDiseqInfo.isALocal(color) && indexEqInfo.isBLocal(color)) { - mInterpolants[color].add(mTheory.not(mIndexEquality)); + mInterpolants[color].add(mTheory.not(mIndexEquality.mAtom)); } else if (!mDiseqInfo.isALocal(color) && indexEqInfo.isALocal(color)) { if (otherIndexOccur.isAB(color)) { - mInterpolants[color].add(mIndexEquality); + mInterpolants[color].add(mIndexEquality.mAtom); } } } @@ -805,16 +900,29 @@ private Term buildConstPathInterpolant(final boolean isAPath, final Term sharedA class ProofPath { private final Term mPathIndex; private final Term[] mPath; + /** + * The select/const edge {@code {left, right}} justifying the single weak-congruence step of this weak path, or + * null if the path has none. Each element is either a select term or the value of a const array; {@code left} + * is on the {@code mPath[0]} side. + */ + private final Term[] mSelectEdge; private ProofPath(final String type, final Object path) { if (type.equals(":subpath")) { mPathIndex = null; mPath = (Term[]) path; + mSelectEdge = null; } else { assert type.equals(":weakpath"); final Object[] indexAndPath = (Object[]) path; mPathIndex = (Term) indexAndPath[0]; mPath = (Term[]) indexAndPath[1]; + if (indexAndPath.length > 2) { + final Object[] edge = (Object[]) indexAndPath[2]; + mSelectEdge = new Term[] { (Term) edge[0], (Term) edge[1] }; + } else { + mSelectEdge = null; + } } } @@ -825,6 +933,10 @@ public Term getIndex() { public Term[] getPath() { return mPath; } + + public Term[] getSelectEdge() { + return mSelectEdge; + } } /** @@ -843,6 +955,11 @@ class WeakPathInfo { * The array containing the array terms on this path. */ Term[] mPath; + /** + * The select/const edge {@code {left, right}} of this weak path from the lemma annotation, or null. It + * justifies the path's single weak-congruence step; {@code left} is on the {@code mPath[0]} side. + */ + Term[] mPathSelectEdge; /** * The set of partitions for which there is an AB-shared path from start to end. */ @@ -881,6 +998,7 @@ public WeakPathInfo(final ProofPath path) { mPathIndex = path.getIndex(); mSharedIndex = new Term[mNumInterpolants]; mPath = path.getPath(); + mPathSelectEdge = path.getSelectEdge(); mHasABPath = new BitSet(mNumInterpolants); mHasABPath.set(0, mNumInterpolants); mMaxColor = mNumInterpolants; @@ -907,8 +1025,8 @@ public Set[] interpolateWeakPathInfo(final boolean close) { final String lemmaType = mLemmaInfo.getLemmaType(); if (lemmaType.equals(":read-over-weakeq") || lemmaType.equals(":read-const-weakeq")) { // The select or value term of the main diseq corresponding to the left path end determines start color. - final InterpolatorAtomInfo diseqInfo = mInterpolator.getAtomTermInfo(mDiseq); - final Term[] diseqTerms = diseqInfo.getEquality().getParameters(); + // Use the annotation's disequality; the clause atom may be offset-canonicalized. + final Term[] diseqTerms = getDiseq(mLemmaInfo).getParameters(); if (isSelectTerm(diseqTerms[0]) && getArrayFromSelect(diseqTerms[0]).equals(mPath[0]) || isConstArray(mPath[0]) && getValueFromConst(mPath[0]).equals(diseqTerms[0])) { headTerm = diseqTerms[0]; @@ -932,14 +1050,14 @@ public Set[] interpolateWeakPathInfo(final boolean close) { for (int i = 0; i < mPath.length - 1; i++) { final Term left = mPath[i]; final Term right = mPath[i + 1]; - final Term lit = mEqualities.get(new SymmetricPair<>(left, right)); + final Term lit = mEqualities.get(new OffsetEqKey(left, right)); Term boundaryTerm = left; // Each step in a weak path can be either an equality literal or a store step of form "a (store a k v)", - // or, in weakeq-ext lemmas only, a select equality. + // or, in weakeq-ext lemmas only, the select/const edge annotated at the weak path. // In the second and third case, there is no equality literal for the arrays in the lemma. if (lit == null) { - if (!mLemmaInfo.getLemmaType().equals(":weakeq-ext") || !checkAndAddSelectEdgeStep(left, right)) { + if (!checkAndAddSelectEdgeStep(left, right)) { addStoreEdgeStep(left, right); } } else { // For equality steps, we just close or open A paths. @@ -985,54 +1103,75 @@ private void addStoreEdgeStep(Term left, Term right) { final Term boundaryTerm = left; final Occurrence stepOcc = mInterpolator.getOccurrence(storeTerm); final Term storeIndex = getIndexFromStore(storeTerm); - final Term indexDiseq = mDisequalities.get(new SymmetricPair<>(storeIndex, mPathIndex)); + final ArrayLit indexDiseq = lookupDisequality(storeIndex, mPathIndex); if (indexDiseq != null) { - final Occurrence indexDiseqOcc = mInterpolator.getAtomOccurenceInfo(indexDiseq); + final Occurrence indexDiseqOcc = indexDiseq.mInfo.getLitInfo(); final Occurrence intersectOcc = stepOcc.intersect(indexDiseqOcc); mTail.closeAPath(mHead, boundaryTerm, stepOcc); mTail.closeAPath(mHead, boundaryTerm, intersectOcc); mTail.openAPath(mHead, boundaryTerm, intersectOcc); mTail.openAPath(mHead, boundaryTerm, stepOcc); - mTail.addIndexDisequality(mHead, indexDiseq); + mTail.addIndexDisequality(mHead, new ProjectedIndexLit(indexDiseq, storeIndex)); } else { // Otherwise indexDiseq is a trivial disequality like x = x + 1. // Treat it as a shared disequality. + assert isTriviallyDistinct(storeIndex, mPathIndex); mTail.closeAPath(mHead, boundaryTerm, stepOcc); mTail.openAPath(mHead, boundaryTerm, stepOcc); } } + /** + * Check whether the step between left and right is this weak path's select/const edge (annotated at the weak + * path by the lemma producer) and if so, add it. The corresponding select equality literal is found via the + * offset-aware lookup; the edge terms from the annotation provide the path-level select/value structure, as + * the clause literal may be offset-canonicalized. + */ private boolean checkAndAddSelectEdgeStep(Term left, Term right) { - final MatchingSelectEquality match = findSelectEquality(left, right); - if (match == null) { + if (mPathSelectEdge == null) { + return false; + } + // The edge's left term is on the mPath[0] side; check that the edge belongs to this step. Each edge term + // either is a select on the step's array or the value of the step's const array — and a select term can + // itself be the value of a const array, so both cases must be considered regardless of the term's shape. + final Term edgeLeft = mPathSelectEdge[0]; + final Term edgeRight = mPathSelectEdge[1]; + final boolean isConstLeft = isConstArray(left) && getValueFromConst(left).equals(edgeLeft); + final boolean isConstRight = isConstArray(right) && getValueFromConst(right).equals(edgeRight); + final boolean leftMatches = isConstLeft + || isSelectTerm(edgeLeft) && getArrayFromSelect(edgeLeft).equals(left); + final boolean rightMatches = isConstRight + || isSelectTerm(edgeRight) && getArrayFromSelect(edgeRight).equals(right); + if (!leftMatches || !rightMatches) { return false; } - final Term selectEq = match.mEqualityLit; - final InterpolatorAtomInfo termInfo = mInterpolator.getAtomTermInfo(selectEq); - final LitInfo stepInfo = mInterpolator.getAtomOccurenceInfo(selectEq); - final ApplicationTerm selectEqApp = termInfo.getEquality(); - final Term leftSelectOrValue = selectEqApp.getParameters()[match.mIsSwapped ? 1 : 0]; - final Term rightSelectOrValue = selectEqApp.getParameters()[match.mIsSwapped ? 0 : 1]; + final ArrayLit selectLit = lookupEquality(edgeLeft, edgeRight); + assert selectLit != null : "select edge without matching literal"; + final Term selectEq = selectLit.mAtom; + final LitInfo stepInfo = selectLit.mInfo.getLitInfo(); Term boundaryTerm = left; mTail.closeAPath(mHead, boundaryTerm, stepInfo); mTail.openAPath(mHead, boundaryTerm, stepInfo); final TermVariable mixedVar = stepInfo.getMixedVar(); if (mixedVar != null) { - final Occurrence leftOcc = mInterpolator.getOccurrence(leftSelectOrValue); + // The mixed variable denotes the literal-level select value; a constant shift between the literal + // and the path-level edge is not yet supported here (the marker mechanism cannot carry it). + assert selectLit.mInfo.getShift().equals(Rational.ZERO) : "mixed select edge with offset"; + final Occurrence leftOcc = mInterpolator.getOccurrence(edgeLeft); // The left select can be A-local although we were on a B-path (and vice versa) mTail.closeAPath(mHead, boundaryTerm, leftOcc); mTail.openAPath(mHead, boundaryTerm, leftOcc); } // Add the index equality for the first select term (if it is a select) - if (!match.mIsConstLeft) { - mTail.addSelectIndexEquality(mHead, leftSelectOrValue); + if (!isConstLeft) { + mTail.addSelectIndexEquality(mHead, edgeLeft); } if (mixedVar != null) { - final Occurrence rightOcc = mInterpolator.getOccurrence(rightSelectOrValue); - if (match.mIsConstLeft || match.mIsConstRight) { + final Occurrence rightOcc = mInterpolator.getOccurrence(edgeRight); + if (isConstLeft || isConstRight) { // It is a const select equality - we close the path with const(mixedVar) boundaryTerm = buildConst(mixedVar, left.getSort()); } else { @@ -1043,8 +1182,8 @@ private boolean checkAndAddSelectEdgeStep(Term left, Term right) { } // The other index equality is added after opening/closing (if the rightTerm is // a select) - if (!match.mIsConstRight) { - mTail.addSelectIndexEquality(mHead, rightSelectOrValue); + if (!isConstRight) { + mTail.addSelectIndexEquality(mHead, edgeRight); } return true; } @@ -1073,7 +1212,7 @@ public void collectStorePaths() { for (int i = 0; i < mPath.length - 1; i++) { final Term left = mPath[i]; final Term right = mPath[i + 1]; - final Term lit = mEqualities.get(new SymmetricPair<>(left, right)); + final Term lit = mEqualities.get(new OffsetEqKey(left, right)); Term boundaryTerm; boundaryTerm = left; @@ -1241,89 +1380,6 @@ public Set[] interpolateStorePathInfoConst() { return mPathInterpolants; } - class MatchingSelectEquality { - Term mEqualityLit; - boolean mIsSwapped; - boolean mIsConstLeft; - boolean mIsConstRight; - - public MatchingSelectEquality(Term equalityLit, - boolean isSwapped, boolean isConstLeft, boolean isConstRight) { - mEqualityLit = equalityLit; - mIsSwapped = isSwapped; - mIsConstLeft = isConstLeft; - mIsConstRight = isConstRight; - } - } - - /** - * For a step in an index path of a weakeq-ext lemma that is not an array equality, check if we can find a - * select equality between the arrays and corresponding index equalities. - * - * In the presence of constant arrays, one side of the select equality can be the value "v" of a "const(v)". - * - * @return the select equality if it exists, else null. - */ - private MatchingSelectEquality findSelectEquality(final Term leftArray, final Term rightArray) { - for (final SymmetricPair testEq : mEqualities.keySet()) { - // Find some select equality. - final Term eqLeft = testEq.getFirst(); - final Term eqRight = testEq.getSecond(); - final boolean isLeftSelect = isSelectTerm(eqLeft); - final boolean isRightSelect = isSelectTerm(eqRight); - if (isLeftSelect && isRightSelect) { - // Check equalities of the form "arr1[j1] = arr2[j2]". - // Check if the arrays of the select terms match the term pair. - if (isGoodSelect(eqLeft, leftArray) && isGoodSelect(eqRight, rightArray)) { - return new MatchingSelectEquality(mEqualities.get(testEq), false, false, false); - } - if (isGoodSelect(eqRight, leftArray) && isGoodSelect(eqLeft, rightArray)) { - return new MatchingSelectEquality(mEqualities.get(testEq), true, false, false); - } - } - if (isConstArray(leftArray)) { - // Check equalities of the form "arr[j] = v". - final Term value = getValueFromConst(leftArray); - if (eqLeft == value && isRightSelect && isGoodSelect(eqRight, rightArray)) { - return new MatchingSelectEquality(mEqualities.get(testEq), false, true, false); - } - if (eqRight == value && isLeftSelect && isGoodSelect(eqLeft, rightArray)) { - return new MatchingSelectEquality(mEqualities.get(testEq), true, true, false); - } - } - if (isConstArray(rightArray)) { - // Check equalities of the form "arr[j] = v". - final Term value = getValueFromConst(rightArray); - if (eqLeft == value && isRightSelect && isGoodSelect(eqRight, leftArray)) { - return new MatchingSelectEquality(mEqualities.get(testEq), true, false, true); - } - if (eqRight == value && isLeftSelect && isGoodSelect(eqLeft, leftArray)) { - return new MatchingSelectEquality(mEqualities.get(testEq), false, false, true); - } - } - } - // No select equality could be found. - return null; - } - - /** - * Check if the selectTerm is a select on the array term with an index matching - * mPathIndex. - * - * @param selectTerm The select term. Caller must ensure it is a select. - * @param arrayTerm The array term on which the select term must match. - * @return true if the array term match and the index either equals mPathIndex - * or a corresponding equality exists. - */ - private boolean isGoodSelect(Term selectTerm, Term arrayTerm) { - if (getArrayFromSelect(selectTerm) == arrayTerm) { - final Term selectIndex = getIndexFromSelect(selectTerm); - return selectIndex == mPathIndex - || mEqualities.containsKey(new SymmetricPair<>(selectIndex, mPathIndex)); - } - return false; - } - /** * Close the path using the main disequality. * @@ -1354,8 +1410,7 @@ public void addDiseq(final Occurrence headOcc, final Occurrence tailOcc) { if (mLemmaInfo.getLemmaType().equals(":read-over-weakeq")) { if (mIndexEquality != null) { - final LitInfo indexEqInfo = mInterpolator.getAtomOccurenceInfo(mIndexEquality); - mTail.addSelectIndexEqAllColors(mHead, indexEqInfo, mIndexEquality); + mTail.addSelectIndexEqAllColors(mHead, mIndexEquality.mInfo.getLitInfo(), mIndexEquality); } mTail.closeAPath(mHead, mDiseq, headOcc); @@ -1502,33 +1557,29 @@ private void closeWeakeqExt(final Occurrence headOcc, final Occurrence tailOcc) * */ private Term buildFPiTerm(final boolean isAPath, final int color, final Term sharedIndex, - final ArrayList indexDiseqs, final ArrayList indexEqs) { + final ArrayList indexDiseqs, final ArrayList indexEqs) { if (indexDiseqs == null && indexEqs == null) { return isAPath ? mTheory.mFalse : mTheory.mTrue; } final Set indexTerms = new HashSet<>(); if (indexDiseqs != null) { - for (final Term diseq : indexDiseqs) { - final InterpolatorAtomInfo termInfo = mInterpolator.getAtomTermInfo(diseq); - final LitInfo info = mInterpolator.getAtomOccurenceInfo(diseq); - final ApplicationTerm diseqApp = termInfo.getEquality(); + for (final ProjectedIndexLit diseq : indexDiseqs) { // Collected index diseqs are either mixed or B-local on A-paths (resp. A-local on B-paths). // In the first case, there is a mixed term, in the second, the store index is shared. - if (info.isMixed(color)) { - final Term var = info.getMixedVar(); - final Term projection; - projection = mTheory.term(Interpolator.EQ, var, sharedIndex); - indexTerms.add(projection); + if (diseq.mInfo.isMixed(color)) { + // The mixed variable denotes the literal-level index values; shift the path-level shared + // index down accordingly. + indexTerms.add(diseq.mInfo.buildEQ(sharedIndex)); } else { - final Term index = diseqApp.getParameters()[0].equals(mPathIndex) ? diseqApp.getParameters()[1] - : diseqApp.getParameters()[0]; + // The path-level store index (the literal's own parameter may be shifted by a constant). + final Term index = diseq.mProjIndex; // On A-paths, the negated B-projection of the index diseq is added. // It is always an equality (representing an EQ term for mixed index diseqs). Term projection = mTheory.equals(index, sharedIndex); // On B-paths, the A-projection of the index diseq is added. // It is an equality (EQ-term) for mixed index diseq, and a disequality for A-local index diseq. - if (!isAPath && info.isALocal(color)) { + if (!isAPath && diseq.mInfo.isALocal(color)) { projection = mTheory.not(projection); } indexTerms.add(projection); @@ -1536,15 +1587,11 @@ private Term buildFPiTerm(final boolean isAPath, final int color, final Term sha } } if (indexEqs != null) { - for (final Term eq : indexEqs) { - final InterpolatorAtomInfo termInfo = mInterpolator.getAtomTermInfo(eq); - final LitInfo info = mInterpolator.getAtomOccurenceInfo(eq); - final ApplicationTerm eqApp = termInfo.getEquality(); + for (final ProjectedIndexLit eq : indexEqs) { // Index eqs are either mixed or B-local on A-paths (resp. A-local on B-paths). - // In the first case, there is a mixed term, in the second, the select index is shared. - final Term index = info.isMixed(color) ? info.getMixedVar() - : eqApp.getParameters()[0].equals(mPathIndex) ? eqApp.getParameters()[1] - : eqApp.getParameters()[0]; + // In the first case, there is a mixed term (lifted to path level), in the second, the select + // index is shared (we use the path-level term; the literal's parameter may be shifted). + final Term index = eq.mInfo.isMixed(color) ? eq.mInfo.getMixedBoundary() : eq.mProjIndex; // On B-paths, the A-projection is added. It is always an equality. Term projection = mTheory.equals(index, sharedIndex); // On A-paths, the negated B-projection is added. It is always a disequality. @@ -1589,12 +1636,12 @@ class WeakPathEnd { * For each partition this contains the set of B(resp. A)-local and mixed store index disequalities found on * the A (resp. B) path so far. */ - ArrayList[] mIndexDiseqs; + ArrayList[] mIndexDiseqs; /** * For each partition this contains the set of B(resp. A)-local and mixed select index equalities found on * the A (resp. B) path so far. */ - ArrayList[] mIndexEqs; + ArrayList[] mIndexEqs; /** * For each partition, this stores the store indices on the A (B) path so far. This is only used for the * main path of a weakeq-ext lemma. @@ -1742,8 +1789,8 @@ private void openSingleAPath(final WeakPathEnd other, final Term boundary, final * @param storeTerm * The store term from which we extract the store index. */ - private void addIndexDisequality(final WeakPathEnd other, final Term diseq) { - final LitInfo diseqInfo = mInterpolator.getAtomOccurenceInfo(diseq); + private void addIndexDisequality(final WeakPathEnd other, final ProjectedIndexLit diseq) { + final LitInfo diseqInfo = diseq.mInfo.getLitInfo(); // The diseq has to be added to all partitions where it is mixed and all partitions that lie on the // tree path between the partition of the diseq and the partition of the store term. @@ -1763,7 +1810,7 @@ private void addIndexDisequality(final WeakPathEnd other, final Term diseq) { * the path is. */ private void addIndexDiseqAllColors(final WeakPathEnd other, final Occurrence occur, - final Term diseq) { + final ProjectedIndexLit diseq) { int currentColor = mColor; // Up mHasABPath.and(occur.mInA); @@ -1797,7 +1844,8 @@ private void addIndexDiseqAllColors(final WeakPathEnd other, final Occurrence oc /** * Add the index disequality to one partition. */ - private void addIndexDiseqOneColor(final WeakPathEnd other, final Term diseq, final int color) { + private void addIndexDiseqOneColor(final WeakPathEnd other, final ProjectedIndexLit diseq, + final int color) { // If the path is still open at the other path end, i.e. if other.mLastChange[color] is still null, we // have to store the diseq in the other pathend if (other.mLastChange[color] == null) { @@ -1827,8 +1875,10 @@ private void addSelectIndexEquality(final WeakPathEnd other, final Term selectTe assert isSelectTerm(selectTerm); if (getIndexFromSelect(selectTerm) != mPathIndex) { final Term selectIndex = getIndexFromSelect(selectTerm); - final Term indexEq = mEqualities.get(new SymmetricPair<>(selectIndex, mPathIndex)); - final LitInfo eqInfo = mInterpolator.getAtomOccurenceInfo(indexEq); + final ArrayLit indexEqLit = lookupEquality(selectIndex, mPathIndex); + assert indexEqLit != null; + final ProjectedIndexLit indexEq = new ProjectedIndexLit(indexEqLit, selectIndex); + final LitInfo eqInfo = indexEqLit.mInfo.getLitInfo(); addSelectIndexEqAllColors(other, eqInfo, indexEq); if (eqInfo.getMixedVar() != null) { final Occurrence occur = mInterpolator.getOccurrence(mPathIndex); @@ -1842,7 +1892,8 @@ private void addSelectIndexEquality(final WeakPathEnd other, final Term selectTe * those partitions. This adds the index equality to all partitions where it is not in A (resp. B) while the * path is. */ - private void addSelectIndexEqAllColors(final WeakPathEnd other, final Occurrence occur, final Term eq) { + private void addSelectIndexEqAllColors(final WeakPathEnd other, final Occurrence occur, + final ProjectedIndexLit eq) { int currentColor = mColor; // Up mHasABPath.and(occur.mInA); @@ -1876,7 +1927,8 @@ private void addSelectIndexEqAllColors(final WeakPathEnd other, final Occurrence /** * Add the index equality to one partition. */ - private void addSelectIndexEqOneColor(final WeakPathEnd other, final Term eq, final int color) { + private void addSelectIndexEqOneColor(final WeakPathEnd other, final ProjectedIndexLit eq, + final int color) { // If the path is still open at the other path end, i.e. if other.mLastChange[color] is still null, we // have to store the diseq in the other pathend if (other.mLastChange[color] == null) { @@ -1975,16 +2027,12 @@ private void addInterpolantClausePathSeg(final boolean isAPath, final int color, // Use shared store indices to rewrite "left" to "right" in order to shorten the weq- or nweq-term. final Set sharedIndices = new HashSet<>(); if (mIndexDiseqs[color] != null) { - final Iterator it = mIndexDiseqs[color].iterator(); + final Iterator it = mIndexDiseqs[color].iterator(); while (it.hasNext()) { - final Term diseq = it.next(); - final InterpolatorAtomInfo termInfo = mInterpolator.getAtomTermInfo(diseq); - final LitInfo info = mInterpolator.getAtomOccurenceInfo(diseq); - if (!info.isMixed(color)) { - final ApplicationTerm diseqApp = termInfo.getEquality(); - final Term storeIndex = - diseqApp.getParameters()[0].equals(mPathIndex) ? diseqApp.getParameters()[1] - : diseqApp.getParameters()[0]; + final ProjectedIndexLit diseq = it.next(); + if (!diseq.mInfo.isMixed(color)) { + // The path-level store index (the literal's parameter may be constant-shifted). + final Term storeIndex = diseq.mProjIndex; final Occurrence storeOcc = mInterpolator.getOccurrence(storeIndex); if (storeOcc.isAB(color)) { sharedIndices.add(storeIndex); @@ -2266,12 +2314,12 @@ private void buildRecursiveInterpolant(final int color, final WeakPathEnd other, // Needed for case without shared index (then there are no indexEqs) final int fPiOrderForRecursion; if (equals(mHead)) { // recursionPath is the left outer path - final ArrayList indexDiseqs = indexPath.mTail.mIndexDiseqs[color]; + final ArrayList indexDiseqs = indexPath.mTail.mIndexDiseqs[color]; fPiOrderForRecursion = indexDiseqs == null ? 0 : indexDiseqs.size(); fPi = indexPath.buildFPiTerm(!isAPath, color, rewriteAtIndex, indexDiseqs, indexPath.mTail.mIndexEqs[color]); } else { // recursionPath is the right outer path - final ArrayList indexDiseqs = indexPath.mHead.mIndexDiseqs[color]; + final ArrayList indexDiseqs = indexPath.mHead.mIndexDiseqs[color]; fPiOrderForRecursion = indexDiseqs == null ? 0 : indexDiseqs.size(); fPi = indexPath.buildFPiTerm(!isAPath, color, rewriteAtIndex, indexDiseqs, indexPath.mHead.mIndexEqs[color]); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/CCInterpolator.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/CCInterpolator.java index 6e925164d..b1bc3106d 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/CCInterpolator.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/CCInterpolator.java @@ -34,7 +34,7 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.LitInfo; import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.Occurrence; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.linar.InfinitesimalNumber; -import de.uni_freiburg.informatik.ultimate.smtinterpol.util.SymmetricPair; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.OffsetEqKey; /** * The interpolator for congruence-closure theory lemmata. @@ -44,8 +44,8 @@ public class CCInterpolator { Interpolator mInterpolator; - LitInfo mDiseqOccurrences; - HashMap, LitInfo> mEqualityOccurrences; + OffsetLitInfo mDiseqInfo; + HashMap mEqualityOccurrences; Theory mTheory; int mNumInterpolants; @@ -172,13 +172,13 @@ public void openAPath(final PathEnd other, final Term boundaryTerm, final Occurr } } - public void closeAPathMixed(final TermVariable mixedVar, final Occurrence occur) { + public void closeAPathMixed(final OffsetLitInfo diseqInfo, final Occurrence occur) { // go to the mixed parent and insert the EQ to the boundary term for all partitions on the path. while (occur.isBLocal(mColor)) { final int color = mColor; mColor = getParent(color); assert color < mMaxColor; - mInterpolants[color].add(mTheory.term(Interpolator.EQ, mixedVar, mTerm[color])); + mInterpolants[color].add(diseqInfo.buildEQ(mTerm[color])); mTerm[color] = null; } } @@ -244,19 +244,20 @@ private Term[] interpolateCongruence(final ApplicationTerm left, final Applicati final Term[] interpolants = new Term[mNumInterpolants]; final Term[] leftParams = left.getParameters(); final Term[] rightParams = right.getParameters(); - final LitInfo[] paramInfos = new LitInfo[leftParams.length]; + final OffsetLitInfo[] paramInfos = new OffsetLitInfo[leftParams.length]; assert left.getFunction() == right.getFunction() && leftParams.length == rightParams.length; + final OffsetLitInfo diseqInfo = mDiseqInfo.reorient(new OffsetEqKey(left, right)); for (int i = 0; i < leftParams.length; i++) { if (leftParams[i] == rightParams[i]) { paramInfos[i] = null; } else { - paramInfos[i] = mEqualityOccurrences.get(new SymmetricPair<>(leftParams[i], rightParams[i])); + paramInfos[i] = getEqualityInfo(leftParams[i], rightParams[i]); } } for (int part = 0; part < mNumInterpolants; part++) { - if (mDiseqOccurrences.isBorShared(part)) { + if (diseqInfo.isBorShared(part)) { final ArrayDeque terms = new ArrayDeque<>(leftParams.length); for (int paramNr = 0; paramNr < leftParams.length; paramNr++) { // Collect A-local literals. @@ -264,14 +265,14 @@ private Term[] interpolateCongruence(final ApplicationTerm left, final Applicati terms.add(mTheory.term("=", leftParams[paramNr], rightParams[paramNr])); } else if (paramInfos[paramNr] != null && paramInfos[paramNr].isMixed(part)) { // Collect A-local parts in mixed parameter equalities. - final TermVariable mixedVar = paramInfos[paramNr].getMixedVar(); - final Term sideA = paramInfos[paramNr].getLhsOccur().isALocal(part) ? leftParams[paramNr] + final Term mixedBoundary = paramInfos[paramNr].getMixedBoundary(); + final Term sideA = paramInfos[paramNr].isLeftALocal(part) ? leftParams[paramNr] : rightParams[paramNr]; - terms.add(mTheory.term("=", mixedVar, sideA)); + terms.add(mTheory.term("=", mixedBoundary, sideA)); } } interpolants[part] = mTheory.and(terms.toArray(new Term[terms.size()])); - } else if (mDiseqOccurrences.isALocal(part)) { + } else if (diseqInfo.isALocal(part)) { final ArrayDeque terms = new ArrayDeque<>(leftParams.length); for (int paramNr = 0; paramNr < leftParams.length; paramNr++) { // Collect negated B-local literals. @@ -279,10 +280,10 @@ private Term[] interpolateCongruence(final ApplicationTerm left, final Applicati terms.add(mTheory.not(mTheory.term("=", leftParams[paramNr], rightParams[paramNr]))); } else if (paramInfos[paramNr] != null && paramInfos[paramNr].isMixed(part)) { // Collect B-local parts in mixed parameter equalities. - final TermVariable mixedVar = paramInfos[paramNr].getMixedVar(); - final Term sideB = paramInfos[paramNr].getLhsOccur().isBLocal(part) ? leftParams[paramNr] + final Term mixedBoundary = paramInfos[paramNr].getMixedBoundary(); + final Term sideB = paramInfos[paramNr].isLeftBLocal(part) ? leftParams[paramNr] : rightParams[paramNr]; - terms.add(mTheory.not(mTheory.term("=", mixedVar, sideB))); + terms.add(mTheory.not(mTheory.term("=", mixedBoundary, sideB))); } } interpolants[part] = mTheory.or(terms.toArray(new Term[terms.size()])); @@ -296,8 +297,8 @@ private Term[] interpolateCongruence(final ApplicationTerm left, final Applicati // term occurs left and right, so this is obviously shared boundaryTerms[paramNr] = leftParams[paramNr]; } else if (paramInfos[paramNr].isMixed(part)) { - // mixed case: take mixed var - boundaryTerms[paramNr] = paramInfos[paramNr].getMixedVar(); + // mixed case: take mixed var (shifted to the level of the congruence arguments) + boundaryTerms[paramNr] = paramInfos[paramNr].getMixedBoundary(); } else if (paramInfos[paramNr].isAorShared(part)) { // the argument of the B-local side of the congruence is shared boundaryTerms[paramNr] = isLeftAlocal ? rightParams[paramNr] : leftParams[paramNr]; @@ -310,7 +311,7 @@ private Term[] interpolateCongruence(final ApplicationTerm left, final Applicati } } final Term sharedTerm = mTheory.term(left.getFunction(), boundaryTerms); - interpolants[part] = mTheory.term(Interpolator.EQ, mDiseqOccurrences.getMixedVar(), sharedTerm); + interpolants[part] = diseqInfo.buildEQ(sharedTerm); } } return interpolants; @@ -344,25 +345,26 @@ public Term[] interpolateTransitivity() { for (int i = 0; i < mPath.length - 1; i++) { final Term left = mPath[i]; final Term right = mPath[i + 1]; - final LitInfo info = mEqualityOccurrences.get(new SymmetricPair<>(left, right)); - mTail.closeAPath(mHead, left, info); - mTail.openAPath(mHead, left, info); + final OffsetLitInfo info = getEqualityInfo(left, right); + mTail.closeAPath(mHead, left, info.getLitInfo()); + mTail.openAPath(mHead, left, info.getLitInfo()); if (info.getMixedVar() != null) { final Occurrence occ = mInterpolator.getOccurrence(right); - mTail.closeAPath(mHead, info.getMixedVar(), occ); - mTail.openAPath(mHead, info.getMixedVar(), occ); + mTail.closeAPath(mHead, info.getMixedBoundary(), occ); + mTail.openAPath(mHead, info.getMixedBoundary(), occ); } } // add the disequality if present - if (mDiseqOccurrences != null) { - mTail.closeAPath(mHead, tailTerm, mDiseqOccurrences); - mTail.openAPath(mHead, tailTerm, mDiseqOccurrences); - mHead.closeAPath(mTail, headTerm, mDiseqOccurrences); - mHead.openAPath(mTail, headTerm, mDiseqOccurrences); - - if (mDiseqOccurrences.getMixedVar() != null) { - mTail.closeAPathMixed(mDiseqOccurrences.getMixedVar(), headOccur); - mHead.closeAPathMixed(mDiseqOccurrences.getMixedVar(), tailOccur); + if (mDiseqInfo != null) { + final OffsetLitInfo diseqInfo = mDiseqInfo.reorient(new OffsetEqKey(headTerm, tailTerm)); + mTail.closeAPath(mHead, tailTerm, diseqInfo.getLitInfo()); + mTail.openAPath(mHead, tailTerm, diseqInfo.getLitInfo()); + mHead.closeAPath(mTail, headTerm, diseqInfo.getLitInfo()); + mHead.openAPath(mTail, headTerm, diseqInfo.getLitInfo()); + + if (diseqInfo.getMixedVar() != null) { + mTail.closeAPathMixed(diseqInfo, headOccur); + mHead.closeAPathMixed(diseqInfo, tailOccur); } } // close the still open ends where head and tail have different color. The headTerm/tailTerm must be @@ -424,13 +426,13 @@ public Term[] interpolateInstantiation(final InterpolatorClauseInfo proofTermInf terms.add(lits[i]); } else if (litInfo.isMixed(part)) { final TermVariable mixedVar = litInfo.getMixedVar(); - InterpolatorAtomInfo atomInfo = mInterpolator.getAtomTermInfo(atom); + final InterpolatorAtomInfo atomInfo = mInterpolator.getAtomTermInfo(atom); if (atomInfo.isCCEquality()) { // Collect B-part from splitting mixed literal. final Term sideB = litInfo.getLhsOccur().isBLocal(part) ? ((ApplicationTerm) atom).getParameters()[0] : ((ApplicationTerm) atom).getParameters()[1]; - + if (mInterpolator.isNegatedTerm(lits[i])) { terms.add(mTheory.not(mTheory.term(SMTLIBConstants.EQUALS, mixedVar, sideB))); } else { @@ -480,7 +482,7 @@ public Term[] interpolateInstantiation(final InterpolatorClauseInfo proofTermInf } else if (litInfo.isMixed(part)) { // Collect A-part from splitting mixed literal. final TermVariable mixedVar = litInfo.getMixedVar(); - InterpolatorAtomInfo atomInfo = mInterpolator.getAtomTermInfo(atom); + final InterpolatorAtomInfo atomInfo = mInterpolator.getAtomTermInfo(atom); if (atomInfo.isCCEquality()) { final Term sideA = litInfo.getLhsOccur().isALocal(part) ? ((ApplicationTerm) atom).getParameters()[0] @@ -523,21 +525,35 @@ public Term[] interpolateInstantiation(final InterpolatorClauseInfo proofTermInf return interpolants; } + /** + * Find the literal info for a clause equality justifying the step {@code left = right}. The literal may differ + * from the step by a constant shift and may have swapped sides; the returned info accounts for both. + * + * @return the reoriented literal info, or null if the clause contains no matching equality. + */ + private OffsetLitInfo getEqualityInfo(final Term left, final Term right) { + final OffsetEqKey key = new OffsetEqKey(left, right); + final OffsetLitInfo info = mEqualityOccurrences.get(key); + return info == null ? null : info.reorient(key); + } + public Term[] computeInterpolants(final InterpolatorClauseInfo proofTermInfo) { // Collect the literal infos for all equalities in the clause. mEqualityOccurrences = new HashMap<>(); for (final Term literal : proofTermInfo.getLiterals()) { final Term atom = mInterpolator.getAtom(literal); + final InterpolatorAtomInfo atomTermInfo = mInterpolator.getAtomTermInfo(atom); + final LitInfo occInfo = mInterpolator.getAtomOccurenceInfo(atom); + assert atomTermInfo.isCCEquality(); + final ApplicationTerm eq = atomTermInfo.getEquality(); + final OffsetEqKey key = new OffsetEqKey(eq.getParameters()[0], eq.getParameters()[1]); + final OffsetLitInfo info = new OffsetLitInfo(mTheory, occInfo, key); if (atom != literal) { // negated equality in clause, i.e., positive in conflict. - final InterpolatorAtomInfo atomTermInfo = mInterpolator.getAtomTermInfo(atom); - final LitInfo occInfo = mInterpolator.getAtomOccurenceInfo(atom); - assert atomTermInfo.isCCEquality(); - final ApplicationTerm eq = atomTermInfo.getEquality(); - mEqualityOccurrences.put(new SymmetricPair<>(eq.getParameters()[0], eq.getParameters()[1]), occInfo); + mEqualityOccurrences.put(key, info); } else { - assert mDiseqOccurrences == null; - mDiseqOccurrences = mInterpolator.getAtomOccurenceInfo(atom); + assert mDiseqInfo == null; + mDiseqInfo = info; } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/DatatypeInterpolator.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/DatatypeInterpolator.java index 98b6da5ad..04069e94c 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/DatatypeInterpolator.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/DatatypeInterpolator.java @@ -21,6 +21,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.Map.Entry; import de.uni_freiburg.informatik.ultimate.logic.ApplicationTerm; import de.uni_freiburg.informatik.ultimate.logic.DataType; @@ -31,6 +32,7 @@ import de.uni_freiburg.informatik.ultimate.logic.Theory; import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.LitInfo; import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.Occurrence; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.OffsetEqKey; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.SymmetricPair; /** @@ -99,6 +101,24 @@ public Term[] computeDatatypeInterpolants(final InterpolatorClauseInfo clauseInf } } + /** + * Compute the literal info oriented and shifted to match the annotation-level equality + * {@code left = right}. For numeric sorts the clause literal is canonicalized and may + * therefore differ from the equality in the lemma annotation by a constant offset (and + * by swapped sides), e.g. clause literal {@code (= b (+ a (- 2)))} for the annotation + * equality {@code (= (+ b 7) (+ a 5))} in a dt-injective lemma. + * + * @param atomPair the clause atom's parameters in atom order. + * @param info the literal info of the clause atom. + * @param left the left-hand side of the annotation equality. + * @param right the right-hand side of the annotation equality. + */ + private OffsetLitInfo getAnnotationLitInfo(final SymmetricPair atomPair, final LitInfo info, + final Term left, final Term right) { + final OffsetEqKey litKey = new OffsetEqKey(atomPair.getFirst(), atomPair.getSecond()); + return new OffsetLitInfo(mTheory, info, litKey).reorient(new OffsetEqKey(left, right)); + } + /** * Interpolate a datatype uniqueness conflict. The conflict has the form * {@code ((_ is cons1) u1) == true, ((_ is cons2) u2) == true, u1 == u2}. The @@ -207,11 +227,15 @@ private Term[] interpolateDTInjective(Object[] annot) { final ApplicationTerm diseqAtom = (ApplicationTerm) annot[0]; final Term[] diseqParams = diseqAtom.getParameters(); assert mDisequalityInfos.size() == 1; - final SymmetricPair diseqPair = mDisequalityInfos.keySet().iterator().next(); - final LitInfo diseqInfo = mDisequalityInfos.get(diseqPair); + final Entry, LitInfo> diseqEntry = mDisequalityInfos.entrySet().iterator().next(); + final OffsetLitInfo diseqInfo = getAnnotationLitInfo(diseqEntry.getKey(), diseqEntry.getValue(), + diseqParams[0], diseqParams[1]); final ApplicationTerm firstConsTerm = (ApplicationTerm) annot[2]; final ApplicationTerm secondConsTerm = (ApplicationTerm) annot[3]; - final LitInfo eqInfo = mEqualityInfos.get(new SymmetricPair<>(firstConsTerm, secondConsTerm)); + assert mEqualityInfos.size() == 1; + final Entry, LitInfo> eqEntry = mEqualityInfos.entrySet().iterator().next(); + final OffsetLitInfo eqInfo = getAnnotationLitInfo(eqEntry.getKey(), eqEntry.getValue(), + firstConsTerm, secondConsTerm); String selFunc = null; for (int partition = 0; partition < mNumInterpolants; partition++) { @@ -228,18 +252,18 @@ private Term[] interpolateDTInjective(Object[] annot) { if (selFunc == null) { selFunc = getSelectorForPair(firstConsTerm, secondConsTerm, diseqParams); } + // sharedSelTerm equals the constructor arguments at annotation level, so the + // shared term must also be the annotation-level argument on the other side. final Term sharedSelTerm = mTheory.term(selFunc, eqInfo.getMixedVar()); if (diseqInfo.isAorShared(partition)) { - final Term sharedTerm = eqInfo.getLhsOccur().isALocal(partition) ? diseqPair.getSecond() - : diseqPair.getFirst(); + final Term sharedTerm = eqInfo.isLeftALocal(partition) ? diseqParams[1] : diseqParams[0]; interpolants[partition] = mTheory.term(SMTLIBConstants.NOT, mTheory.term(SMTLIBConstants.EQUALS, sharedTerm, sharedSelTerm)); } else if (diseqInfo.isBLocal(partition)) { - final Term sharedTerm = eqInfo.getLhsOccur().isALocal(partition) ? diseqPair.getFirst() - : diseqPair.getSecond(); + final Term sharedTerm = eqInfo.isLeftALocal(partition) ? diseqParams[0] : diseqParams[1]; interpolants[partition] = mTheory.term(SMTLIBConstants.EQUALS, sharedTerm, sharedSelTerm); } else { - interpolants[partition] = mTheory.term(Interpolator.EQ, diseqInfo.getMixedVar(), sharedSelTerm); + interpolants[partition] = diseqInfo.buildEQ(sharedSelTerm); } } } @@ -361,7 +385,9 @@ private Term[] interpolateDTProject(Object[] annot) { // equality is missing because it is trivial if (mEqualityInfos.size() == 0) { - final LitInfo diseqInfo = mDisequalityInfos.values().iterator().next(); + final Entry, LitInfo> diseqEntry = mDisequalityInfos.entrySet().iterator().next(); + final OffsetLitInfo diseqInfo = getAnnotationLitInfo(diseqEntry.getKey(), diseqEntry.getValue(), + goalEq.getParameters()[0], goalEq.getParameters()[1]); for (int partition = 0; partition < mNumInterpolants; partition++) { if (diseqInfo.isAorShared(partition)) { interpolants[partition] = mTheory.mFalse; @@ -371,14 +397,14 @@ private Term[] interpolateDTProject(Object[] annot) { // mixed case is possible only with quantifiers assert diseqInfo.isMixed(partition); final Term sharedTerm = goalEq.getParameters()[1]; - interpolants[partition] = mTheory.term(Interpolator.EQ, diseqInfo.getMixedVar(), sharedTerm); + interpolants[partition] = diseqInfo.buildEQ(sharedTerm); } } return interpolants; } assert (mEqualityInfos.size() == 1); if (mDisequalityInfos.size() == 0) { - final LitInfo eqInfo = mDisequalityInfos.values().iterator().next(); + final LitInfo eqInfo = mEqualityInfos.values().iterator().next(); for (int partition = 0; partition < mNumInterpolants; partition++) { if (eqInfo.isAorShared(partition)) { interpolants[partition] = mTheory.mFalse; @@ -392,8 +418,9 @@ private Term[] interpolateDTProject(Object[] annot) { } assert (mDisequalityInfos.size() == 1); - final SymmetricPair diseqLit = mDisequalityInfos.keySet().iterator().next(); - final LitInfo diseqInfo = mDisequalityInfos.get(diseqLit); + final Entry, LitInfo> diseqEntry = mDisequalityInfos.entrySet().iterator().next(); + final OffsetLitInfo diseqInfo = getAnnotationLitInfo(diseqEntry.getKey(), diseqEntry.getValue(), + goalEq.getParameters()[0], goalEq.getParameters()[1]); final SymmetricPair eqLit = mEqualityInfos.keySet().iterator().next(); final LitInfo eqInfo = mEqualityInfos.get(eqLit); @@ -415,7 +442,7 @@ private Term[] interpolateDTProject(Object[] annot) { shared1Term = goalEq.getParameters()[1]; } if (diseqInfo.isMixed(partition)) { - interpolants[partition] = mTheory.term(Interpolator.EQ, diseqInfo.getMixedVar(), shared1Term); + interpolants[partition] = diseqInfo.buildEQ(shared1Term); } else if (diseqInfo.isALocal(partition)) { interpolants[partition] = mTheory.term(SMTLIBConstants.NOT, mTheory.term(SMTLIBConstants.EQUALS, shared0Term, goalEq.getParameters()[1])); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/OffsetLitInfo.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/OffsetLitInfo.java new file mode 100644 index 000000000..908a555b1 --- /dev/null +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/interpolate/OffsetLitInfo.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2009-2026 University of Freiburg + * + * This file is part of SMTInterpol. + * + * SMTInterpol is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SMTInterpol is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SMTInterpol. If not, see . + */ +package de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate; + +import de.uni_freiburg.informatik.ultimate.logic.Rational; +import de.uni_freiburg.informatik.ultimate.logic.Term; +import de.uni_freiburg.informatik.ultimate.logic.TermVariable; +import de.uni_freiburg.informatik.ultimate.logic.Theory; +import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.LitInfo; +import de.uni_freiburg.informatik.ultimate.smtinterpol.interpolate.Interpolator.Occurrence; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.OffsetEqKey; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.Polynomial; + +/** + * The {@link LitInfo} of an (offset) equality literal together with the offset shift and orientation between the + * literal and the concrete instance in which it is used, e.g. a path step in a CC lemma. If the literal is + * {@code l = r}, the instance is {@code l + shift = r + shift} (with sides possibly swapped). The mixed variable of + * the literal names the shared value of {@code l} and {@code r}; at instance level it therefore stands for + * {@code mixedVar + shift}, see {@link #getMixedBoundary()}. Conversely, an {@code EQ} placeholder equating the + * mixed variable with an instance-level shared term must subtract the shift from that term, see + * {@link #buildEQ(Term)}. + */ +public class OffsetLitInfo { + private final Theory mTheory; + private final OffsetEqKey mLitKey; + private final LitInfo mLitInfo; + private final Rational mShift; + private final boolean mSwapped; + + /** + * Create the info for an equality literal itself, i.e. with zero shift and unswapped sides. + * + * @param theory the SMT theory. + * @param litInfo the occurrence info of the literal. + * @param litKey the key built from the literal's own lhs and rhs. + */ + public OffsetLitInfo(final Theory theory, final LitInfo litInfo, final OffsetEqKey litKey) { + this(theory, litInfo, litKey, Rational.ZERO, false); + } + + private OffsetLitInfo(final Theory theory, final LitInfo litInfo, final OffsetEqKey litKey, final Rational shift, + final boolean swapped) { + mTheory = theory; + mLitInfo = litInfo; + mLitKey = litKey; + mShift = shift; + mSwapped = swapped; + } + + /** + * Derive the info for a concrete instance of the literal, computing the shift and orientation of the instance + * relative to the literal. + * + * @param instanceKey the key built from the instance's left and right side; must be {@code equals} to the + * literal's key. + */ + public OffsetLitInfo reorient(final OffsetEqKey instanceKey) { + final Rational shift = instanceKey.getShift(mLitKey); + final boolean swapped = instanceKey.isSwapped(mLitKey); + if (shift.equals(mShift) && swapped == mSwapped) { + return this; + } + return new OffsetLitInfo(mTheory, mLitInfo, mLitKey, shift, swapped); + } + + public LitInfo getLitInfo() { + return mLitInfo; + } + + public TermVariable getMixedVar() { + return mLitInfo.getMixedVar(); + } + + public Rational getShift() { + return mShift; + } + + public boolean isALocal(final int partition) { + return mLitInfo.isALocal(partition); + } + + public boolean isBLocal(final int partition) { + return mLitInfo.isBLocal(partition); + } + + public boolean isAorShared(final int partition) { + return mLitInfo.isAorShared(partition); + } + + public boolean isBorShared(final int partition) { + return mLitInfo.isBorShared(partition); + } + + public boolean isMixed(final int partition) { + return mLitInfo.isMixed(partition); + } + + /** + * Check whether the left side of the instance is A-local. This is the literal's lhs occurrence corrected for + * the instance's orientation. Only meaningful for partitions where the literal is mixed. + */ + public boolean isLeftALocal(final int partition) { + final Occurrence lhsOccur = mLitInfo.getLhsOccur(); + return mSwapped ? lhsOccur.isBLocal(partition) : lhsOccur.isALocal(partition); + } + + /** + * Check whether the left side of the instance is B-local. This is the literal's lhs occurrence corrected for + * the instance's orientation. Only meaningful for partitions where the literal is mixed. + */ + public boolean isLeftBLocal(final int partition) { + final Occurrence lhsOccur = mLitInfo.getLhsOccur(); + return mSwapped ? lhsOccur.isALocal(partition) : lhsOccur.isBLocal(partition); + } + + /** + * The mixed variable lifted to instance level, i.e. {@code mixedVar + shift}. This is the shared boundary term + * for the instance of the mixed literal. The result is in normal form, since the mixed variable is a plain + * term variable and the constant comes last. + */ + public Term getMixedBoundary() { + final TermVariable mixedVar = mLitInfo.getMixedVar(); + if (mShift.equals(Rational.ZERO)) { + return mixedVar; + } + return mTheory.term("+", mixedVar, mShift.toTerm(mixedVar.getSort())); + } + + /** + * Build the placeholder {@code EQ(mixedVar, sharedTerm - shift)}. The shared term is given at instance level; + * subtracting the shift moves it to the literal level at which the mixed variable lives. The subtraction is + * normalized, as the shared term may itself be a sum or a constant. + * + * @param sharedTerm the instance-level shared term the mixed variable should be equated with. + */ + public Term buildEQ(final Term sharedTerm) { + Term shifted = sharedTerm; + if (!mShift.equals(Rational.ZERO)) { + final Polynomial poly = new Polynomial(sharedTerm); + poly.add(mShift.negate()); + shifted = poly.toTerm(sharedTerm.getSort()); + } + return mTheory.term(Interpolator.EQ, mLitInfo.getMixedVar(), shifted); + } +} diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/model/ModelProver.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/model/ModelProver.java index f18f2a1bc..9ef90a6c6 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/model/ModelProver.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/model/ModelProver.java @@ -948,7 +948,7 @@ private Term interpret(Term origTerm, final FunctionSymbol fs, final Term[] args } case SMTLIBConstants.CONST: - return theory.term(fs, args[0]); + return annotateProof(mProofRules.refl(funcTerm), funcTerm); case SMTLIBConstants.SELECT: { // we assume that the array parameter is a term of the form diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/proof/ProofSimplifier.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/proof/ProofSimplifier.java index 2f717f64d..d63f554f4 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/proof/ProofSimplifier.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/proof/ProofSimplifier.java @@ -56,8 +56,8 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.resolute.ProofLiteral; import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.resolute.ProofRules; import de.uni_freiburg.informatik.ultimate.smtinterpol.smtlib2.SMTInterpol; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.OffsetEqKey; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.Polynomial; -import de.uni_freiburg.informatik.ultimate.smtinterpol.util.SymmetricPair; /** * This class explains an SMTInterpol proof with oracles using the low-level @@ -2106,29 +2106,35 @@ private Term convertRewriteDistinct(final String rewriteRule, final Term rewrite switch (rewriteRule) { case ":distinctBool": assert args.length > 2 && args[0].getSort().getName() == "Bool" && isApplication("false", rhs); - final Term eq01 = theory.term("=", args[0], args[1]); - final Term eq02 = theory.term("=", args[0], args[2]); - final Term eq12 = theory.term("=", args[1], args[2]); - final Term proof01 = mProofRules.distinctElim(0, 1, lhs); - final Term proof02 = mProofRules.distinctElim(0, 2, lhs); - final Term proof12 = mProofRules.distinctElim(1, 2, lhs); - // Prove contradiction using the three equalities eq01, eq02, eq12. - // Do case distinction over three boolean values and show that in each case one - // equality needs to hold. - Term proof = mProofRules.resolutionRule(args[0], - mProofRules.resolutionRule(args[1], mProofRules.iffIntro1(eq01), - mProofRules.resolutionRule(args[2], mProofRules.iffIntro1(eq02), - mProofRules.iffIntro2(eq12))), - mProofRules.resolutionRule(args[1], mProofRules.resolutionRule(args[2], mProofRules.iffIntro1(eq12), - mProofRules.iffIntro2(eq02)), mProofRules.iffIntro2(eq01))); - // Now use the fact that one of the equalities is false, to prove that distinct - // is false. - proof = mProofRules.resolutionRule(eq01, - mProofRules.resolutionRule(eq02, mProofRules.resolutionRule(eq12, proof, proof12), proof02), - proof01); - proof = proveIffFalse(rewrite, proof); - return proof; + // check that the first three args are different. Otherwise use distinctSame + // proof. + if (args[0] != args[1] && args[0] != args[2] && args[1] != args[2]) { + final Term eq01 = theory.term("=", args[0], args[1]); + final Term eq02 = theory.term("=", args[0], args[2]); + final Term eq12 = theory.term("=", args[1], args[2]); + final Term proof01 = mProofRules.distinctElim(0, 1, lhs); + final Term proof02 = mProofRules.distinctElim(0, 2, lhs); + final Term proof12 = mProofRules.distinctElim(1, 2, lhs); + // Prove contradiction using the three equalities eq01, eq02, eq12. + // Do case distinction over three boolean values and show that in each case one + // equality needs to hold. + Term proof = mProofRules.resolutionRule(args[0], + mProofRules.resolutionRule(args[1], mProofRules.iffIntro1(eq01), + mProofRules.resolutionRule(args[2], mProofRules.iffIntro1(eq02), + mProofRules.iffIntro2(eq12))), + mProofRules.resolutionRule(args[1], mProofRules.resolutionRule(args[2], + mProofRules.iffIntro1(eq12), mProofRules.iffIntro2(eq02)), + mProofRules.iffIntro2(eq01))); + // Now use the fact that one of the equalities is false, to prove that distinct + // is false. + proof = mProofRules.resolutionRule(eq01, + mProofRules.resolutionRule(eq02, mProofRules.resolutionRule(eq12, proof, proof12), proof02), + proof01); + proof = proveIffFalse(rewrite, proof); + return proof; + } + /* fall through into distinctSame case */ case ":distinctSame": { // (distinct ... x ... x ...) = false assert isApplication("false", rhs); @@ -3116,20 +3122,17 @@ private Term convertEQLemma(final ProofLiteral[] clause) { * @param equalities HashMap to store equalities (negated in the clause). * @param disequalities HashMap to store disequalities (positive in the clause). */ - private void collectEqualities(final ProofLiteral[] clause, final HashMap, Term> equalities, - final HashMap, Term> disequalities) { + private void collectEqualities(final ProofLiteral[] clause, final HashMap equalities, + final HashMap disequalities) { for (final ProofLiteral literal : clause) { final Term atom = literal.getAtom(); assert isApplication("=", atom); final Term[] sides = ((ApplicationTerm) atom).getParameters(); - assert sides.length == 2; - if (literal.getPolarity()) { - // positive in clause -> disequality in conflict - disequalities.put(new SymmetricPair<>(sides[0], sides[1]), atom); - } else { - // negated atom in clause -> equality in conflict - equalities.put(new SymmetricPair<>(sides[0], sides[1]), atom); - } + final OffsetEqKey key = new OffsetEqKey(sides[0], sides[1]); + + // positive in clause -> disequality in conflict -> collect into disequalities + // negative in clause -> equality in conflict -> collect into equalities + (literal.getPolarity() ? disequalities : equalities).put(key, atom); } } @@ -3146,8 +3149,8 @@ private Term convertCCLemma(final ProofLiteral[] clause, String lemmaType, final final Theory theory = mainPath[0].getTheory(); /* collect literals and search for the disequality */ - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); assert allDisequalities.size() <= 1; @@ -3200,7 +3203,7 @@ private Term convertCCLemma(final ProofLiteral[] clause, String lemmaType, final * neededEqualities. */ private Term proveSelectConst(final Term value, final Term array, final Term weakIdx, - final Set> allEqualities, final Set neededEqualities) { + final Map allEqualities, final Set neededEqualities) { final Theory theory = value.getTheory(); // Check if value is (select array idx2) with (weakIdx = idx2) in equalities or // syntactically equal. @@ -3210,7 +3213,7 @@ private Term proveSelectConst(final Term value, final Term array, final Term wea if (args[1] == weakIdx) { return mProofRules.refl(value); } - if (allEqualities.contains(new SymmetricPair<>(weakIdx, args[1]))) { + if (allEqualities.containsKey(new OffsetEqKey(weakIdx, args[1]))) { neededEqualities.add(theory.term(SMTLIBConstants.EQUALS, array, array)); neededEqualities.add(theory.term(SMTLIBConstants.EQUALS, weakIdx, args[1])); return mProofRules.cong(theory.term(SMTLIBConstants.SELECT, array, weakIdx), value); @@ -3272,69 +3275,6 @@ private Term proveSelectPathTrans(final Term arrayLeft, final Term value1, final return proof; } - /** - * Prove for a step in a weak array path that - * {@code (select arrayLeft weakIdx) = (select arrayRight weakIdx)}, for the - * case that there is an explicit select equality (or the edge-case where this - * explicit select equality would be trivial. A select equality is an equality - * of the form {@code (select arrayLeft idx1) = (select arrayRight idx2)}, where - * an equality between weakIdx and idx1 resp. idx2 is either trivial or in the - * equalities set. In case arrayLeft is the term {@pre (const v)} the left-hand - * side of the equality can be simply {@pre v}, similarly for arrayRight. - * - * @param arrayLeft the left array of the step. - * @param arrayRight the right array of the step. - * @param weakIdx the weak path index. - * @param equalities the equality literals from the clause. - * @param neededEqualities a set into which needed equalities are added. - * @return the proof for the equality between the two selects. The proof uses - * the equality between the select index in the equality and weakIndex, - * which it adds to neededEqualities. It returns null if this is not a - * store step. - */ - private Term proveSelectPath(final Term arrayLeft, final Term arrayRight, final Term weakIdx, - final Set> allEqualities, final Set neededEqualities) { - for (final SymmetricPair candidateEquality : allEqualities) { - // Check for each candidate equality if it explains a select edge for a - // weakeq-ext lemma. - // We check if termPair.first[weakIdx]] equals one side of the equality and - // termPair.second[weakIdx] - // equals the other side. - final Term first = candidateEquality.getFirst(); - final Term second = candidateEquality.getSecond(); - Term eq1 = proveSelectConst(first, arrayLeft, weakIdx, allEqualities, neededEqualities); - Term eq2 = proveSelectConst(second, arrayRight, weakIdx, allEqualities, neededEqualities); - if (eq1 != null && eq2 != null) { - return proveSelectPathTrans(arrayLeft, first, second, arrayRight, weakIdx, eq1, eq2, neededEqualities); - } - eq1 = proveSelectConst(second, arrayLeft, weakIdx, allEqualities, neededEqualities); - eq2 = proveSelectConst(first, arrayRight, weakIdx, allEqualities, neededEqualities); - if (eq1 != null && eq2 != null) { - return proveSelectPathTrans(arrayLeft, second, first, arrayRight, weakIdx, eq1, eq2, neededEqualities); - } - } - // No candidate equality was found but it could also be a select-const edge - // where a[i] and v are - // syntactically equal, in which case there is no equality. - if (isApplication(SMTLIBConstants.CONST, arrayLeft)) { - final Term value = ((ApplicationTerm) arrayLeft).getParameters()[0]; - final Term eq2 = proveSelectConst(value, arrayRight, weakIdx, allEqualities, neededEqualities); - if (eq2 != null) { - return proveSelectPathTrans(arrayLeft, value, value, arrayRight, weakIdx, - mProofRules.constArray(value, weakIdx), eq2, neededEqualities); - } - } - if (isApplication(SMTLIBConstants.CONST, arrayRight)) { - final Term value = ((ApplicationTerm) arrayRight).getParameters()[0]; - final Term eq1 = proveSelectConst(value, arrayLeft, weakIdx, allEqualities, neededEqualities); - if (eq1 != null) { - return proveSelectPathTrans(arrayLeft, value, value, arrayRight, weakIdx, eq1, - mProofRules.constArray(value, weakIdx), neededEqualities); - } - } - return null; - } - /** * Try to prove for a step in a weak array path that * {@code (select arrayLeft weakIdx) = (select arrayRight weakIdx)}, for the @@ -3352,13 +3292,13 @@ private Term proveSelectPath(final Term arrayLeft, final Term arrayRight, final * to neededDisequalities. It returns null if this is not a store step. */ private Term proveStoreStep(final Term arrayLeft, final Term arrayRight, final Term weakIdx, - final Set> disequalities, final Set neededDisequalities) { + final Map disequalities, final Set neededDisequalities) { if (isApplication("store", arrayLeft)) { final Term[] storeArgs = ((ApplicationTerm) arrayLeft).getParameters(); if (storeArgs[0] == arrayRight) { // this is a step from a to (store a storeIndex v). Check if storeIndex is okay. final Term storeIdx = ((ApplicationTerm) arrayLeft).getParameters()[1]; - if (disequalities.contains(new SymmetricPair<>(weakIdx, storeIdx))) { + if (disequalities.containsKey(new OffsetEqKey(weakIdx, storeIdx))) { final Term storeVal = ((ApplicationTerm) arrayLeft).getParameters()[2]; final Theory theory = arrayLeft.getTheory(); neededDisequalities.add(theory.term(SMTLIBConstants.EQUALS, storeIdx, weakIdx)); @@ -3391,12 +3331,12 @@ private Term proveStoreStep(final Term arrayLeft, final Term arrayRight, final T * which case they are added to the needed(Dis)Equalities set. */ private Term proveSelectOverPathStep(final Term arrayLeft, final Term arrayRight, final Term weakIdx, - final Term selectLeft, final Term selectRight, final Set> equalities, - final Set> disequalities, final Set neededEqualities, - final Set neededDisequalities) { + final Term selectLeft, final Term selectRight, final Map equalities, + final Map disequalities, final Set neededEqualities, + final Set neededDisequalities, final Term[] selectEdge) { final Theory theory = arrayLeft.getTheory(); /* check for strong path first */ - if (equalities.contains(new SymmetricPair<>(arrayLeft, arrayRight))) { + if (equalities.containsKey(new OffsetEqKey(arrayLeft, arrayRight))) { neededEqualities.add(theory.term(SMTLIBConstants.EQUALS, arrayLeft, arrayRight)); neededEqualities.add(theory.term(SMTLIBConstants.EQUALS, weakIdx, weakIdx)); return mProofRules.cong(selectLeft, selectRight); @@ -3412,10 +3352,17 @@ private Term proveSelectOverPathStep(final Term arrayLeft, final Term arrayRight mProofRules.symm(selectLeft, selectRight)); } /* - * check for select path with select indices equal to weakIdx, both trivially - * equal and proven equal by a strong path + * This is a select/const edge. Prefer the select/const values recorded in the annotation (see + * WeakCongruencePath.WeakSubPath.setSelectEdge): they are the bare select or the full const value, so + * proveSelectConst matches them directly and the offset-rendered clause equality is bridged by + * resolveNeededEqualities. Only fall back to searching the clause equalities if no edge is annotated or it does + * not match this step. proveSelectPathTrans may legitimately return null (a trivial step), so the match is + * decided by proveSelectConst succeeding on both sides, not by the returned proof. */ - return proveSelectPath(arrayLeft, arrayRight, weakIdx, equalities, neededEqualities); + final Term eq1 = proveSelectConst(selectEdge[0], arrayLeft, weakIdx, equalities, neededEqualities); + final Term eq2 = proveSelectConst(selectEdge[1], arrayRight, weakIdx, equalities, neededEqualities); + return proveSelectPathTrans(arrayLeft, selectEdge[0], selectEdge[1], arrayRight, weakIdx, eq1, eq2, + neededEqualities); } /** @@ -3436,9 +3383,9 @@ private Term proveSelectOverPathStep(final Term arrayLeft, final Term arrayRight * some trivial (dis)equalities or some from (dis)equalities set, in * which case they are added to the needed(Dis)Equalities set. */ - private Term proveSelectOverPath(final Term weakIdx, final Term[] path, final Set> equalities, - final Set> disequalities, final Set neededEqualities, - final Set neededDisequalities) { + private Term proveSelectOverPath(final Term weakIdx, final Term[] path, final Map equalities, + final Map disequalities, final Set neededEqualities, + final Set neededDisequalities, final Term[] selectEdge) { // note that a read-const-weakeq path can have length 1 assert path.length >= 1; final Theory theory = path[0].getTheory(); @@ -3452,7 +3399,7 @@ private Term proveSelectOverPath(final Term weakIdx, final Term[] path, final Se Term proof = selectChain.length > 2 ? mProofRules.trans(selectChain) : null; for (int i = 0; i < path.length - 1; i++) { final Term subproof = proveSelectOverPathStep(path[i], path[i + 1], weakIdx, selectChain[i], - selectChain[i + 1], equalities, disequalities, neededEqualities, neededDisequalities); + selectChain[i + 1], equalities, disequalities, neededEqualities, neededDisequalities, selectEdge); proof = res(theory.term(SMTLIBConstants.EQUALS, selectChain[i], selectChain[i + 1]), subproof, proof); } return proof; @@ -3473,9 +3420,9 @@ private Term convertArraySelectConstWeakEqLemma(final ProofLiteral[] clause, fin * weak path was proven for this pair. strongPaths contains the sets of all * proven strong paths. */ - final HashMap, Term> allEqualities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); /* indexDiseqs contains all index equalities in the clause */ - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3497,8 +3444,8 @@ private Term convertArraySelectConstWeakEqLemma(final ProofLiteral[] clause, fin final Term mainIdx = (Term) weakItems[0]; final Term[] mainPath = (Term[]) weakItems[1]; - Term proof = proveSelectOverPath(mainIdx, mainPath, allEqualities.keySet(), allDisequalities.keySet(), - neededEqualities, neededDisequalities); + Term proof = proveSelectOverPath(mainIdx, mainPath, allEqualities, allDisequalities, neededEqualities, + neededDisequalities, null); final Term firstTerm = theory.term("select", mainPath[0], mainIdx); final Term lastTerm = theory.term("select", mainPath[mainPath.length - 1], mainIdx); assert isApplication("const", mainPath[mainPath.length - 1]); @@ -3527,9 +3474,9 @@ private Term convertArraySelectWeakEqLemma(final ProofLiteral[] clause, final Ob * weak path was proven for this pair. strongPaths contains the sets of all * proven strong paths. */ - final HashMap, Term> allEqualities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); /* indexDiseqs contains all index equalities in the clause */ - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3551,8 +3498,8 @@ private Term convertArraySelectWeakEqLemma(final ProofLiteral[] clause, final Ob final Term mainIdx = (Term) weakItems[0]; final Term[] mainPath = (Term[]) weakItems[1]; - Term proof = proveSelectOverPath(mainIdx, mainPath, allEqualities.keySet(), allDisequalities.keySet(), - neededEqualities, neededDisequalities); + Term proof = proveSelectOverPath(mainIdx, mainPath, allEqualities, allDisequalities, + neededEqualities, neededDisequalities, null); assert isApplication("select", goalTerms[0]) && isApplication("select", goalTerms[1]); final int goalOrder = ((ApplicationTerm) goalTerms[0]).getParameters()[0] == mainPath[0] ? 0 : 1; final Term goal1 = goalTerms[goalOrder]; @@ -3593,9 +3540,9 @@ private Term convertArrayWeakEqExtLemma(final ProofLiteral[] clause, final Objec * weak path was proven for this pair. strongPaths contains the sets of all * proven strong paths. */ - final HashMap, Term> allEqualities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); /* indexDiseqs contains all index equalities in the clause */ - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3624,14 +3571,14 @@ private Term convertArrayWeakEqExtLemma(final ProofLiteral[] clause, final Objec final Term selectLeftDiff = mainSelectChain[0]; final Term selectRightDiff = mainSelectChain[mainPath.length - 1]; - final HashSet> weakDisequalities = new HashSet<>(); + final HashMap weakDisequalities = new HashMap<>(); final HashSet neededWeakDisequalities = new HashSet<>(); /* Collect weak paths */ for (int i = 3; i < ccAnnotation.length; i += 2) { assert ccAnnotation[i] == ":weakpath"; final Object[] weakItems = (Object[]) ccAnnotation[i + 1]; final Term idx = (Term) weakItems[0]; - weakDisequalities.add(new SymmetricPair<>(idx, diffTerm)); + weakDisequalities.put(new OffsetEqKey(idx, diffTerm), theory.term(SMTLIBConstants.EQUALS, idx, diffTerm)); } /* @@ -3640,7 +3587,7 @@ private Term convertArrayWeakEqExtLemma(final ProofLiteral[] clause, final Objec Term mainChainProof = mainPath.length > 2 ? mProofRules.trans(mainSelectChain) : null; for (int i = 0; i < mainPath.length - 1; i++) { Term proofSelectEq; - final SymmetricPair pair = new SymmetricPair<>(mainPath[i], mainPath[i + 1]); + final OffsetEqKey pair = new OffsetEqKey(mainPath[i], mainPath[i + 1]); /* check for strong path first */ if (allEqualities.containsKey(pair)) { neededEqualities.add(theory.term(SMTLIBConstants.EQUALS, mainPath[i], mainPath[i + 1])); @@ -3666,6 +3613,15 @@ private Term convertArrayWeakEqExtLemma(final ProofLiteral[] clause, final Objec final Object[] weakItems = (Object[]) ccAnnotation[i + 1]; final Term idx = (Term) weakItems[0]; final Term[] weakPath = (Term[]) weakItems[1]; + // The optional third element is the select/const edge {left, right} justifying this weak path's + // weak-congruence step; left is on the weakPath[0] side (see CCProofGenerator.buildLemma). + final Term[] selectEdge; + if (weakItems.length > 2) { + final Object[] edge = (Object[]) weakItems[2]; + selectEdge = new Term[] { (Term) edge[0], (Term) edge[1] }; + } else { + selectEdge = null; + } /* check end points */ assert arrayLeft == weakPath[0] && arrayRight == weakPath[weakPath.length - 1]; @@ -3675,8 +3631,8 @@ private Term convertArrayWeakEqExtLemma(final ProofLiteral[] clause, final Objec final Term selectLeftIdx = theory.term(SMTLIBConstants.SELECT, arrayLeft, idx); final Term selectRightIdx = theory.term(SMTLIBConstants.SELECT, arrayRight, idx); - Term subproof = proveSelectOverPath(idx, weakPath, allEqualities.keySet(), allDisequalities.keySet(), - neededEqualities, neededDisequalities); + Term subproof = proveSelectOverPath(idx, weakPath, allEqualities, allDisequalities, + neededEqualities, neededDisequalities, selectEdge); subproof = res(theory.term(SMTLIBConstants.EQUALS, selectLeftIdx, selectRightIdx), subproof, mProofRules.trans(selectLeftDiff, selectLeftIdx, selectRightIdx, selectRightDiff)); subproof = res(theory.term(SMTLIBConstants.EQUALS, selectLeftDiff, selectLeftIdx), @@ -3715,8 +3671,8 @@ private Term convertDTProject(final ProofLiteral[] clause, final Object[] ccAnno assert ccAnnotation.length == 3; final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3767,8 +3723,8 @@ private Term convertDTProject(final ProofLiteral[] clause, final Object[] ccAnno private Term convertDTTester(final ProofLiteral[] clause, final Object[] ccAnnotation) { assert ccAnnotation.length == 3; final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3828,8 +3784,8 @@ private Term convertDTConstructor(final ProofLiteral[] clause, final Object[] cc assert ccAnnotation.length == 1; final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3868,8 +3824,8 @@ private Term convertDTConstructor(final ProofLiteral[] clause, final Object[] cc */ private Term convertDTCases(final ProofLiteral[] clause, final Object[] ccAnnotation) { final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3913,8 +3869,8 @@ private Term convertDTCases(final ProofLiteral[] clause, final Object[] ccAnnota */ private Term convertDTUnique(final ProofLiteral[] clause, final Object[] ccAnnotation) { final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -3969,8 +3925,8 @@ private Term convertDTUnique(final ProofLiteral[] clause, final Object[] ccAnnot */ private Term convertDTInjective(final ProofLiteral[] clause, final Object[] ccAnnotation) { final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -4028,8 +3984,8 @@ private Term convertDTInjective(final ProofLiteral[] clause, final Object[] ccAn */ private Term convertDTDisjoint(final ProofLiteral[] clause, final Object[] ccAnnotation) { final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -4091,8 +4047,8 @@ private int checkAndFindConsArg(final Term consTerm, final Term argTerm) { */ private Term convertDTCycle(final ProofLiteral[] clause, final Object[] ccAnnotation) { final Theory theory = clause[0].getAtom().getTheory(); - final HashMap, Term> allEqualities = new HashMap<>(); - final HashMap, Term> allDisequalities = new HashMap<>(); + final HashMap allEqualities = new HashMap<>(); + final HashMap allDisequalities = new HashMap<>(); collectEqualities(clause, allEqualities, allDisequalities); final HashSet neededEqualities = new HashSet<>(); @@ -4145,34 +4101,42 @@ private Term convertDTCycle(final ProofLiteral[] clause, final Object[] ccAnnota final String[] selectors = c.getSelectors(); for (pos = 0; pos < selectors.length; pos++) { if (selectors[pos].equals(appTerm.getFunction().getName())) { + final Term isConsTerm = theory.term(SMTLIBConstants.IS, new String[] { c.getName() }, + null, consTerm); final Term[] consArgs = new Term[selectors.length]; - final Term[] runningArgs = new Term[selectors.length]; for (int argnr = 0; argnr < consArgs.length; argnr++) { consArgs[argnr] = theory.term(selectors[argnr], consTerm); - if (argnr != pos) { - runningArgs[argnr] = consArgs[argnr]; - neededEqualities.add( - theory.term(SMTLIBConstants.EQUALS, consArgs[argnr], consArgs[argnr])); - } else { - runningArgs[argnr] = runningTerm; - } } final Term newConsTerm = theory.term(c.getName(), null, (c.needsReturnOverload() ? consTerm.getSort() : null), consArgs); - final Term newRunningTerm = theory.term(c.getName(), null, - (c.needsReturnOverload() ? consTerm.getSort() : null), runningArgs); - final Term isConsTerm = theory.term(SMTLIBConstants.IS, new String[] { c.getName() }, - null, consTerm); - proof = res(theory.term(SMTLIBConstants.EQUALS, runningTerm, selectTerm), proof, - mProofRules.cong(newRunningTerm, newConsTerm)); - proof = mProofUtils.proveTransitivity(newRunningTerm, newConsTerm, consTerm, proof, - mProofRules.dtCons(isConsTerm)); + if (runningTerm == selectTerm) { + proof = mProofRules.dtCons(isConsTerm); + runningTerm = newConsTerm; + } else { + final Term[] runningArgs = new Term[selectors.length]; + for (int argnr = 0; argnr < consArgs.length; argnr++) { + if (argnr != pos) { + runningArgs[argnr] = consArgs[argnr]; + neededEqualities.add(theory.term(SMTLIBConstants.EQUALS, consArgs[argnr], + consArgs[argnr])); + } else { + runningArgs[argnr] = runningTerm; + } + } + final Term newRunningTerm = theory.term(c.getName(), null, + (c.needsReturnOverload() ? consTerm.getSort() : null), runningArgs); + proof = res(theory.term(SMTLIBConstants.EQUALS, runningTerm, selectTerm), proof, + mProofRules.cong(newRunningTerm, newConsTerm)); + proof = mProofUtils.proveTransitivity(newRunningTerm, newConsTerm, consTerm, proof, + mProofRules.dtCons(isConsTerm)); + runningTerm = newRunningTerm; + } + final Term isConsEq = theory.term(SMTLIBConstants.EQUALS, isConsTerm, theory.mTrue); proof = res(isConsTerm, res(theory.mTrue, mProofRules.trueIntro(), mProofRules.iffElim1(isConsEq)), proof); neededEqualities.add(isConsEq); - runningTerm = newRunningTerm; argSequence[i / 2] = pos; break findSelector; } @@ -4623,30 +4587,50 @@ private AnnotatedTerm substituteInQuantInst(final Term[] subst, final Quantified * in the proved clause). * @return the modified proof. */ - private Term resolveNeededEqualities(Term proof, final Map, Term> allEqualities, - final Map, Term> allDisequalities, final Set neededEqualities, + private Term resolveNeededEqualities(Term proof, final Map allEqualities, + final Map allDisequalities, final Set neededEqualities, final Set neededDisequalities) { for (final Term eq : neededEqualities) { assert isApplication("=", eq); final Term[] eqParam = ((ApplicationTerm) eq).getParameters(); - final Term clauseEq = allEqualities.get(new SymmetricPair<>(eqParam[0], eqParam[1])); - if (clauseEq != null) { - if (clauseEq != eq) { + final OffsetEqKey eqKey = new OffsetEqKey(eqParam[0], eqParam[1]); + final ApplicationTerm clauseEq = (ApplicationTerm) allEqualities.get(eqKey); + if (clauseEq == null) { + final Term proofEq = mProofUtils.proveTrivialEquality(eqParam[0], eqParam[1]); + proof = res(eq, proofEq, proof); + } else { + final Term[] clauseEqParam = clauseEq.getParameters(); + if (clauseEq == eq) { + // nothing to do + } else if (clauseEqParam[1] == eqParam[0] && clauseEqParam[0] == eqParam[1]) { // need symmetry proof = res(eq, mProofRules.symm(eqParam[0], eqParam[1]), proof); + } else { + // need shifted offset + final OffsetEqKey clauseEqKey = new OffsetEqKey(clauseEqParam[0], clauseEqParam[1]); + final Rational factor = (clauseEqKey.getLhs() == eqKey.getLhs() ? Rational.ONE : Rational.MONE); + final Term bridge = mProofUtils.proveEqWithMultiplier(clauseEqParam, eqParam, factor); + proof = res(eq, bridge, proof); } - } else { - final Term proofEq = mProofUtils.proveTrivialEquality(eqParam[0], eqParam[1]); - proof = res(eq, proofEq, proof); } } for (final Term eq : neededDisequalities) { assert isApplication("=", eq); final Term[] eqParam = ((ApplicationTerm) eq).getParameters(); - final Term clauseEq = allDisequalities.get(new SymmetricPair<>(eqParam[0], eqParam[1])); - if (clauseEq != eq) { + final OffsetEqKey eqKey = new OffsetEqKey(eqParam[0], eqParam[1]); + final ApplicationTerm clauseEq = (ApplicationTerm) allDisequalities.get(eqKey); + final Term[] clauseEqParam = clauseEq.getParameters(); + if (clauseEq == eq) { + // nothing to do + } else if (clauseEqParam[1] == eqParam[0] && clauseEqParam[0] == eqParam[1]) { // need symmetry proof = res(eq, proof, mProofRules.symm(eqParam[1], eqParam[0])); + } else { + // need shifted offset + final OffsetEqKey clauseEqKey = new OffsetEqKey(clauseEqParam[0], clauseEqParam[1]); + final Rational factor = (clauseEqKey.getLhs() == eqKey.getLhs() ? Rational.ONE : Rational.MONE); + final Term bridge = mProofUtils.proveEqWithMultiplier(eqParam, clauseEqParam, factor); + proof = res(eq, proof, bridge); } } return proof; diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/bitvector/BvToIntUtils.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/bitvector/BvToIntUtils.java index de2aa095b..371500707 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/bitvector/BvToIntUtils.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/bitvector/BvToIntUtils.java @@ -89,9 +89,9 @@ public Term normalizeMod(final Term lhs, final Rational maxNumber) { final Polynomial arg0 = new Polynomial(lhs); arg0.mod(maxNumber); final Term div = arg0.isConstant() ? arg0.getConstant().div(maxNumber).floor().toTerm(sort) - : theory.term("div", arg0.toTerm(sort), maxNumber.toTerm(sort)); + : theory.term("div", mPolyUnifier.unifyPolynomial(arg0, sort), maxNumber.toTerm(sort)); arg0.add(maxNumber.negate(), div); - return arg0.toTerm(sort); + return mPolyUnifier.unifyPolynomial(arg0, sort); } private Rational pow2(int exponent) { @@ -164,7 +164,7 @@ public Term translateBvshl(final IProofTracker tracker, final FunctionSymbol fsy final int shiftAsInt = shiftValue.numerator().intValue(); final Polynomial multiply = new Polynomial(); multiply.add(pow2(shiftAsInt), translatedLHS); - transformedAsInt = multiply.toTerm(mInteger); + transformedAsInt = mPolyUnifier.unifyPolynomial(multiply, mInteger); } } else { final int logWidth = log2(width); @@ -175,11 +175,11 @@ public Term translateBvshl(final IProofTracker tracker, final FunctionSymbol fsy final Polynomial compare = new Polynomial(); compare.add(shiftStep); compare.add(Rational.MONE, shift); - final Term cond = theory.term("<=", compare.toTerm(mInteger), zero); + final Term cond = theory.term("<=", mPolyUnifier.unifyPolynomial(compare, mInteger), zero); shift.add(Rational.ONE, theory.term("ite", cond, shiftStep.negate().toTerm(mInteger), zero)); final Polynomial multiply = new Polynomial(); multiply.add(pow2(1 << i), result); - result = theory.term("ite", cond, multiply.toTerm(mInteger), result); + result = theory.term("ite", cond, mPolyUnifier.unifyPolynomial(multiply, mInteger), result); } transformedAsInt = result; } @@ -223,7 +223,7 @@ public Term translateBvshr(final IProofTracker tracker, final FunctionSymbol fsy final Polynomial compare = new Polynomial(); compare.add(shiftStep); compare.add(Rational.MONE, shift); - final Term cond = theory.term("<=", compare.toTerm(mInteger), zero); + final Term cond = theory.term("<=", mPolyUnifier.unifyPolynomial(compare, mInteger), zero); shift.add(Rational.ONE, theory.term("ite", cond, shiftStep.negate().toTerm(mInteger), zero)); final Term divide = theory.term(SMTLIBConstants.DIV, result, pow2(1 << i).toTerm(mInteger)); result = theory.term("ite", cond, divide, result); @@ -318,14 +318,14 @@ private final Term applyMod(final int width, Term integerTerm, boolean isSigned) poly.add(signBit); } poly.mod(maxNumber); - final Term shiftedX = poly.toTerm(mInteger); + final Term shiftedX = mPolyUnifier.unifyPolynomial(poly, mInteger); if (!poly.isConstant()) { poly.add(maxNumber.negate(), theory.term(SMTLIBConstants.DIV, shiftedX, maxNumber.toTerm(mInteger))); } if (isSigned) { poly.add(signBit.negate()); } - return poly.toTerm(mInteger); + return mPolyUnifier.unifyPolynomial(poly, mInteger); } public Term bitBlastAndConstant(final Term lhs, final Rational rhs, int width) { @@ -362,7 +362,7 @@ public Term bitBlastAndConstant(final Term lhs, final Rational rhs, int width) { final Rational powHighRat = Rational.valueOf(powHigh, BigInteger.ONE); result.add(powHighRat.negate(), theory.term(SMTLIBConstants.DIV, lhs, powHighRat.toTerm(mInteger))); } - return result.toTerm(mInteger); + return mPolyUnifier.unifyPolynomial(result, mInteger); } public Term bitBlastAnd(final Term lhs, final Term rhs, int width) { @@ -381,7 +381,7 @@ public Term bitBlastAnd(final Term lhs, final Term rhs, int width) { final Term ite = theory.term("ite", theory.term("=", isel(i, lhs), one), isel(i, rhs), zero); poly.add(pow2(i), ite); } - return poly.toTerm(mInteger); + return mPolyUnifier.unifyPolynomial(poly, mInteger); } public Term translateBvandSum(final IProofTracker tracker, final FunctionSymbol fsym, final Term convertedApp) { @@ -415,7 +415,7 @@ public Term translateBvor(final IProofTracker tracker, final FunctionSymbol fsym final Polynomial poly = new Polynomial(lhs); poly.add(Rational.ONE, rhs); poly.add(Rational.MONE, bitBlastAnd(lhs, rhs, width)); - final Term transformed = int2bv(poly.toTerm(mInteger), bvSort.getIndices()); + final Term transformed = int2bv(mPolyUnifier.unifyPolynomial(poly, mInteger), bvSort.getIndices()); return trackBvRewrite(convertedApp, transformed, ProofConstants.RW_BVBLAST); } @@ -428,7 +428,7 @@ public Term translateBvxor(final IProofTracker tracker, final FunctionSymbol fsy final Polynomial poly = new Polynomial(lhs); poly.add(Rational.ONE, rhs); poly.add(Rational.TWO.negate(), bitBlastAnd(lhs, rhs, width)); - final Term transformed = int2bv(poly.toTerm(mInteger), bvSort.getIndices()); + final Term transformed = int2bv(mPolyUnifier.unifyPolynomial(poly, mInteger), bvSort.getIndices()); return trackBvRewrite(convertedApp, transformed, ProofConstants.RW_BVBLAST); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ArrayTheory.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ArrayTheory.java index 5492e0275..cbf38d8f1 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ArrayTheory.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ArrayTheory.java @@ -160,13 +160,15 @@ class ArrayNode { * classes and to quickly compute new equivalences. We only have to remember one of the select terms for each * index, since the other are supposed to be equal. * - * The key is always the representative of the index of the corresponding select. + * The key is always the value identity (representative and offset-to-representative) of the index of the + * corresponding select, i.e. {@link CCParameter#getValueKey()} of {@link #getIndexParamFromSelect}. Two selects + * on indices with the same value share a key. * * This is an arbitrary map for weak representative (no primary edge), should be a singleton for weak-i * representatives (primary edge exists, but no secondary edge) and empty for all other nodes (where both edges * exist). */ - Map mSelects; + Map mSelects; CCAppTerm mConstTerm; @@ -205,11 +207,11 @@ public ArrayNode getWeakRepresentative() { * the index i for which the weak-i equivalence class representative should be returned. * @return the representative array node. */ - public ArrayNode getWeakIRepresentative(CCTerm index) { - index = index.getRepresentative(); + public ArrayNode getWeakIRepresentative(CCParameter index) { + index = index.getValueKey(); ArrayNode node = this; while (node.mPrimaryEdge != null) { - if (getIndexFromStore(node.mPrimaryStore).getRepresentative() == index) { + if (getIndexFromStore(node.mPrimaryStore).getValueKey().equals(index)) { if (node.mSecondaryEdge == null) { break; } @@ -230,7 +232,7 @@ public ArrayNode getWeakIRepresentative(CCTerm index) { public void makeWeakIRepresentative() { assert mPrimaryEdge != null; assert mSecondaryEdge != null; - final CCTerm index = getIndexFromStore(mPrimaryStore).getRepresentative(); + final CCParameter index = getIndexFromStore(mPrimaryStore).getValueKey(); assert getWeakIRepresentative(index) != getWeakRepresentative(); ArrayNode prev = this; ArrayNode next = prev.mSecondaryEdge; @@ -265,12 +267,12 @@ public void makeWeakRepresentative() { } // hash map from store indices we have seen on the primary path to the previous destination/new source of // the primary edge (or in other words, to the new candidate for weak-i equivalence class). - final LinkedHashMap seenStores = new LinkedHashMap<>(); + final LinkedHashMap seenStores = new LinkedHashMap<>(); // for each index, the information about secondary edge that we later need to invert. - final LinkedHashMap todoSecondary = new LinkedHashMap<>(); - final LinkedHashMap todoSecondaryStores = new LinkedHashMap<>(); + final LinkedHashMap todoSecondary = new LinkedHashMap<>(); + final LinkedHashMap todoSecondaryStores = new LinkedHashMap<>(); // the select information for the new root node. - final LinkedHashMap newSelectMap = new LinkedHashMap<>(); + final LinkedHashMap newSelectMap = new LinkedHashMap<>(); ArrayNode node = this; ArrayNode prev = null; CCAppTerm prevStore = null; @@ -281,7 +283,7 @@ public void makeWeakRepresentative() { node.mPrimaryEdge = prev; node.mPrimaryStore = prevStore; - final CCTerm index = getIndexFromStore(nextStore).getRepresentative(); + final CCParameter index = getIndexFromStore(nextStore).getValueKey(); final ArrayNode old = seenStores.put(index, next); if (old == node) { // the node is in the middle of two consecutive stores on the same @@ -300,7 +302,7 @@ public void makeWeakRepresentative() { // insert the secondary edge of node into old node. The reasoning is that // the old node is connected using only primary edges on different indices, so it has // the same weak-i representative as node. - assert (getIndexFromStore(old.mPrimaryStore).getRepresentative() == index); + assert (getIndexFromStore(old.mPrimaryStore).getValueKey().equals(index)); assert (old.mSecondaryEdge == null); old.mSecondaryEdge = node.mSecondaryEdge; old.mSecondaryStore = node.mSecondaryStore; @@ -328,19 +330,19 @@ public void makeWeakRepresentative() { node.mPrimaryStore = prevStore; mConstTerm = node.mConstTerm; node.mConstTerm = null; - final Map rootSelects = node.mSelects; + final Map rootSelects = node.mSelects; node.mSelects = Collections.emptyMap(); - for (final Entry entry : seenStores.entrySet()) { + for (final Entry entry : seenStores.entrySet()) { // The seen stores get the new representatives of their weak-i equivalence groups - final CCTerm index = entry.getKey(); + final CCParameter index = entry.getKey(); final CCAppTerm select = rootSelects.remove(index); if (select != null) { entry.getValue().mSelects = Collections.singletonMap(index, select); } } // Now invert the secondary edges - for (final Entry entry : todoSecondary.entrySet()) { - final CCTerm index = entry.getKey(); + for (final Entry entry : todoSecondary.entrySet()) { + final CCParameter index = entry.getKey(); ArrayNode dest = entry.getValue(); dest = dest.findSecondaryNode(index); if (dest.mSecondaryEdge != null) { @@ -373,31 +375,32 @@ public void mergeWith(final ArrayNode storeNode, final CCAppTerm store, final Co // merge the consts; // this map collects all selects in the other class, that may need to be merge with the const. - final LinkedHashMap mergeConstSelects = new LinkedHashMap<>(); + final LinkedHashMap mergeConstSelects = new LinkedHashMap<>(); if (mConstTerm != null) { if (storeNode.mConstTerm == null) { storeNode.mConstTerm = mConstTerm; mConstTerm = null; mergeConstSelects.putAll(storeNode.mSelects); } else { - final CCTerm const1 = getValueFromConst(mConstTerm); - final CCTerm const2 = getValueFromConst(storeNode.mConstTerm); - if (const1.getRepresentative() != const2.getRepresentative()) { + final CCParameter const1 = getValueFromConst(mConstTerm); + final CCParameter const2 = getValueFromConst(storeNode.mConstTerm); + if (const1.getRepresentative() != const2.getRepresentative() + || !const1.getOffsetToRep().equals(const2.getOffsetToRep())) { propLemmas.add(new ArrayLemma(RuleKind.CONST_WEAKEQ, const1, const2)); } } } else if (storeNode.mConstTerm != null) { mergeConstSelects.putAll(mSelects); } - mergeConstSelects.remove(getIndexFromStore(store).getRepresentative()); + mergeConstSelects.remove(getIndexFromStore(store).getValueKey()); // merge the selects; - Map newSelects = Collections.emptyMap(); - for (final Entry entry : mSelects.entrySet()) { - final CCTerm index = entry.getKey(); + Map newSelects = Collections.emptyMap(); + for (final Entry entry : mSelects.entrySet()) { + final CCParameter index = entry.getKey(); final CCAppTerm select = entry.getValue(); assert select != null; - if (index == getIndexFromStore(store).getRepresentative()) { + if (index.equals(getIndexFromStore(store).getValueKey())) { newSelects = Collections.singletonMap(index, select); } else { final CCAppTerm otherSelect = storeNode.mSelects.get(index); @@ -406,7 +409,9 @@ public void mergeWith(final ArrayNode storeNode, final CCAppTerm store, final Co storeNode.mSelects.put(index, select); } else { mergeConstSelects.remove(index); - if (select.getRepresentative() != otherSelect.getRepresentative()) { + // Offset-aware: two selects in the same class but at different offsets are NOT already equal — + // the propagated equality is a (false) offset equality that raises the conflict. + if (!select.sameValueAs(otherSelect)) { // add propagated equality propLemmas.add(new ArrayLemma(RuleKind.READ_OVER_WEAKEQ, select, otherSelect)); } @@ -415,16 +420,18 @@ public void mergeWith(final ArrayNode storeNode, final CCAppTerm store, final Co } mSelects = newSelects; if (storeNode.mConstTerm != null) { - final CCTerm const1 = getValueFromConst(storeNode.mConstTerm); + final CCParameter const1 = getValueFromConst(storeNode.mConstTerm); final ArrayNode constNode = mCongRoots.get(storeNode.mConstTerm.getRepresentative()); - for (final Entry entry : mergeConstSelects.entrySet()) { - final CCTerm index = entry.getKey(); + for (final Entry entry : mergeConstSelects.entrySet()) { + final CCParameter index = entry.getKey(); final CCAppTerm select = entry.getValue(); // check if selected array is weakly equal to the const array if (constNode.getWeakIRepresentative(index) == storeNode) { // do not keep select, we will merge it with the constant value storeNode.mSelects.remove(index); - if (select.getRepresentative() != const1.getRepresentative()) { + // Offset-aware: a select in the same class as the const value but at a different offset is NOT + // already equal — propagating the (false) offset equality raises the conflict. + if (!select.sameValueAs(const1)) { propLemmas.add(new ArrayLemma(RuleKind.READ_CONST_WEAKEQ, select, const1)); } } @@ -447,13 +454,13 @@ public void mergeWith(final ArrayNode storeNode, final CCAppTerm store, final Co public void mergeSecondary(final ArrayNode storeNode, final CCAppTerm store, final Collection propEqualities) { assert storeNode.mPrimaryEdge == null; assert mPrimaryEdge != null; - assert getIndexFromStore(mPrimaryStore).getRepresentative() != getIndexFromStore(store).getRepresentative(); + assert !getIndexFromStore(mPrimaryStore).sameValueAs(getIndexFromStore(store)); if (mSecondaryEdge != null) { makeWeakIRepresentative(); } mSecondaryEdge = storeNode; mSecondaryStore = store; - final CCTerm storeIndex = getIndexFromStore(mPrimaryStore).getRepresentative(); + final CCParameter storeIndex = getIndexFromStore(mPrimaryStore).getValueKey(); if (!mSelects.isEmpty()) { final CCAppTerm select = mSelects.get(storeIndex); assert (select != null); @@ -461,7 +468,7 @@ public void mergeSecondary(final ArrayNode storeNode, final CCAppTerm store, fin if (otherSelect == null) { storeNode.mSelects.put(storeIndex, select); } else { - if (select.getRepresentative() != otherSelect.getRepresentative()) { + if (!select.sameValueAs(otherSelect)) { propEqualities.add(new ArrayLemma(RuleKind.READ_OVER_WEAKEQ, select, otherSelect)); } } @@ -474,8 +481,8 @@ public void mergeSecondary(final ArrayNode storeNode, final CCAppTerm store, fin if (constNode.getWeakIRepresentative(storeIndex) == storeNode) { // do not keep select, we will merge it with the constant value final CCAppTerm select = storeNode.mSelects.remove(storeIndex); - final CCTerm const1 = getValueFromConst(storeNode.mConstTerm); - if (select != null && select.getRepresentative() != const1.getRepresentative()) { + final CCParameter const1 = getValueFromConst(storeNode.mConstTerm); + if (select != null && !select.sameValueAs(const1)) { propEqualities.add(new ArrayLemma(RuleKind.READ_CONST_WEAKEQ, select, const1)); } } @@ -489,12 +496,12 @@ public void mergeSecondary(final ArrayNode storeNode, final CCAppTerm store, fin * the index i. * @return the number of secondary edges from this to the weak-i root. */ - public int countSecondaryEdges(final CCTerm index) { - assert index.isRepresentative(); + public int countSecondaryEdges(final CCParameter index) { + assert index.getValueKey().equals(index); int count = 0; ArrayNode node = this; while (node.mPrimaryEdge != null) { - if (getIndexFromStore(node.mPrimaryStore).getRepresentative() == index) { + if (getIndexFromStore(node.mPrimaryStore).getValueKey().equals(index)) { if (node.mSecondaryEdge == null) { break; } @@ -510,10 +517,10 @@ public int countSecondaryEdges(final CCTerm index) { /** * Find the next node on the primary chain where the primary edge uses the given index. */ - public ArrayNode findSecondaryNode(final CCTerm index) { - assert index.isRepresentative(); + public ArrayNode findSecondaryNode(final CCParameter index) { + assert index.getValueKey().equals(index); ArrayNode node = this; - while (node.mPrimaryEdge != null && getIndexFromStore(node.mPrimaryStore).getRepresentative() != index) { + while (node.mPrimaryEdge != null && !getIndexFromStore(node.mPrimaryStore).getValueKey().equals(index)) { node = node.mPrimaryEdge; } return node; @@ -551,10 +558,10 @@ public String toString() { private static class ArrayLemma { RuleKind mRule; - SymmetricPair mPropagatedEq; + SymmetricPair mPropagatedEq; Set mUndecidedLits; - public ArrayLemma(final RuleKind rule, final CCTerm lhs, final CCTerm rhs) { + public ArrayLemma(final RuleKind rule, final CCParameter lhs, final CCParameter rhs) { mRule = rule; mPropagatedEq = new SymmetricPair<>(lhs, rhs); } @@ -563,7 +570,7 @@ public RuleKind getRule() { return mRule; } - public SymmetricPair getEquality() { + public SymmetricPair getEquality() { return mPropagatedEq; } @@ -617,7 +624,7 @@ public String toString() { * The value is the select cc term representative. If there is no select term for that index, the value is the * weak-i representative node. At index null, the map stores the weak representative node. */ - Map> mArrayModels = null; + Map> mArrayModels = null; // =============== STATISTICS =============== private int mNumInstsSelect = 0; @@ -738,7 +745,7 @@ public int checkCompleteness() { } private Clause explainPropagation(final ArrayLemma lemma) { - final SymmetricPair equality = lemma.getEquality(); + final SymmetricPair equality = lemma.getEquality(); final long start = System.nanoTime(); mNumInstsSelect++; final WeakCongruencePath path = new WeakCongruencePath(this); @@ -798,11 +805,11 @@ public void dumpModel(final LogProxy logger) { if (!logger.isInfoEnabled()) { return; } - for (final Entry> entry : mArrayModels.entrySet()) { + for (final Entry> entry : mArrayModels.entrySet()) { final StringBuilder sb = new StringBuilder(); sb.append(entry.getKey().mTerm).append(" = store["); sb.append(entry.getKey().getWeakRepresentative().mTerm); - for (final Entry storeEntry : entry.getValue().entrySet()) { + for (final Entry storeEntry : entry.getValue().entrySet()) { if (storeEntry.getKey() == null) { continue; } @@ -908,8 +915,10 @@ public void fillInModel(final ModelBuilder builder, final List ccArrayTe // constant array or // it's not weakly equivalent to one. if (node.mConstTerm != null) { - final CCTerm value = getValueFromConst(node.mConstTerm).getRepresentative(); - final Term nodeValue = t.term(SMTLIBConstants.CONST, null, arraySort, builder.getModelValue(value)); + // getModelValue shifts the representative's value by the const value's offset; using + // getRepresentative() would drop the offset and build a wrong model value. + final Term nodeValue = t.term(SMTLIBConstants.CONST, null, arraySort, + builder.getModelValue(getValueFromConst(node.mConstTerm))); builder.setModelValue(node.mTerm, nodeValue); } else { Term nodeValue; @@ -917,7 +926,7 @@ public void fillInModel(final ModelBuilder builder, final List ccArrayTe // the array is based on a certain const value, see computeWeakEqExt() final Term valueTerm; if (hasFiniteElemSort(node.mTerm.getFlatTerm().getSort())) { - final CCTerm value = (CCTerm) mArrayModels.get(node).get(null); + final CCParameter value = (CCParameter) mArrayModels.get(node).get(null); valueTerm = builder.getModelValue(value); } else { valueTerm = model.extendFresh(valueSort); @@ -928,9 +937,11 @@ public void fillInModel(final ModelBuilder builder, final List ccArrayTe nodeValue = model.extendFresh(arraySort); } // change all indices to the right select value - for (final Entry indexValuePairs : node.mSelects.entrySet()) { - final CCTerm index = indexValuePairs.getKey(); - final CCTerm value = indexValuePairs.getValue().getRepresentative(); + for (final Entry indexValuePairs : node.mSelects.entrySet()) { + final CCParameter index = indexValuePairs.getKey(); + // value identity (getValueKey), so a select whose value sits at a non-zero offset from its + // representative renders the shifted value, not the representative's. + final CCParameter value = indexValuePairs.getValue().getValueKey(); nodeValue = t.term("store", nodeValue, builder.getModelValue(index), builder.getModelValue(value)); } @@ -943,7 +954,7 @@ public void fillInModel(final ModelBuilder builder, final List ccArrayTe continue; } assert other.mSelects.size() == 1; - final CCTerm index = other.mSelects.keySet().iterator().next(); + final CCParameter index = other.mSelects.keySet().iterator().next(); if (!node.mSelects.containsKey(index)) { // we have another array in the weak-equivalence class that may by accident // store the @@ -979,11 +990,11 @@ public void fillInModel(final ModelBuilder builder, final List ccArrayTe } else { secondParentTerm = null; } - final CCTerm ccIndex = getIndexFromStore(node.mPrimaryStore).getRepresentative(); - final CCTerm ccValue = node.mSelects.get(ccIndex); + final CCParameter ccIndex = getIndexFromStore(node.mPrimaryStore).getValueKey(); + final CCAppTerm ccValue = node.mSelects.get(ccIndex); final Term index = builder.getModelValue(ccIndex); final Term value = secondParentTerm != null ? arraySortInterpretation.getSelect(secondParentTerm, index) - : ccValue == null ? model.extendFresh(valueSort) : builder.getModelValue(ccValue); + : ccValue == null ? model.extendFresh(valueSort) : builder.getModelValue(ccValue.getValueKey()); Term nodeValue = t.term("store", parentTerm, index, value); nodeValue = arraySortInterpretation.normalizeStoreTerm(nodeValue); builder.setModelValue(node.mTerm, nodeValue); @@ -1007,28 +1018,28 @@ public void notifyDiff(final CCAppTerm diff) { mDiffs.add(diff); } - public static boolean isSelectTerm(final CCTerm term) { + public static boolean isSelectTerm(final CCParameter term) { if (term instanceof CCAppTerm) { return ((CCAppTerm) term).getFunctionSymbol().getName().equals(SMTLIBConstants.SELECT); } return false; } - public static boolean isStoreTerm(final CCTerm term) { + public static boolean isStoreTerm(final CCParameter term) { if (term instanceof CCAppTerm) { return ((CCAppTerm) term).getFunctionSymbol().getName().equals(SMTLIBConstants.STORE); } return false; } - public static boolean isConstTerm(final CCTerm term) { + public static boolean isConstTerm(final CCParameter term) { if (term instanceof CCAppTerm) { return ((CCAppTerm) term).getFunctionSymbol().getName().equals(SMTLIBConstants.CONST); } return false; } - public static boolean isDiffTerm(final CCTerm term) { + public static boolean isDiffTerm(final CCParameter term) { if (term instanceof CCAppTerm) { return ((CCAppTerm) term).getFunctionSymbol().getName().equals(SMTInterpolConstants.DIFF); } @@ -1037,42 +1048,54 @@ public static boolean isDiffTerm(final CCTerm term) { public static CCTerm getArrayFromSelect(final CCAppTerm select) { assert isSelectTerm(select); - return select.getArgument(0); + // the array argument is never numeric; the cast asserts it carries no offset + return (CCTerm) select.getArgParam(0); } - public static CCTerm getIndexFromSelect(final CCAppTerm select) { + /** + * The value of the index of a select term as a {@link CCParameter}, i.e. argument 1 together with its structural + * offset. With offset equalities the index of {@code (select a (+ i 5))} is the CCTerm {@code i} plus offset 5. + */ + public static CCParameter getIndexFromSelect(final CCAppTerm select) { assert isSelectTerm(select); - return select.getArgument(1); + return select.getArgParam(1); } public static CCTerm getArrayFromStore(final CCAppTerm store) { assert isStoreTerm(store); - return store.getArgument(0); + // the array argument is never numeric; the cast asserts it carries no offset + return (CCTerm) store.getArgParam(0); } - public static CCTerm getIndexFromStore(final CCAppTerm store) { + /** + * The value of the index of a store term as a {@link CCParameter}, i.e. argument 1 together with its structural + * offset (see {@link #getIndexFromSelect}). + */ + public static CCParameter getIndexFromStore(final CCAppTerm store) { assert isStoreTerm(store); - return store.getArgument(1); + return store.getArgParam(1); } - public static CCTerm getValueFromStore(final CCAppTerm store) { + public static CCParameter getValueFromStore(final CCAppTerm store) { assert isStoreTerm(store); - return store.getArgument(2); + return store.getArgParam(2); } - public static CCTerm getValueFromConst(final CCAppTerm constArr) { + public static CCParameter getValueFromConst(final CCAppTerm constArr) { assert isConstTerm(constArr); - return constArr.getArgument(0); + return constArr.getArgParam(0); } public static CCTerm getLeftFromDiff(final CCAppTerm diff) { assert isDiffTerm(diff); - return diff.getArgument(0); + // diff arguments are arrays, never numeric; the cast asserts they carry no offset + return (CCTerm) diff.getArgParam(0); } public static CCTerm getRightFromDiff(final CCAppTerm diff) { assert isDiffTerm(diff); - return diff.getArgument(1); + // diff arguments are arrays, never numeric; the cast asserts they carry no offset + return (CCTerm) diff.getArgParam(1); } public static Sort getArraySortFromSelect(final CCAppTerm select) { @@ -1085,9 +1108,9 @@ public static Sort getArraySortFromStore(final CCAppTerm store) { return store.getFunctionSymbol().getParameterSorts()[0]; } - CCAppTerm findConst(final CCTerm value) { + CCAppTerm findConst(final CCParameter value) { for (final CCAppTerm constTerm : mConsts) { - if (value == getValueFromConst(constTerm)) { + if (value.equals(getValueFromConst(constTerm))) { return constTerm; } } @@ -1095,18 +1118,18 @@ CCAppTerm findConst(final CCTerm value) { } private void setConst(final CCAppTerm term) { - final CCTerm const1 = getValueFromConst(term); + final CCParameter const1 = getValueFromConst(term); final CCTerm rep = term.getRepresentative(); final ArrayNode node = mCongRoots.get(rep); if (node.mConstTerm != null) { - final CCTerm const2 = getValueFromConst(node.mConstTerm); - if (const1.getRepresentative() != const2.getRepresentative()) { + final CCParameter const2 = getValueFromConst(node.mConstTerm); + if (!const1.sameValueAs(const2)) { mPropClauses.add(new ArrayLemma(RuleKind.CONST_WEAKEQ, const1, const2)); } } else { node.mConstTerm = term; for (final CCAppTerm select : node.mSelects.values()) { - if (select.getRepresentative() != const1.getRepresentative()) { + if (!select.sameValueAs(const1)) { mPropClauses.add(new ArrayLemma(RuleKind.READ_CONST_WEAKEQ, select, const1)); } } @@ -1124,7 +1147,7 @@ private void merge(final CCAppTerm store) { mNumMerges++; if (mLogger.isDebugEnabled()) { mLogger.debug( - "Merge: [" + getIndexFromStore(store).getRepresentative() + "] " + arrayNode + " and " + storeNode); + "Merge: [" + getIndexFromStore(store).getValueKey() + "] " + arrayNode + " and " + storeNode); } arrayNode.makeWeakRepresentative(); @@ -1141,12 +1164,12 @@ private void merge(final CCAppTerm store) { // Otherwise arrayNode would have stayed its representative. // We need to insert appropriate select edges. - final HashSet seenIndices = new HashSet<>(); - final CCTerm storeIndex = getIndexFromStore(store).getRepresentative(); + final HashSet seenIndices = new HashSet<>(); + final CCParameter storeIndex = getIndexFromStore(store).getValueKey(); seenIndices.add(storeIndex); ArrayNode node = arrayNode; while (node.mPrimaryEdge != null) { - final CCTerm index = getIndexFromStore(node.mPrimaryStore).getRepresentative(); + final CCParameter index = getIndexFromStore(node.mPrimaryStore).getValueKey(); /* * add the index to the seen indices and merge weak-i equivalence classes if index was not seen before * and they are not already the same. @@ -1183,7 +1206,7 @@ public Cursor(final CCTerm term, final ArrayNode node) { * @param storeIndices * a map where the store indices are collected to. */ - public void collectOverPrimaries(ArrayNode destNode, final Set storeIndices) { + public void collectOverPrimaries(ArrayNode destNode, final Set storeIndices) { // compute the steps to the common root int steps1 = mNode.countPrimaryEdges(); int steps2 = destNode.countPrimaryEdges(); @@ -1217,7 +1240,7 @@ public void collectOverPrimaries(ArrayNode destNode, final Set storeIndi * @param storeIndices * All store indices are added to this map as side-effect. */ - private void collectOneSecondary(final CCTerm index, final Set storeIndices) { + private void collectOneSecondary(final CCParameter index, final Set storeIndices) { final ArrayNode selectNode = mNode.findSecondaryNode(index); final CCAppTerm store = selectNode.mSecondaryStore; final CCTerm array = getArrayFromStore(store); @@ -1244,7 +1267,7 @@ private void collectOneSecondary(final CCTerm index, final Set storeIndi * @param storeIndices * initially an empty map. All store indices are added to this map as side-effect. */ - public void collect(final CCTerm index, final Cursor dest, final Set storeIndices) { + public void collect(final CCParameter index, final Cursor dest, final Set storeIndices) { int steps1 = mNode.countSecondaryEdges(index); int steps2 = dest.mNode.countSecondaryEdges(index); while (steps1 > steps2) { @@ -1275,33 +1298,44 @@ public void collect(final CCTerm index, final Cursor dest, final Set sto * @param storeIndices * initially an empty map. All store indices are added to this map as side-effect. */ - private void computeStoreIndices(final CCTerm index, final CCTerm array1, final CCTerm array2, final Set storeIndices) { + private void computeStoreIndices(final CCParameter index, final CCTerm array1, final CCTerm array2, final Set storeIndices) { final ArrayNode node1 = mCongRoots.get(array1.getRepresentative()); final ArrayNode node2 = mCongRoots.get(array2.getRepresentative()); final Cursor cursor1 = new Cursor(array1, node1); final Cursor cursor2 = new Cursor(array2, node2); - assert index == index.getRepresentative(); + assert index.getValueKey().equals(index); cursor1.collect(index, cursor2, storeIndices); } + /** + * Create the equality literal expressing that the two index values are equal, i.e. + * {@code value(index) == value(storeIndex)}. With offsets this is the offset equality + * {@code index.ccTerm == storeIndex.ccTerm + (storeIndex.offset - index.offset)}. If both share the same underlying + * CCTerm but differ by a constant the equality is unsatisfiable, so no literal is created (returns {@code null}); the + * disjunct it would contribute is always false and can be dropped from the array lemma. + */ + private CCEquality createIndexEquality(final CCParameter index, final CCParameter storeIndex) { + return getCClosure().createEquality(index, storeIndex, false); + } + private void createPropagatedClauses() { for (final ArrayLemma lemma : mPropClauses) { - final CCTerm lhs = lemma.getEquality().getFirst(); - final CCTerm rhs = lemma.getEquality().getSecond(); - assert lhs.getRepresentative() != rhs.getRepresentative(); + final CCParameter lhs = lemma.getEquality().getFirst(); + final CCParameter rhs = lemma.getEquality().getSecond(); + assert !lhs.sameValueAs(rhs); switch (lemma.getRule()) { case READ_OVER_WEAKEQ: { final CCAppTerm select1 = (CCAppTerm) lhs; final CCAppTerm select2 = (CCAppTerm) rhs; - final CCTerm index1 = getIndexFromSelect(select1); + final CCParameter index1 = getIndexFromSelect(select1); final CCTerm array1 = getArrayFromSelect(select1); final CCTerm array2 = getArrayFromSelect(select2); - final Set storeIndices = new LinkedHashSet<>(); - computeStoreIndices(index1.getRepresentative(), array1, array2, storeIndices); + final Set storeIndices = new LinkedHashSet<>(); + computeStoreIndices(index1.getValueKey(), array1, array2, storeIndices); final Set propClause = new LinkedHashSet<>(); - for (final CCTerm idx : storeIndices) { - assert index1.getRepresentative() != idx.getRepresentative(); - final CCEquality lit = getCClosure().createEquality(index1, idx, false); + for (final CCParameter idx : storeIndices) { + assert !index1.sameValueAs(idx); + final CCEquality lit = createIndexEquality(index1, idx); if (lit != null) { assert lit.getDecideStatus() != lit; if (lit.getDecideStatus() == null) { @@ -1331,15 +1365,15 @@ private void createPropagatedClauses() { } case READ_CONST_WEAKEQ: { final CCAppTerm select1 = (CCAppTerm) lhs; - final CCTerm index1 = getIndexFromSelect(select1); + final CCParameter index1 = getIndexFromSelect(select1); final CCTerm array1 = getArrayFromSelect(select1); final CCTerm array2 = findConst(rhs); - final Set storeIndices = new LinkedHashSet<>(); - computeStoreIndices(index1.getRepresentative(), array1, array2, storeIndices); + final Set storeIndices = new LinkedHashSet<>(); + computeStoreIndices(index1.getValueKey(), array1, array2, storeIndices); final Set propClause = new LinkedHashSet<>(); - for (final CCTerm idx : storeIndices) { - assert index1.getRepresentative() != idx.getRepresentative(); - final CCEquality lit = getCClosure().createEquality(index1, idx, false); + for (final CCParameter idx : storeIndices) { + assert !index1.sameValueAs(idx); + final CCEquality lit = createIndexEquality(index1, idx); if (lit != null) { assert lit.getDecideStatus() != lit; if (lit.getDecideStatus() == null) { @@ -1373,9 +1407,8 @@ private void collectSelects(Sort arraySort) { for (final CCTerm select : mCClosure.getAllFuncApps(selectFsym)) { final CCAppTerm selectApp = (CCAppTerm) select; final CCTerm array = getArrayFromSelect(selectApp); - final CCTerm index = getIndexFromSelect(selectApp); final ArrayNode node = mCongRoots.get(array.getRepresentative()); - node.mSelects.put(index.getRepresentative(), selectApp); + node.mSelects.put(getIndexFromSelect(selectApp).getValueKey(), selectApp); } } @@ -1438,8 +1471,8 @@ private boolean computeWeakeqExt() { * fact mSelects is empty since we removed them). */ mArrayModels = new LinkedHashMap<>(); - final HashMap defaultValue = new HashMap<>(); - final HashMap, ArrayNode>> inverse = new HashMap<>(); + final HashMap defaultValue = new HashMap<>(); + final HashMap, ArrayNode>> inverse = new HashMap<>(); final HashSet> propEqualities = new LinkedHashSet<>(); final ArrayDeque todoQueue = new ArrayDeque<>(mCongRoots.values()); while (!todoQueue.isEmpty()) { @@ -1454,12 +1487,16 @@ private boolean computeWeakeqExt() { } final Sort arraySort = node.mTerm.getFlatTerm().getSort(); todoQueue.removeFirst(); - final HashMap nodeMapping = new LinkedHashMap<>(); + final HashMap nodeMapping = new LinkedHashMap<>(); final ArrayNode weakRep = node.getWeakRepresentative(); if (node == weakRep) { - CCTerm constRep = null; + // The element values stored in the fingerprint are value identities (getValueKey = representative + + // offsetToRep), not bare representatives: two arrays whose elements agree up to a constant offset must + // NOT get the same fingerprint, or weakeq-ext would propagate a spurious array equality (e.g. + // store(c,i,y) vs store(c,i,y+1)). Comparisons against constRep therefore use equals(), not ==. + CCParameter constRep = null; if (weakRep.mConstTerm != null) { - constRep = getValueFromConst(weakRep.mConstTerm).getRepresentative(); + constRep = getValueFromConst(weakRep.mConstTerm).getValueKey(); nodeMapping.put(null, constRep); } else if (hasFiniteIndexSort(arraySort)) { // For finite index sorts we cannot just assume that non weak-equivalent arrays @@ -1482,7 +1519,7 @@ private boolean computeWeakeqExt() { cleanCaches(); return true; } - constRep = weakRep.mSelects.values().iterator().next().getRepresentative(); + constRep = weakRep.mSelects.values().iterator().next().getValueKey(); defaultValue.put(arraySort, constRep); } nodeMapping.put(null, constRep); @@ -1498,22 +1535,23 @@ private boolean computeWeakeqExt() { } else { nodeMapping.put(null, node); } - for (final Entry e : node.mSelects.entrySet()) { - final CCTerm value = e.getValue().getRepresentative(); - if (value != constRep) { + for (final Entry e : node.mSelects.entrySet()) { + final CCParameter value = e.getValue().getValueKey(); + if (!value.equals(constRep)) { nodeMapping.put(e.getKey(), value); } } } else { - final CCTerm storeIndex = getIndexFromStore(node.mPrimaryStore).getRepresentative(); + final CCParameter storeIndex = getIndexFromStore(node.mPrimaryStore).getValueKey(); nodeMapping.putAll(mArrayModels.get(node.mPrimaryEdge)); final Object constRep = nodeMapping.get(null); nodeMapping.remove(storeIndex); final ArrayNode weakiRep = node.getWeakIRepresentative(storeIndex); - final CCTerm value = weakiRep.mSelects.get(storeIndex); - if (value != null) { // NOPMD - if (value.getRepresentative() != constRep) { - nodeMapping.put(storeIndex, value.getRepresentative()); + final CCAppTerm valueSelect = weakiRep.mSelects.get(storeIndex); + if (valueSelect != null) { // NOPMD + final CCParameter value = valueSelect.getValueKey(); + if (!value.equals(constRep)) { + nodeMapping.put(storeIndex, value); } } else if (weakiRep != weakRep) { nodeMapping.put(storeIndex, weakiRep); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAnnotation.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAnnotation.java index fa35d0896..798b8d274 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAnnotation.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAnnotation.java @@ -225,63 +225,105 @@ public String getKind() { final RuleKind mRule; /** - * The disequality of the theory lemma. This is the only positive atom in the - * generated theory clause. If this is null, then the first and last element in - * the main paths are distinct terms. + * The disequality of the theory lemma as a pair of {@link CCParameter}s (so a numeric side keeps its offset). This + * is the only positive atom in the generated theory clause. If this is null, then the first and last element in the + * main paths are distinct terms. */ - final SymmetricPair mDiseq; + final SymmetricPair mDiseqParam; /** - * A sequence of paths. The main path with index 0 must always exist and explain - * the diseq. The other paths must be in such an order that later paths explain + * A sequence of paths, as {@link CCParameter}s with offsets (e.g. {@code x+2, y+6, z+8}). The main path with index 0 + * must always exist and explain the diseq. The other paths must be in such an order that later paths explain * congruences on earlier. */ - final CCTerm[][] mPaths; + final CCParameter[][] mParamPaths; - final CCTerm[] mWeakIndices; + final CCParameter[] mWeakIndices; + + /** + * For each path, the select/const edge justifying its single weak-congruence step, or {@code null} if it has none + * (all store/equality steps, or not a weak path). When present, {@code mSelectEdges[i]} is a two-element array + * {@code {selectLeft, selectRight}} with {@code selectLeft} on the {@code mParamPaths[i][0]} side. This lets the + * proof and interpolation consumers use the edge directly instead of searching the clause equalities for it. + */ + final CCParameter[][] mSelectEdges; final DataTypeLemma mDTLemma; - public CCAnnotation(final SymmetricPair diseq, final Collection paths, final RuleKind rule) { - mDiseq = diseq; - mPaths = new CCTerm[paths.size()][]; - mWeakIndices = new CCTerm[mPaths.length]; - int i = 0; - for (final SubPath p : paths) { - mPaths[i] = p.getTerms(); + public CCAnnotation(final SymmetricPair diseq, final Collection paths, final RuleKind rule) { + this(diseq, paths, rule, null); + } + + public CCAnnotation(final SymmetricPair diseq, final Collection paths, + final DataTypeLemma lemma) { + this(diseq, paths, lemma.getRule(), lemma); + } + + /** + * Annotation with an explicit, pre-built main path. Used by the offset conflict explainer (see + * {@link CongruencePath#computeMergeConflictCycle}): the main path may span two congruence classes joined by the + * conflicting equality/congruence bridge, so its node offsets cannot be derived from {@code mOffsetToRep} (which is + * relative to each node's own representative). The two single-class halves are stitched by hand into {@code mainPath} + * with the bridging offset, while the {@code otherPaths} (congruence sub-lemmas, each within one class) keep deriving + * their offsets the usual way. + */ + public CCAnnotation(final SymmetricPair diseq, final CCParameter[] mainPath, + final Collection otherPaths, final RuleKind rule) { + mDiseqParam = diseq; + final int n = 1 + otherPaths.size(); + mParamPaths = new CCParameter[n][]; + mWeakIndices = new CCParameter[n]; + mSelectEdges = new CCParameter[n][]; + mParamPaths[0] = mainPath; + mWeakIndices[0] = null; + mSelectEdges[0] = null; + int i = 1; + for (final SubPath p : otherPaths) { + mParamPaths[i] = p.getParams(); mWeakIndices[i] = p instanceof WeakSubPath ? ((WeakSubPath) p).getIndex() : null; + mSelectEdges[i] = p instanceof WeakSubPath ? ((WeakSubPath) p).getSelectEdge() : null; i++; } mRule = rule; mDTLemma = null; } - public CCAnnotation(final SymmetricPair diseq, final Collection paths, final DataTypeLemma lemma) { - mDiseq = diseq; - mPaths = new CCTerm[paths.size()][]; - mWeakIndices = new CCTerm[mPaths.length]; + private CCAnnotation(final SymmetricPair diseq, final Collection paths, final RuleKind rule, + final DataTypeLemma lemma) { + mDiseqParam = diseq; + mParamPaths = new CCParameter[paths.size()][]; + mWeakIndices = new CCParameter[mParamPaths.length]; + mSelectEdges = new CCParameter[mParamPaths.length][]; int i = 0; for (final SubPath p : paths) { - mPaths[i] = p.getTerms(); + mParamPaths[i] = p.getParams(); mWeakIndices[i] = p instanceof WeakSubPath ? ((WeakSubPath) p).getIndex() : null; + mSelectEdges[i] = p instanceof WeakSubPath ? ((WeakSubPath) p).getSelectEdge() : null; i++; } - mRule = lemma.getRule(); + mRule = rule; mDTLemma = lemma; } - public SymmetricPair getDiseq() { - return mDiseq; + /** The disequality with offsets. */ + public SymmetricPair getDiseqParam() { + return mDiseqParam; } - public CCTerm[][] getPaths() { - return mPaths; + /** The paths with offsets. */ + public CCParameter[][] getParamPaths() { + return mParamPaths; } - public CCTerm[] getWeakIndices() { + public CCParameter[] getWeakIndices() { return mWeakIndices; } + /** For each path, its select/const edge {@code {left, right}} or {@code null}; see {@link #mSelectEdges}. */ + public CCParameter[][] getSelectEdges() { + return mSelectEdges; + } + /** * Convert the annotation into a term suitable to add to the proof tree. The * output is a lemma where all congruences are explained by auxiliary CC lemmas @@ -305,8 +347,8 @@ public Term toTerm(final Clause clause, final ProofRules proofRules) { public String toString() { final StringBuilder sb = new StringBuilder(); sb.append('('); - sb.append(mDiseq); - for (int i = 0; i < mPaths.length; i++) { + sb.append(mDiseqParam); + for (int i = 0; i < mParamPaths.length; i++) { if (mWeakIndices[i] != null) { sb.append(" :weak ").append(mWeakIndices[i]).append(' '); } else { @@ -314,8 +356,8 @@ public String toString() { } sb.append("("); String comma = ""; - for (final CCTerm term : mPaths[i]) { - sb.append(comma).append(term); + for (final CCParameter param : mParamPaths[i]) { + sb.append(comma).append(param); comma = " "; } sb.append(")"); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAppTerm.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAppTerm.java index 511d4d3fb..eab48481e 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAppTerm.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCAppTerm.java @@ -19,31 +19,47 @@ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure; import java.util.ArrayDeque; +import java.util.ArrayList; import java.util.HashMap; import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.logic.Term; import de.uni_freiburg.informatik.ultimate.util.HashUtils; public class CCAppTerm extends CCTerm { final FunctionSymbol mFunc; - final CCTerm[] mArgs; + /** + * The arguments of the application as {@link CCParameter}s: argument {@code i} of the represented application has + * value {@code mArgs[i]}, i.e. {@code mArgs[i].getCCTerm() + mArgs[i].getOffset()}. An offset-free argument is a + * bare {@link CCTerm} (no wrapper object), so a term like {@code f(x+5)} stores the offset-free CCTerm {@code x} + * wrapped in an {@link OffsettedCCTerm} remembering the {@code +5} only for that one argument; plain congruence + * closure allocates nothing extra. + */ + final CCParameter[] mArgs; Term mSmtTerm; CongruenceTrigger mCongTrigger; FindTriggerTrigger mFindTrigger; + /** + * The reverse trigger signatures created for this application by {@link MasterReverseTrigger#activate}, one per + * master trigger watching this application's function symbol. They must be removed together with this term (see + * {@code CClosure.removeTerm}), otherwise a stale application entry survives a pop and later trigger merges + * activate reverse triggers on the removed term. + */ + ArrayList mReverseTriggers; - public CCAppTerm(FunctionSymbol fsym, final CCTerm[] args, + public CCAppTerm(FunctionSymbol fsym, final CCParameter[] args, final CClosure engine, final boolean isFromQuant) { super(HashUtils.hashJenkins(fsym.hashCode(), (Object[]) args), isFromQuant ? CCAppTerm.computeAge(args) : 0); mFunc = fsym; mArgs = args; } - private final static int computeAge(CCTerm[] args) { + private final static int computeAge(CCParameter[] args) { int age = 1; for (int i = 0; i < args.length; i++) { - age = Math.max(age, args[i].mAge + 1); + age = Math.max(age, args[i].getCCTerm().mAge + 1); } return age; } @@ -52,11 +68,25 @@ public FunctionSymbol getFunctionSymbol() { return mFunc; } - public CCTerm[] getArguments() { - return mArgs; + /** The number of arguments of this application. */ + public int getArgCount() { + return mArgs.length; } - public CCTerm getArgument(int argPosition) { + /** + * @return the constant offset added to the argument at the given position, i.e. the argument value is + * {@code getArgParam(argPosition).getCCTerm() + getArgOffset(argPosition)}. + */ + public Rational getArgOffset(int argPosition) { + return mArgs[argPosition].getOffset(); + } + + /** + * The value of the argument at the given position as a {@link CCParameter} ("a CCTerm up to a constant offset"). + * This is the only argument accessor: callers that genuinely want the offset-free structural CCTerm must say so + * explicitly via {@code getArgParam(i).getCCTerm()}. + */ + public CCParameter getArgParam(int argPosition) { return mArgs[argPosition]; } @@ -79,7 +109,10 @@ public String toString() { visited.put(app, visited.size()); todo.add(")"); for (int i = app.mArgs.length - 1; i >= 0; i--) { - todo.add(app.mArgs[i]); + if (!app.getArgOffset(i).equals(Rational.ZERO)) { + todo.add("+" + app.getArgOffset(i)); + } + todo.add(app.mArgs[i].getCCTerm()); todo.add(" "); } todo.add(app.mFunc.getApplicationString()); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCEquality.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCEquality.java index a962ecd6a..1994eaa74 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCEquality.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCEquality.java @@ -28,7 +28,19 @@ public class CCEquality extends DPLLAtom { private final CCTerm mLhs, mRhs; + /** + * The constant offset of this equality: it states {@code mLhs == mRhs + mOffset}. For a plain equality this is + * {@link Rational#ZERO}; a non-zero value turns this into an offset equality, used to relate terms that differ by a + * constant (e.g. {@code a == b + 3}). + */ + private Rational mOffset = Rational.ZERO; CCEquality mDiseqReason; + /** + * Orientation of {@link #mDiseqReason} relative to this equality, captured when the reason is set (while the two + * sides are still in distinct classes): {@code true} iff {@code mDiseqReason.getLhs()} is in {@code getLhs()}'s class. + * Needed because once the two sides later merge, {@code getRepresentative()} can no longer tell the orientation. + */ + boolean mDiseqOrientation; private LAEquality mLasd; private Rational mLAFactor; private final Entry mEntry; @@ -39,11 +51,12 @@ public CCEquality getCCEquality() { } } - CCEquality(final int assertionstacklevel, final CCTerm c1, final CCTerm c2) { + CCEquality(final int assertionstacklevel, final CCTerm c1, final CCTerm c2, Rational offset) { super(HashUtils.hashJenkins(c1.hashCode(), c2), assertionstacklevel); mLhs = c1; mRhs = c2; mEntry = new Entry(); + mOffset = offset; } public CCTerm getLhs() { @@ -54,6 +67,24 @@ public CCTerm getRhs() { return mRhs; } + /** + * @return the constant offset of this equality, such that {@code getLhs() == getRhs() + getOffset()}. + */ + public Rational getOffset() { + return mOffset; + } + + /** + * Set the separating disequality that makes this equality false, recording its orientation relative to this equality. + * Must be called while this equality's two sides are still in distinct classes (which holds at every propagation + * site), so the orientation is unambiguous; it is then used even after the two sides are later merged. + */ + void setDiseqReason(final CCEquality reason) { + assert reason.getLhs().getRepresentative() != reason.getRhs().getRepresentative(); + mDiseqReason = reason; + mDiseqOrientation = reason.getLhs().getRepresentative() == mLhs.getRepresentative(); + } + public Entry getEntry() { return mEntry; } @@ -84,11 +115,22 @@ public void removeLASharedData() { @Override public Term getSMTFormula(final Theory smtTheory) { - return smtTheory.term("=", mLhs.getFlatTerm(), mRhs.getFlatTerm()); + final Term lhs = mLhs.getFlatTerm(); + final Term rhs = mRhs.getFlatTerm(); + if (mOffset.equals(Rational.ZERO)) { + return smtTheory.term("=", lhs, rhs); + } + // this equality states lhs == rhs + mOffset; build the right-hand side as a + // flattened sum (not a nested (+ rhs offset)) so the proof checker's + // non-recursive Polynomial parsing relates it to the original flat term. + return smtTheory.term("=", lhs, CCParameter.addConstant(rhs, mOffset)); } @Override public String toString() { - return mLhs + " == " + mRhs; + if (mOffset.equals(Rational.ZERO)) { + return mLhs + " == " + mRhs; + } + return mLhs + " == " + mRhs + " + " + mOffset; } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCParameter.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCParameter.java new file mode 100644 index 000000000..51b336af5 --- /dev/null +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCParameter.java @@ -0,0 +1,124 @@ +/* + * Copyright (C) 2026 University of Freiburg + * + * This file is part of SMTInterpol. + * + * SMTInterpol is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SMTInterpol is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SMTInterpol. If not, see . + */ +package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure; + +import de.uni_freiburg.informatik.ultimate.logic.ApplicationTerm; +import de.uni_freiburg.informatik.ultimate.logic.Rational; +import de.uni_freiburg.informatik.ultimate.logic.Term; +import de.uni_freiburg.informatik.ultimate.smtinterpol.util.Polynomial; + +/** + * A value of the form {@code getCCTerm() + getOffset()}: a CCTerm together with a constant offset. This is what every + * consumer of a function application argument actually deals with once offset equalities exist (the argument of + * {@code f(x+5)} is the CCTerm {@code x} plus the offset {@code 5}), as well as array indices, congruence arguments and + * shared-term comparisons. + * + *

A bare {@link CCTerm} is a {@code CCParameter} with offset {@link Rational#ZERO}, so the common offset-free + * case needs no wrapper object; only non-zero offsets allocate an {@link OffsettedCCTerm}. Use {@link #of} to build one + * with this canonicalization. + * + *

The value identity of a parameter is the pair {@code (getRepresentative(), getOffsetToRep())}: two + * parameters denote the same value iff they agree on both. Note this identity changes when the underlying class is + * merged (the representative and its offset shift), so it must not be used as a key in a map that persists across + * merges; see {@link #sameValueAs}. + * + * @author Jochen Hoenicke + */ +public interface CCParameter { + + /** The underlying CCTerm. */ + CCTerm getCCTerm(); + + /** The constant offset added to the CCTerm; {@link Rational#ZERO} for a bare {@link CCTerm}. */ + Rational getOffset(); + + /** + * The SMT-LIB term denoting this value: the underlying term when the offset is zero, otherwise {@code (+ term + * offset)} (or a plain constant when the underlying term is the constant {@code 0}). A bare {@link CCTerm} returns + * its own flat term unchanged, so offset-free uses are byte-identical; only a non-zero (necessarily numeric) offset + * builds the sum. + */ + Term getFlatTerm(); + + /** The representative of the underlying CCTerm's congruence class. */ + default CCTerm getRepresentative() { + return getCCTerm().getRepresentative(); + } + + /** The offset of this value relative to the representative: {@code value == value(representative) + this}. */ + default Rational getOffsetToRep() { + return getCCTerm().getOffsetToRep().add(getOffset()); + } + + /** Whether this and the other parameter currently denote the same value (same representative and offset). */ + default boolean sameValueAs(final CCParameter other) { + return getRepresentative() == other.getRepresentative() && getOffsetToRep().equals(other.getOffsetToRep()); + } + + /** + * A canonical key for this value, suitable as a map key within a single snapshot (e.g. while the array theory + * rebuilds its structures). It is built from the representative, so its {@code equals}/{@code hashCode} (the bare + * representative for offset zero, an {@link OffsettedCCTerm} over {@code (representative, offsetToRep)} otherwise) + * coincide with value identity. The key changes when the class is merged, so it must be rebuilt after merges, not + * stored across them. + */ + default CCParameter getValueKey() { + return CCParameter.of(getRepresentative(), getOffsetToRep()); + } + + /** + * Build a parameter for {@code term + offset}, returning the bare term when the offset is zero so that the common + * case allocates nothing. + */ + static CCParameter of(final CCTerm term, final Rational offset) { + return offset.equals(Rational.ZERO) ? term : new OffsettedCCTerm(term, offset); + } + + /** + * Build the SMT term {@code term + offset} as a flattened sum: when {@code term} is itself a {@code +} + * application, its summands are spliced in rather than nested, so the result is {@code (+ z w offset)} rather than + * {@code (+ (+ z w) offset)}. This matters because the proof checker parses a {@code +} term with the non-recursive + * {@link de.uni_freiburg.informatik.ultimate.smtinterpol.util.Polynomial} (only the top level is flattened); a nested + * sum would be treated as an opaque monomial and could not be related arithmetically to the original flat parameter + * term that the {@link de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTermBuilder} split into this + * {@code (base, offset)} pair. Flattening reconstructs that original parameter term. + * + *

A constant {@code term} (the base {@code 0} of a plain-numeral parameter) folds into a plain constant, e.g. + * {@code 5} rather than {@code (+ 0 5)}, matching the canonic term of that value. A zero offset returns + * {@code term} unchanged. + */ + static Term addConstant(final Term term, final Rational offset) { + if (offset.equals(Rational.ZERO)) { + return term; + } + final Rational termValue = Polynomial.parseConstant(term); + if (termValue != null) { + return termValue.add(offset).toTerm(term.getSort()); + } + final Term offsetTerm = offset.toTerm(term.getSort()); + if (term instanceof ApplicationTerm && ((ApplicationTerm) term).getFunction().getName().equals("+")) { + final Term[] inner = ((ApplicationTerm) term).getParameters(); + final Term[] args = new Term[inner.length + 1]; + System.arraycopy(inner, 0, args, 0, inner.length); + args[inner.length] = offsetTerm; + return term.getTheory().term("+", args); + } + return term.getTheory().term("+", term, offsetTerm); + } +} diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCProofGenerator.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCProofGenerator.java index af3037e6f..d2cbf72ff 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCProofGenerator.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCProofGenerator.java @@ -52,59 +52,46 @@ public class CCProofGenerator { * This class is used to keep together paths and their indices (i.e. null for subpaths, and weakpathindex else). */ private static class IndexedPath { - private final CCTerm mIndex; - private final CCTerm[] mPath; + private final CCParameter mIndex; + /** The path nodes as CCParameters (with offsets); a step's offset is justified by an offset equality. */ + private final CCParameter[] mPath; + /** + * The select/const edge justifying this weak path's single weak-congruence step, or {@code null}. See + * {@link CCAnnotation#mSelectEdges}: {@code mSelectEdge[0]} is on the {@code mPath[0]} side. + */ + private final CCParameter[] mSelectEdge; - public IndexedPath(final CCTerm index, final CCTerm[] path) { + public IndexedPath(final CCParameter index, final CCParameter[] path, final CCParameter[] selectEdge) { mIndex = index; mPath = path; + mSelectEdge = selectEdge; } - public CCTerm getIndex() { + public CCParameter getIndex() { return mIndex; } - public CCTerm[] getPath() { + public CCParameter[] getPath() { return mPath; } - public SymmetricPair getPathEnds() { - return new SymmetricPair<>(mPath[0], mPath[mPath.length - 1]); + public CCParameter[] getSelectEdge() { + return mSelectEdge; } - @Override - public String toString() { - return mIndex + ": " + Arrays.toString(mPath); - } - } - - /** - * This class is used to represent a select edge and select-const edges in weak congruences. - */ - private static class SelectEdge { - private final CCTerm mLeft; - private final CCTerm mRight; - - public SelectEdge(final CCTerm left, final CCTerm right) { - mLeft = left; - mRight = right; - } - - public SymmetricPair toSymmetricPair() { - return new SymmetricPair<>(mLeft, mRight); - } - - public CCTerm getLeft() { - return mLeft; + /** The two path ends as a CCParameter pair (with offsets), i.e. the equality this path proves. */ + public SymmetricPair getPathEndParams() { + return new SymmetricPair<>(mPath[0], mPath[mPath.length - 1]); } - public CCTerm getRight() { - return mRight; + /** The offset-aware key of the path ends, used to look the path up among shared subpaths. */ + public OffsetPair getPathEnds() { + return key(getPathEndParams()); } @Override public String toString() { - return mLeft + " <-> " + mRight; + return mIndex + ": " + Arrays.toString(mPath); } } @@ -131,9 +118,10 @@ private class ProofInfo { // Information needed to build the proof graph /** - * The equality this lemma proves. + * The equality this lemma proves, as a CCParameter pair (with offsets); the offset-free key (see + * {@link CCProofGenerator#key}) is used for the lookup maps. */ - private SymmetricPair mLemmaDiseq; + private SymmetricPair mLemmaDiseq; /** * The literals this lemma requires for the proof. */ @@ -159,7 +147,7 @@ public ProofInfo() { mNumVisitedParents = 0; } - public SymmetricPair getDiseq() { + public SymmetricPair getDiseq() { return mLemmaDiseq; } @@ -196,7 +184,7 @@ private void addLiteral(Literal literal) { mProofLiterals.add(new ProofLiteral(literal.getAtom().getSMTFormula(theory), literal.getSign() > 0)); } - private void addAuxLiteral(SymmetricPair equality, boolean positive) { + private void addAuxLiteral(SymmetricPair equality, boolean positive) { final Term eqTerm = addAuxEquality(equality); mProofLiterals.add(new ProofLiteral(eqTerm, positive)); } @@ -209,14 +197,15 @@ private void addSubProof(ProofInfo congruence) { /** * Collect the proof info for one path. */ - private boolean collectEquality(final SymmetricPair termPair) { + private boolean collectEquality(final SymmetricPair termPair) { + final OffsetPair termKey = key(termPair); if (isEqualityLiteral(termPair)) { // equality literals are just added - addLiteral(mEqualityLiterals.get(termPair)); + addLiteral(mEqualityLiterals.get(termKey)); return true; - } else if (mPathProofMap.containsKey(termPair)) { + } else if (mPathProofMap.containsKey(termKey)) { // already created sub proof; add it - addSubProof(mPathProofMap.get(termPair)); + addSubProof(mPathProofMap.get(termKey)); return true; } else { // create congruence sub proof @@ -224,7 +213,7 @@ private boolean collectEquality(final SymmetricPair termPair) { if (congruence == null) { return false; } - mPathProofMap.put(termPair, congruence); + mPathProofMap.put(termKey, congruence); addSubProof(congruence); return true; } @@ -235,25 +224,26 @@ private boolean collectEquality(final SymmetricPair termPair) { */ private void collectStrongPath(final IndexedPath indexedPath) { assert (indexedPath.getIndex() == null); - final CCTerm[] path = indexedPath.getPath(); + final CCParameter[] path = indexedPath.getPath(); // Check cases (i) - (iv) for all term pairs. for (int i = 0; i < path.length - 1; i++) { - final CCTerm firstTerm = path[i]; - final CCTerm secondTerm = path[i + 1]; - final SymmetricPair termPair = new SymmetricPair<>(firstTerm, secondTerm); + final CCParameter firstTerm = path[i]; + final CCParameter secondTerm = path[i + 1]; + final SymmetricPair termPair = new SymmetricPair<>(firstTerm, secondTerm); if (!collectEquality(termPair)) { throw new IllegalArgumentException("Cannot explain term pair " + termPair.toString()); } } } - private void collectSelectIndexEquality(final CCTerm select, final CCTerm pathIndex) { - if (ArrayTheory.isSelectTerm(select)) { - final CCTerm index = ArrayTheory.getIndexFromSelect((CCAppTerm) select); - if (index != pathIndex) { - if (!collectEquality(new SymmetricPair<>(pathIndex, index))) { - throw new AssertionError("Cannot find select index equality " + pathIndex + " = " + index); - } + private void collectSelectIndexEquality(final CCParameter select, final CCTerm array, final CCParameter pathIndex) { + if (!ArrayTheory.isSelectTerm(select) || ArrayTheory.getArrayFromSelect((CCAppTerm) select) != array) { + throw new AssertionError(); + } + final CCParameter index = ArrayTheory.getIndexFromSelect((CCAppTerm) select); + if (!index.equals(pathIndex)) { + if (!collectEquality(new SymmetricPair(pathIndex, index))) { + throw new AssertionError("Cannot find select index equality " + pathIndex + " = " + index); } } } @@ -263,13 +253,17 @@ private void collectSelectIndexEquality(final CCTerm select, final CCTerm pathIn */ private void collectWeakPath(final IndexedPath indexedPath) { assert (indexedPath.getIndex() != null || mRule == RuleKind.WEAKEQ_EXT || mRule == RuleKind.CONST_WEAKEQ); - final CCTerm pathIndex = indexedPath.getIndex(); - final CCTerm[] path = indexedPath.getPath(); - // Check cases (i) - (iv) for all term pairs. + final CCParameter pathIndex = indexedPath.getIndex(); + final CCParameter[] path = indexedPath.getPath(); + // Check cases (i) - (iv) for all term pairs. Array path nodes are offset-free, so we use the structural + // CCTerms for the array cases and the CCParameters only for the equality (case i). for (int i = 0; i < path.length - 1; i++) { - final CCTerm firstTerm = path[i]; - final CCTerm secondTerm = path[i + 1]; - final SymmetricPair termPair = new SymmetricPair<>(firstTerm, secondTerm); + final CCParameter firstParam = path[i]; + final CCParameter secondParam = path[i + 1]; + // Array terms are offset free, this cast should never fail. + final CCTerm firstTerm = (CCTerm) firstParam; + final CCTerm secondTerm = (CCTerm) secondParam; + final SymmetricPair termPair = new SymmetricPair<>(firstParam, secondParam); // Case (i) if (collectEquality(termPair)) { continue; @@ -288,32 +282,33 @@ private void collectWeakPath(final IndexedPath indexedPath) { if (pathIndex == null) { continue; } - final CCTerm storeIndex = ArrayTheory.getIndexFromStore((CCAppTerm) storeTerm); - final SymmetricPair indexPair = new SymmetricPair<>(pathIndex, storeIndex); + final CCParameter storeIndex = ArrayTheory.getIndexFromStore((CCAppTerm) storeTerm); + final SymmetricPair indexPair = new SymmetricPair<>(pathIndex, storeIndex); if (isDisequalityLiteral(indexPair)) { - addLiteral(mEqualityLiterals.get(indexPair)); + addLiteral(mEqualityLiterals.get(key(indexPair))); continue; } if (isTrivialDisequality(indexPair)) { - mTrivialDisequalities.add(indexPair); + mTrivialDisequalities.add(key(indexPair)); addAuxLiteral(indexPair, true); continue; } } // Case (iv) select if (mRule == RuleKind.WEAKEQ_EXT && pathIndex != null) { - final SelectEdge selectEdge = findSelectPath(termPair, pathIndex); + // Use the select/const edge recorded in the annotation. + final CCParameter[] selectEdge = indexedPath.getSelectEdge(); if (selectEdge != null) { - if (selectEdge.getLeft() != selectEdge.getRight()) { - if (!collectEquality(selectEdge.toSymmetricPair())) { + if (selectEdge[0] != selectEdge[1]) { + if (!collectEquality(new SymmetricPair<>(selectEdge[0], selectEdge[1]))) { throw new AssertionError("Cannot find select edge " + selectEdge); } } - if (!isConst(firstTerm, selectEdge.getLeft())) { - collectSelectIndexEquality(selectEdge.getLeft(), pathIndex); + if (!isConst(firstTerm, selectEdge[0])) { + collectSelectIndexEquality(selectEdge[0], firstTerm, pathIndex); } - if (!isConst(secondTerm, selectEdge.getRight())) { - collectSelectIndexEquality(selectEdge.getRight(), pathIndex); + if (!isConst(secondTerm, selectEdge[1])) { + collectSelectIndexEquality(selectEdge[1], secondTerm, pathIndex); } continue; } @@ -335,19 +330,97 @@ public String toString() { // Store the self-built auxiliary equality literals, such that the // arguments of the equality are always in the same order. - private HashMap, Term> mAuxLiterals; - private HashMap, Literal> mEqualityLiterals; - private HashMap, ProofInfo> mPathProofMap; - private LinkedHashSet> mAllEqualities; - private LinkedHashSet> mTrivialDisequalities; + private HashMap mAuxLiterals; + private HashMap mEqualityLiterals; + private HashMap mPathProofMap; + private LinkedHashSet mAllEqualities; + private LinkedHashSet mTrivialDisequalities; private ProofRules mProofRules; + /** + * A lookup key identifying an (offset) equality fact between two CCTerms: {@code value(first) == value(second) + + * offset}. The maps in this class are keyed on this triple so that two facts sharing offset-free endpoints but + * differing in offset — e.g. {@code car(x) = y} (offset 0, a dt-project consequence) and {@code car(x) = y-1} + * (offset -1, asserted) in an offset anti-cycle — are kept distinct, while different renderings of the same + * fact (e.g. {@code x+5 = y+7} and {@code x+7 = y+9}, both {@code x = y+2}) still collapse to one key. The key is + * canonical under swapping the two terms (which negates the offset), mirroring {@link CCTermPairHash}. + */ + static final class OffsetPair { + final CCTerm mFirst; + final CCTerm mSecond; + final Rational mOffset; + + OffsetPair(final CCParameter first, final CCParameter second) { + mFirst = first.getCCTerm(); + mSecond = second.getCCTerm(); + // Structural offset (matching CCEquality.getOffset() used in collectClauseLiterals); the dynamic + // getOffsetToRep() must not be used here, as disequality endpoints are not in the same class. + mOffset = second.getOffset().sub(first.getOffset()); + } + + OffsetPair(final CCTerm first, final CCTerm second, final Rational offset) { + mFirst = first; + mSecond = second; + mOffset = offset; + } + + CCTerm getFirst() { + return mFirst; + } + + CCTerm getSecond() { + return mSecond; + } + + @Override + public int hashCode() { + // symmetric in (first, second); offsetHash is invariant under (first, second, off) -> (second, first, -off) + return (mFirst.hashCode() ^ mSecond.hashCode()) + CCTermPairHash.offsetHash(mFirst, mSecond, mOffset); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof OffsetPair)) { + return false; + } + final OffsetPair o = (OffsetPair) other; + if (mFirst == o.mFirst && mSecond == o.mSecond) { + return mOffset.equals(o.mOffset); + } + if (mFirst == o.mSecond && mSecond == o.mFirst) { + return mOffset.equals(o.mOffset.negate()); + } + return false; + } + + @Override + public String toString() { + return mOffset.equals(Rational.ZERO) ? "(" + mFirst + "," + mSecond + ")" + : "(" + mFirst + "," + mSecond + "+" + mOffset + ")"; + } + } + + /** + * The offset-aware key of a CCParameter equality: {@code value(first) == value(second) + offset}, where + * {@code offset = second.getOffset() - first.getOffset()} is the constant offset between the two underlying CCTerms. + * See {@link OffsetPair}. + */ + static OffsetPair key(final SymmetricPair pair) { + final CCParameter first = pair.getFirst(); + final CCParameter second = pair.getSecond(); + return new OffsetPair(first, second); + } + public CCProofGenerator(final CCAnnotation arrayAnnot) { mAnnot = arrayAnnot; mRule = arrayAnnot.mRule; - mIndexedPaths = new IndexedPath[arrayAnnot.getPaths().length]; + mIndexedPaths = new IndexedPath[arrayAnnot.getParamPaths().length]; for (int i = 0; i < mIndexedPaths.length; i++) { - mIndexedPaths[i] = new IndexedPath(arrayAnnot.getWeakIndices()[i], arrayAnnot.getPaths()[i]); + mIndexedPaths[i] = new IndexedPath(arrayAnnot.getWeakIndices()[i], arrayAnnot.getParamPaths()[i], + arrayAnnot.getSelectEdges()[i]); } } @@ -375,15 +448,15 @@ public Term toTerm(final Clause clause, final ProofRules proofRules) { // Collect the paths needed to prove the main disequality final ProofInfo mainInfo = findMainPaths(); - if (mAnnot.mDiseq != null) { - final SymmetricPair mainDiseq = mAnnot.mDiseq; + if (mAnnot.mDiseqParam != null) { + final SymmetricPair mainDiseq = mAnnot.mDiseqParam; if (!isDisequalityLiteral(mainDiseq)) { assert isTrivialDisequality(mainDiseq); - mTrivialDisequalities.add(mainDiseq); + mTrivialDisequalities.add(key(mainDiseq)); addAuxEquality(mainDiseq); } mainInfo.mLemmaDiseq = mainDiseq; - mPathProofMap.put(mainDiseq, mainInfo); + mPathProofMap.put(key(mainDiseq), mainInfo); } else { assert mAnnot.mRule == RuleKind.DT_CASES || mAnnot.mRule == RuleKind.DT_UNIQUE || mAnnot.mRule == RuleKind.DT_DISJOINT @@ -407,7 +480,9 @@ private void collectClauseLiterals(final Clause clause) { for (int i = 0; i < clause.getSize(); i++) { final Literal literal = clause.getLiteral(i); final CCEquality atom = (CCEquality) literal.getAtom(); - final SymmetricPair pair = new SymmetricPair<>(atom.getLhs(), atom.getRhs()); + // key on the actual offset so two literals between the same terms but with different offsets + // (e.g. car(x)=y and car(x)=y-1 in an offset anti-cycle) stay distinct. + final OffsetPair pair = new OffsetPair(atom.getLhs(), atom.getRhs(), atom.getOffset()); mEqualityLiterals.put(pair, literal); if (literal.getSign() < 0) { /* equality in conflict (negated in clause) */ @@ -429,8 +504,9 @@ private void collectStrongEqualities() { // never a strong equality, even though its weak index is null. if (indexedPath.getIndex() == null && ((mRule != RuleKind.WEAKEQ_EXT && mRule != RuleKind.CONST_WEAKEQ) || i > 0)) { - final CCTerm[] path = indexedPath.getPath(); - final SymmetricPair pathEnds = new SymmetricPair<>(path[0], path[path.length - 1]); + final CCParameter[] path = indexedPath.getPath(); + final SymmetricPair pathEndParams = indexedPath.getPathEndParams(); + final OffsetPair pathEnds = key(pathEndParams); if (mAllEqualities.add(pathEnds) && !mPathProofMap.containsKey(pathEnds)) { if (path.length == 2) { // A path of length 2 must be a congruence, otherwise we would not be able to explain it @@ -439,7 +515,7 @@ private void collectStrongEqualities() { mPathProofMap.put(pathEnds, congruence); } else { final ProofInfo pathInfo = new ProofInfo(); - pathInfo.mLemmaDiseq = pathEnds; + pathInfo.mLemmaDiseq = pathEndParams; pathInfo.mProofPaths = new IndexedPath[] { indexedPath }; pathInfo.collectStrongPath(indexedPath); mPathProofMap.put(pathEnds, pathInfo); @@ -463,13 +539,13 @@ private ProofInfo findMainPaths() { return mPathProofMap.get(mIndexedPaths[0].getPathEnds()); case READ_OVER_WEAKEQ: { // collect index equality and the weak path - final SymmetricPair selectEquality = mAnnot.mDiseq; + final SymmetricPair selectEquality = mAnnot.mDiseqParam; assert ArrayTheory.isSelectTerm(selectEquality.getFirst()) && ArrayTheory.isSelectTerm(selectEquality.getSecond()); // collect the index equality - final CCTerm idx1 = ArrayTheory.getIndexFromSelect((CCAppTerm) selectEquality.getFirst()); - final CCTerm idx2 = ArrayTheory.getIndexFromSelect((CCAppTerm) selectEquality.getSecond()); - if (idx1 != idx2) { + final CCParameter idx1 = ArrayTheory.getIndexFromSelect((CCAppTerm) selectEquality.getFirst()); + final CCParameter idx2 = ArrayTheory.getIndexFromSelect((CCAppTerm) selectEquality.getSecond()); + if (!idx1.equals(idx2)) { mainProof.collectEquality(new SymmetricPair<>(idx1, idx2)); } // Only a weak path, which must be the first path @@ -514,7 +590,8 @@ private ProofInfo findMainPaths() { case DT_TESTER: case DT_UNIQUE: for (final SymmetricPair dependentEq : mAnnot.mDTLemma.getReason()) { - mainProof.collectEquality(dependentEq); + // datatype lemma reasons are offset-free + mainProof.collectEquality(new SymmetricPair(dependentEq.getFirst(), dependentEq.getSecond())); } mainProof.mProofPaths = new IndexedPath[0]; break; @@ -558,8 +635,9 @@ private ArrayList determineProofOrder(final ProofInfo mainInfo) { return proofOrder; } - private Term addAuxEquality(SymmetricPair equality) { - if (!mAuxLiterals.containsKey(equality)) { + private Term addAuxEquality(SymmetricPair equality) { + final OffsetPair eqKey = key(equality); + if (!mAuxLiterals.containsKey(eqKey)) { final Theory theory = mProofRules.getTheory(); Term lhs = equality.getFirst().getFlatTerm(); Term rhs = equality.getSecond().getFlatTerm(); @@ -571,9 +649,9 @@ private Term addAuxEquality(SymmetricPair equality) { rhs = lhs; lhs = constantTerm; } - mAuxLiterals.put(equality, theory.term("=", lhs, rhs)); + mAuxLiterals.put(eqKey, theory.term("=", lhs, rhs)); } - return mAuxLiterals.get(equality); + return mAuxLiterals.get(eqKey); } private Term buildLemma(final ProofRules proofRules, RuleKind rule, final ProofInfo info, final Term mainEq) { @@ -595,7 +673,7 @@ private Term buildLemma(final ProofRules proofRules, RuleKind rule, final ProofI if (rule == RuleKind.CONG) { // this is a transitivity or congruence lemma assert info.getPaths().length == 1; - final CCTerm[] path = info.getPaths()[0].getPath(); + final CCParameter[] path = info.getPaths()[0].getPath(); final Term[] subs = new Term[path.length]; for (int j = 0; j < path.length; ++j) { subs[j] = path[j].getFlatTerm(); @@ -604,7 +682,7 @@ private Term buildLemma(final ProofRules proofRules, RuleKind rule, final ProofI subannots = subs; } else { final IndexedPath[] paths = info.getPaths(); - final SymmetricPair infoDiseq = info.getDiseq(); + final SymmetricPair infoDiseq = info.getDiseq(); Object[] lemmaAnnot = new Object[0]; if (mAnnot.mDTLemma != null && mAnnot.mDTLemma.getAnnotation() != null) { lemmaAnnot = mAnnot.mDTLemma.getAnnotation(); @@ -620,8 +698,8 @@ private Term buildLemma(final ProofRules proofRules, RuleKind rule, final ProofI subannots[k++] = annot; } for (final IndexedPath p : paths) { - final CCTerm index = p.getIndex(); - final CCTerm[] path = p.getPath(); + final CCParameter index = p.getIndex(); + final CCParameter[] path = p.getPath(); final Term[] subs = new Term[path.length]; for (int j = 0; j < path.length; ++j) { subs[j] = path[j].getFlatTerm(); @@ -631,7 +709,16 @@ private Term buildLemma(final ProofRules proofRules, RuleKind rule, final ProofI subannots[k++] = subs; } else { subannots[k++] = ":weakpath"; - subannots[k++] = new Object[] { index.getFlatTerm(), subs }; + final CCParameter[] edge = p.getSelectEdge(); + if (edge == null) { + subannots[k++] = new Object[] { index.getFlatTerm(), subs }; + } else { + // Append the select/const edge {left, right} that justifies this weak path's weak-congruence + // step; left is on the subs[0] side. Consumers that predate this element ignore it (they read + // only [0] and [1]). + subannots[k++] = new Object[] { index.getFlatTerm(), subs, + new Object[] { edge[0].getFlatTerm(), edge[1].getFlatTerm() } }; + } } } } @@ -650,26 +737,26 @@ private Term buildProofTerm(final Clause clause, final ProofRules proofRules, // Build main lemma final ProofInfo mainInfo = proofOrder.get(0); - assert mainInfo.getDiseq() == mAnnot.getDiseq(); + assert mainInfo.getDiseq() == mAnnot.getDiseqParam(); // The equality proved by the lemma. It is null for rules without main equality final Term mainEq = mainInfo.getDiseq() == null ? null : isDisequalityLiteral(mainInfo.getDiseq()) - ? mEqualityLiterals.get(mainInfo.getDiseq()).getSMTFormula(theory) - : mAuxLiterals.get(mainInfo.getDiseq()); + ? mEqualityLiterals.get(key(mainInfo.getDiseq())).getSMTFormula(theory) + : mAuxLiterals.get(key(mainInfo.getDiseq())); Term proof = buildLemma(proofRules, mRule, mainInfo, mainEq); // Resolve with sub-lemmas. for (int lemmaNo = 1; lemmaNo < proofOrder.size(); lemmaNo++) { // Build the lemma clause. final ProofInfo info = proofOrder.get(lemmaNo); // auxLiteral should already have been created by the lemma that needs it. - assert mAuxLiterals.containsKey(info.getDiseq()); - final Term provedEq = mAuxLiterals.get(info.getDiseq()); + assert mAuxLiterals.containsKey(key(info.getDiseq())); + final Term provedEq = mAuxLiterals.get(key(info.getDiseq())); // Build lemma annotations. final Term lemma = buildLemma(proofRules, RuleKind.CONG, info, provedEq); proof = proofRules.resolutionRule(provedEq, lemma, proof); } - for (final SymmetricPair trivialDiseq : mTrivialDisequalities) { + for (final OffsetPair trivialDiseq : mTrivialDisequalities) { final Term provedEq = mAuxLiterals.get(trivialDiseq); final ProofLiteral[] proofLits = new ProofLiteral[] { new ProofLiteral(provedEq, false) }; final Term diseqLemma = proofRules.oracle(proofLits, EQAnnotation.getAnnotation()); @@ -678,27 +765,26 @@ private Term buildProofTerm(final Clause clause, final ProofRules proofRules, return proof; } - private boolean isEqualityLiteral(final SymmetricPair termPair) { - return mEqualityLiterals.containsKey(termPair) && mEqualityLiterals.get(termPair).getSign() < 0; + private boolean isEqualityLiteral(final SymmetricPair termPair) { + final OffsetPair k = key(termPair); + return mEqualityLiterals.containsKey(k) && mEqualityLiterals.get(k).getSign() < 0; } - private boolean isDisequalityLiteral(final SymmetricPair termPair) { - return mEqualityLiterals.containsKey(termPair) && mEqualityLiterals.get(termPair).getSign() > 0; + private boolean isDisequalityLiteral(final SymmetricPair termPair) { + final OffsetPair k = key(termPair); + return mEqualityLiterals.containsKey(k) && mEqualityLiterals.get(k).getSign() > 0; } - private boolean isTrivialDisequality(final SymmetricPair termPair) { - final CCTerm first = termPair.getFirst(); - final CCTerm second = termPair.getSecond(); - final Polynomial smtAffine = new Polynomial(first.getFlatTerm()); - smtAffine.add(Rational.MONE, second.getFlatTerm()); + private boolean isTrivialDisequality(final SymmetricPair termPair) { + // use the CCParameter flat terms so an offset clash (e.g. x+2 vs x+5) is detected + final Polynomial smtAffine = new Polynomial(termPair.getFirst().getFlatTerm()); + smtAffine.add(Rational.MONE, termPair.getSecond().getFlatTerm()); if (smtAffine.isConstant()) { return smtAffine.getConstant() != Rational.ZERO; } return smtAffine.isAllIntSummands() && !smtAffine.getConstant().div(smtAffine.getGcd()).isIntegral(); } - - /** * Find argument paths for a congruence. These may also be literals from the original clause. Note that a function * can have several arguments, and a path is needed for each of them! @@ -706,33 +792,51 @@ private boolean isTrivialDisequality(final SymmetricPair termPair) { * @return The argument paths, if they exist for all arguments, or null to indicate that the termpair is not a * congruence. */ - private ProofInfo findCongruencePaths(CCTerm first, CCTerm second) { - final ProofInfo proofInfo = new ProofInfo(); - proofInfo.mLemmaDiseq = new SymmetricPair<>(first, second); - proofInfo.mProofPaths = new IndexedPath[] { new IndexedPath(null, new CCTerm[] { first, second }) }; - if (!(first instanceof CCAppTerm) || !(second instanceof CCAppTerm)) { + private ProofInfo findCongruencePaths(final CCParameter first, final CCParameter second) { + final CCTerm firstTerm = first.getCCTerm(); + final CCTerm secondTerm = second.getCCTerm(); + if (!(firstTerm instanceof CCAppTerm) || !(secondTerm instanceof CCAppTerm) + || !first.getOffset().equals(second.getOffset())) { // This is not a congruence return null; } - final CCAppTerm firstApp = (CCAppTerm) first; - final CCAppTerm secondApp = (CCAppTerm) second; + final CCAppTerm firstApp = (CCAppTerm) firstTerm; + final CCAppTerm secondApp = (CCAppTerm) secondTerm; if (firstApp.getFunctionSymbol() != secondApp.getFunctionSymbol()) { // This is not a congruence return null; } - final CCTerm[] firstArgs = firstApp.getArguments(); - final CCTerm[] secondArgs = secondApp.getArguments(); - assert firstArgs.length == secondArgs.length; - for (int i = 0; i < firstArgs.length; i++) { - if (firstArgs[i] != secondArgs[i]) { - final SymmetricPair argPair = new SymmetricPair<>(firstArgs[i], secondArgs[i]); + final ProofInfo proofInfo = new ProofInfo(); + // A congruence is offset-free: f(a) and f(b) have equal value once their arguments are equal, so the two app + // terms always sit at the same offset on a path and the congruence proves the offset-free f(a) = f(b). Render + // the lemma and its (length-2) proof path with the offset-free app terms; the surrounding :trans/:cong checker + // absorbs the common constant shift. Carrying the offset (e.g. (= (+ f(a) -1) (+ f(b) -1)) :cong) is not a valid + // single congruence — that step is over +, not over f — and breaks the lowlevel proof. The diseq key is offset + // invariant (the offset cancels in the difference), so all lookups are unaffected. + proofInfo.mLemmaDiseq = new SymmetricPair<>(firstTerm, secondTerm); + proofInfo.mProofPaths = + new IndexedPath[] { new IndexedPath(null, new CCParameter[] { firstTerm, secondTerm }, null) }; + + assert firstApp.getArgCount() == secondApp.getArgCount(); + for (int i = 0; i < firstApp.getArgCount(); i++) { + final CCParameter firstArg = firstApp.getArgParam(i); + final CCParameter secondArg = secondApp.getArgParam(i); + // Resolve on the exact CCParameter equality of the arguments. Offset-free this is a direct literal or + // subpath lookup; the lookup maps are keyed offset-free so a shared offset edge is reused. + // TODO offset equalities: when the available proof establishes a shifted version of this argument equality + // (same offset-free edge, different offset, e.g. f(x,x+2)=f(y+2,y+4) where the (x,y) path proves x=y+2 but + // arg 1 needs x+2=y+4), bridge it with a one-step (offset) transitivity lemma instead of looking up the + // exact param pair. This path is only reachable once offsets are enabled under proofs. + if (firstArg.getCCTerm() != secondArg.getCCTerm()) { + final SymmetricPair argPair = new SymmetricPair<>(firstArg, secondArg); + final OffsetPair argKey = key(argPair); if (isEqualityLiteral(argPair)) { - proofInfo.addLiteral(mEqualityLiterals.get(argPair)); - } else if (mPathProofMap.containsKey(argPair)) { - proofInfo.addSubProof(mPathProofMap.get(argPair)); + proofInfo.addLiteral(mEqualityLiterals.get(argKey)); + } else if (mPathProofMap.containsKey(argKey)) { + proofInfo.addSubProof(mPathProofMap.get(argKey)); } else { - // If no path was found for the arguments, termPair is not a congruence! + // If no path was found for the arguments, termpair is not a congruence! return null; } } @@ -740,65 +844,10 @@ private ProofInfo findCongruencePaths(CCTerm first, CCTerm second) { return proofInfo; } - /** - * Search for a select path between two given arrays and paths from the select indices to the weakpath index for - * which one wants to prove equality of the array elements. - * - * @return the select path and (if needed) index paths, or null if there were no suitable paths for the term pair. - */ - private SelectEdge findSelectPath(final SymmetricPair termPair, final CCTerm weakpathindex) { - // first check for trivial select-const edges, i.e., (const (select a j)) and a with j = weakpathindex. - if (ArrayTheory.isConstTerm(termPair.getFirst())) { - final CCTerm value = ArrayTheory.getValueFromConst((CCAppTerm) termPair.getFirst()); - if (isSelect(value, termPair.getSecond(), weakpathindex)) { - return new SelectEdge(value, value); - } - } - if (ArrayTheory.isConstTerm(termPair.getSecond())) { - final CCTerm value = ArrayTheory.getValueFromConst((CCAppTerm) termPair.getSecond()); - if (isSelect(value, termPair.getFirst(), weakpathindex)) { - return new SelectEdge(value, value); - } - } - - for (final SymmetricPair equality : mAllEqualities) { - // Find some select path. - final CCTerm start = equality.getFirst(); - final CCTerm end = equality.getSecond(); - if (isGoodSelectStep(start, end, termPair, weakpathindex)) { - return new SelectEdge(start, end); - } - if (isGoodSelectStep(end, start, termPair, weakpathindex)) { - return new SelectEdge(end, start); - } - } - return null; - } - - /** - * Check if the equality sel1 == sel2 explains the weak step on weakpathindex for termPair. - */ - private boolean isGoodSelectStep(final CCTerm sel1, final CCTerm sel2, final SymmetricPair termPair, - final CCTerm weakpathindex) { - return (isSelect(sel1, termPair.getFirst(), weakpathindex) || isConst(termPair.getFirst(), sel1)) - && (isSelect(sel2, termPair.getSecond(), weakpathindex) || isConst(termPair.getSecond(), sel2)); - } - - /** - * Check if select is a select on array on weakpathindex or something equal to weakpathindex. - */ - private boolean isSelect(final CCTerm select, final CCTerm array, final CCTerm weakpathindex) { - if (!ArrayTheory.isSelectTerm(select) || ArrayTheory.getArrayFromSelect((CCAppTerm) select) != array) { - return false; - } - final CCTerm index = ArrayTheory.getIndexFromSelect((CCAppTerm) select); - return (index == weakpathindex || mAllEqualities.contains(new SymmetricPair<>(weakpathindex, index))); - } - /** * Check if array is an application of const on value */ - private boolean isConst(final CCTerm array, final CCTerm value) { - return (ArrayTheory.isConstTerm(array) && ArrayTheory.getValueFromConst((CCAppTerm) array) == value); + private boolean isConst(final CCParameter array, final CCParameter value) { + return (ArrayTheory.isConstTerm(array) && ArrayTheory.getValueFromConst((CCAppTerm) array).equals(value)); } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTerm.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTerm.java index bf0088892..2d8fad765 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTerm.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTerm.java @@ -18,6 +18,7 @@ */ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.logic.Term; import de.uni_freiburg.informatik.ultimate.smtinterpol.Config; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.Clause; @@ -48,7 +49,7 @@ * * @author hoenicke */ -public abstract class CCTerm extends SimpleListable { +public abstract class CCTerm extends SimpleListable implements CCParameter { /** * The destination of the outgoing equality edge. Every term has at most one @@ -63,6 +64,14 @@ public abstract class CCTerm extends SimpleListable { * is the one that contains the members, ccpar and eqlits information. */ CCTerm mRepStar; + /** + * The offset of this term relative to its representative: the value of this term equals the value of + * {@link #mRepStar} plus this offset. This realises the affine (offset) equality classes: a member {@code t} + * satisfies {@code t == t.mRepStar + t.mOffsetToRep}. A representative has offset {@link Rational#ZERO} relative to + * itself. As long as no offset equality is ever created this is always {@link Rational#ZERO} and the structure + * degenerates to plain congruence closure. + */ + Rational mOffsetToRep = Rational.ZERO; /** * Points to the next merged representative in the congruence class. * This representative can have another representative of its own, if @@ -187,6 +196,7 @@ final boolean invariant() { // assert parInfo.mCCParents.wellformed(); // assert parInfo.mNext == null || parInfo.mFuncSymbNr < parInfo.mNext.mFuncSymbNr; // } + assert mOffsetToRep.equals(Rational.ZERO) : "representative must have zero offset"; for (final CCTerm m : mMembers) { assert m.mRepStar == this; } @@ -199,10 +209,30 @@ final boolean invariant() { return true; } + @Override public final CCTerm getRepresentative() { return mRepStar; } + /** + * @return the offset of this term relative to its representative, i.e. {@code value(this) == value(rep) + offset}. + */ + @Override + public final Rational getOffsetToRep() { + return mOffsetToRep; + } + + /** A bare CCTerm is a {@link CCParameter} for its own value, with structural offset zero. */ + @Override + public final CCTerm getCCTerm() { + return this; + } + + @Override + public final Rational getOffset() { + return Rational.ZERO; + } + public final boolean isRepresentative() { return mRep == this; } @@ -296,7 +326,9 @@ private void propagateSharedEquality(final CClosure engine, final CCTerm otherSh * as sterm is a newly shared term, which must be linear independent * of all previously created terms. */ - final CCEquality cceq = engine.createEquality(this, otherSharedTerm, true); + // value(this) - value(otherSharedTerm); both are in the same class, so the difference of their offsets. + final Rational offset = mOffsetToRep.sub(otherSharedTerm.mOffsetToRep); + final CCEquality cceq = engine.createEquality(this, otherSharedTerm, offset, true); assert cceq != null; if (engine.getLogger().isDebugEnabled()) { engine.getLogger().debug("PL: %s", cceq); @@ -343,6 +375,25 @@ public void invertEqualEdges(final CClosure engine) { } } + /** + * Compute the value difference {@code value(from) - value(to)} implied by a merge reason. For an offset equality the + * reason states {@code reason.getLhs() == reason.getRhs() + reason.getOffset()}; {@code from} and {@code to} are the + * two terms being merged and must be exactly the two sides of the reason (in either order). A {@code null} reason is + * a congruence merge, where the two function applications have equal value, hence a difference of zero. + */ + private static Rational reasonDiff(final CCEquality reason, final CCTerm from, final CCTerm to) { + if (reason == null) { + return Rational.ZERO; + } + if (from == reason.getLhs() && to == reason.getRhs()) { + return reason.getOffset(); + } + if (from == reason.getRhs() && to == reason.getLhs()) { + return reason.getOffset().negate(); + } + throw new AssertionError("merge reason does not match the merged terms"); + } + public Clause merge(final CClosure engine, final CCTerm lhs, final CCEquality reason) { assert reason != null || (this instanceof CCAppTerm && lhs instanceof CCAppTerm); @@ -355,7 +406,20 @@ public Clause merge(final CClosure engine, final CCTerm lhs, final CCEquality re assert dest.invariant(); assert dest.pairHashValid(engine); if (src == dest) { - /* Terms are already merged. */ + /* + * Terms are already merged. With offset equalities this is only consistent if the offset implied by the + * reason matches the offset that the two terms already have within their common class. A mismatch (e.g. + * asserting x = x + 1) is a conflict. + */ + final Rational existingDiff = lhs.mOffsetToRep.sub(mOffsetToRep); + if (!existingDiff.equals(reasonDiff(reason, lhs, this))) { + // The two terms are congruent (reason == null ⇒ implied value difference 0) but already in the same + // class at a non-zero offset existingDiff (e.g. f(x) and f(y) forced equal while the class already has + // f(x) = f(y) + k). This is an offset conflict: build the anti-cycle from the argument equalities (which + // justify the congruence) plus the existing path (which establishes the conflicting offset). + assert reason == null : "non-congruence merge of terms already in the same class"; + return engine.computeCongruenceAntiCycle((CCAppTerm) this, (CCAppTerm) lhs); + } return null; } @@ -377,21 +441,45 @@ private Clause mergeInternal(final CClosure engine, final CCTerm lhs, final CCEq final CCTerm src = lhs.mRepStar; final CCTerm dest = mRepStar; - // Need to prevent MBTC when we get a conflict. Hence a two-way pass - CCEquality diseq = null; - final CCTermPairHash.Info diseqInfo = engine.mPairHash.getInfo(src, dest); - if (diseqInfo != null) { - diseq = diseqInfo.mDiseq; + /* + * The offset that src (the old representative of the merged class) gets relative to dest: value(src) == + * value(dest) + delta. It is derived from the reason and the offsets the two merged terms already have relative + * to their respective representatives. + */ + final Rational delta = reasonDiff(reason, lhs, this).sub(lhs.mOffsetToRep).add(mOffsetToRep); + + // Detect a conflict (a disequality forbidding the merge, or a shared-term clash) before mutating anything and + // return it immediately. The conflict explainers build each half within its own class and never walk the merge + // bridge, so they do not need the equal edge — returning here (rather than merging, then undoing the edge on a + // conflict) keeps the union-find and MBTC from ever seeing the half-merged conflicting state. A disequality + // conflicts only if it forbids exactly the offset delta at which the two classes are being merged. + final CCTermPairHash.Info diseqInfo = engine.mPairHash.getInfo(src, dest, delta); + if (diseqInfo != null && diseqInfo.mDiseq != null) { + final CCEquality diseq = diseqInfo.mDiseq; + // A disequality forbids this merge. Its two sides straddle the freshly added (not-yet-united) bridge, so + // orient them into the source/destination class and build the two halves separately (computeCycle would + // walk a single path across the bridge and mix offset frames). mRepStar is still the pre-merge rep here. + final CCTerm srcEnd = diseq.getLhs().mRepStar == src ? diseq.getLhs() : diseq.getRhs(); + final CCTerm destEnd = diseq.getLhs().mRepStar == src ? diseq.getRhs() : diseq.getLhs(); + return engine.computeMergeDiseqCycle(srcEnd, destEnd, lhs, this, reason, + reasonDiff(reason, lhs, this), diseq); } - boolean sharedTermConflict = false; - if (diseq == null && src.mSharedTerm != null) { + if (src.mSharedTerm != null) { if (dest.mSharedTerm == null) { dest.mSharedTerm = src.mSharedTerm; } else { final boolean createInLA = dest.mSharedTerm.mFlatTerm.getSort().isNumericSort(); - final CCEquality cceq = engine.createEquality(src.mSharedTerm, dest.mSharedTerm, createInLA); + // value(src.mSharedTerm) - value(dest.mSharedTerm) after the merge: the merge offset delta plus the two + // shared terms' offsets to their (current) representatives. + final Rational sharedOffset = + delta.add(src.mSharedTerm.mOffsetToRep).sub(dest.mSharedTerm.mOffsetToRep); + final CCEquality cceq = + engine.createEquality(src.mSharedTerm, dest.mSharedTerm, sharedOffset, createInLA); /* If cceq cannot be created this is a conflict like merging x+1 and x */ - sharedTermConflict = (cceq == null); + if (cceq == null) { + return engine.computeSharedConflictCycle(src.mSharedTerm, dest.mSharedTerm, lhs, this, reason, + reasonDiff(reason, lhs, this)); + } /* * No need to remember the created equality. It was inserted and will be found later and propagated * automatically. @@ -406,31 +494,26 @@ private Clause mergeInternal(final CClosure engine, final CCTerm lhs, final CCEq lhs.mOldRep = src; src.mReasonLiteral = reason; - /* Check for conflict */ - if (sharedTermConflict || diseq != null) { - final Clause conflict = sharedTermConflict - ? engine.computeCycle(src.mSharedTerm, dest.mSharedTerm) - : engine.computeCycle(diseq); - lhs.mEqualEdge = null; - lhs.mOldRep = null; - src.mReasonLiteral = null; - return conflict; - } - long time; src.mMergeTime = engine.getMergeDepth(); engine.recordMerge(lhs); - engine.getLogger().debug("M %s %s", this, lhs); + engine.getLogger().debug("M %s %s (offset %s)", this, lhs, reasonDiff(reason, this, lhs)); if (Config.PROFILE_TIME) { time = System.nanoTime(); } - engine.rehashSignatures(src, dest, src.mSignatureBackRefs); - /* Update rep fields */ + /* + * Rehash the signatures with delta (computed above): the effective offset of every src-class argument increases + * by delta. Since src is a representative its own offset was ZERO, so after the shift below src.mOffsetToRep == + * delta. Rehashing happens before the offsets are actually updated below. + */ + engine.rehashSignatures(src, dest, delta, src.mSignatureBackRefs); + /* Update rep fields and shift offsets of the merged members into dest's frame */ src.mRep = dest; for (final CCTerm t : src.mMembers) { t.mRepStar = dest; + t.mOffsetToRep = t.mOffsetToRep.add(delta); } if (Config.PROFILE_TIME) { engine.addSetRepTime(System.nanoTime() - time); @@ -446,19 +529,41 @@ private Clause mergeInternal(final CClosure engine, final CCTerm lhs, final CCEq assert pentry.getOtherEntry().mOther == src; final CCTerm other = pentry.mOther; assert other.mRepStar == other; + // offset of this info as seen from src: value(src) - value(other) + final Rational offFromSrc = pentry.getOffsetToOther(); if (other == dest) { - assert (info.mDiseq == null); - for (final CCEquality.Entry eq : info.mEqlits) { - engine.addPending(eq.getCCEquality()); - } - // E-Matching - for (final CompareTrigger trigger : info.mCompareTriggers) { - trigger.activate(); + if (offFromSrc.equals(delta)) { + // The equalities in this info hold at exactly the merge offset, so they are now implied true. + assert (info.mDiseq == null); + for (final CCEquality.Entry eq : info.mEqlits) { + engine.addPending(eq.getCCEquality()); + } + // E-Matching + for (final CompareTrigger trigger : info.mCompareTriggers) { + trigger.activate(); + } + } else { + // These equalities claim an offset different from the merge offset, so they are now implied + // false. We propagate the disequality eagerly here with the merge as its reason; the explanation + // (CClosure.computeAntiCycle, same-class branch) reconstructs it from the path between the two + // terms, so no separating disequality atom is involved and mDiseqReason stays null. An equality + // here cannot already be true, as that would mean src and dest are already in the same class. + for (final CCEquality.Entry eq : info.mEqlits) { + final CCEquality cceq = eq.getCCEquality(); + assert cceq.getDecideStatus() != cceq; + if (cceq.getDecideStatus() == null) { + // this is caused by an offset conflict, not by a set disequality + cceq.mDiseqReason = null; + engine.addPending(cceq.negate()); + } + } } } else { - CCTermPairHash.Info destInfo = engine.mPairHash.getInfo(dest, other); + // Re-key the info from src to dest: value(dest) - value(other) = (value(src) - value(other)) - delta. + final Rational destOffset = offFromSrc.sub(delta); + CCTermPairHash.Info destInfo = engine.mPairHash.getInfo(dest, other, destOffset); if (destInfo == null) { - destInfo = new CCTermPairHash.Info(dest, other); + destInfo = new CCTermPairHash.Info(dest, other, destOffset); engine.mPairHash.add(destInfo); } //System.err.println("M "+src+" "+other+" "+dest); @@ -468,7 +573,7 @@ private Clause mergeInternal(final CClosure engine, final CCTerm lhs, final CCEq for (final CCEquality.Entry eq : destInfo.mEqlits) { assert eq.getCCEquality().getDecideStatus() != eq.getCCEquality(); if (eq.getCCEquality().getDecideStatus() == null) { - eq.getCCEquality().mDiseqReason = info.mDiseq; + eq.getCCEquality().setDiseqReason(info.mDiseq); engine.addPending(eq.getCCEquality().negate()); } } @@ -476,7 +581,7 @@ private Clause mergeInternal(final CClosure engine, final CCTerm lhs, final CCEq for (final CCEquality.Entry eq : info.mEqlits) { assert eq.getCCEquality().getDecideStatus() != eq.getCCEquality(); if (eq.getCCEquality().getDecideStatus() == null) { - eq.getCCEquality().mDiseqReason = destInfo.mDiseq; + eq.getCCEquality().setDiseqReason(destInfo.mDiseq); engine.addPending(eq.getCCEquality().negate()); } } @@ -512,7 +617,7 @@ private Clause mergeInternal(final CClosure engine, final CCTerm lhs, final CCEq } public void undoMerge(final CClosure engine, final CCTerm lhs) { - engine.getLogger().debug("U %s %s", lhs, this); + engine.getLogger().debug("U %s %s (offset %s)", lhs, this, mOldRep == null ? null : mOldRep.mOffsetToRep); long time; CCTerm src, dest; @@ -528,9 +633,15 @@ public void undoMerge(final CClosure engine, final CCTerm lhs) { dest = mRepStar; assert src.mRep == dest; + /* + * The delta that was added to every src-class member at merge time is src's current offset relative to dest + * (src was a representative with offset ZERO before the merge). Undoing reverses both the representative change + * (dest -> src) and the offset shift (by -delta). Rehash before the offsets below are restored. + */ + final Rational delta = src.mOffsetToRep; dest.mSignatureBackRefs.unjoinList(src.mSignatureBackRefs); engine.getLogger().debug("Unmerge Backrefs: %s", src.mSignatureBackRefs); - engine.rehashSignatures(dest, src, src.mSignatureBackRefs); + engine.rehashSignatures(dest, src, delta.negate(), src.mSignatureBackRefs); src.mReasonLiteral = null; for (final CCTermPairHash.Info.Entry pentry : src.mPairInfos.reverse()) { @@ -543,7 +654,9 @@ public void undoMerge(final CClosure engine, final CCTerm lhs) { final CCTerm other = pentry.mOther; assert other.mRepStar == other; if (other != dest) { - final CCTermPairHash.Info destInfo = engine.mPairHash.getInfo(dest, other); + // Mirror the merge re-keying: the merged info lived at value(dest) - value(other) = offFromSrc - delta. + final Rational destOffset = pentry.getOffsetToOther().sub(delta); + final CCTermPairHash.Info destInfo = engine.mPairHash.getInfo(dest, other, destOffset); if (destInfo == null) { continue; } @@ -567,9 +680,14 @@ public void undoMerge(final CClosure engine, final CCTerm lhs) { if (Config.PROFILE_TIME) { time = System.nanoTime(); } + /* + * Undo the offset shift applied at merge (delta computed above). Subtracting it restores each member's offset + * relative to src and resets src.mOffsetToRep to ZERO. + */ dest.mMembers.unjoinList(src.mMembers); for (final CCTerm t : src.mMembers) { t.mRepStar = src; + t.mOffsetToRep = t.mOffsetToRep.sub(delta); } src.mRep = src; @@ -600,6 +718,7 @@ public int hashCode() { return mHashCode; } + @Override public Term getFlatTerm() { return mFlatTerm; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTermPairHash.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTermPairHash.java index eabcda27d..ea7e2c3a1 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTermPairHash.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CCTermPairHash.java @@ -18,6 +18,7 @@ */ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.SimpleList; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.SimpleListable; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.CuckooHashSet; @@ -26,6 +27,14 @@ public class CCTermPairHash extends CuckooHashSet { public final static class Info { CCEquality mDiseq; + /** + * The offset of this pair info: it relates the two endpoints {@code A = mRhsEntry.mOther} and + * {@code B = mLhsEntry.mOther} by {@code value(A) == value(B) + mOffset}. Several infos with the same endpoints + * but different offsets may coexist (e.g. for the equalities {@code a == b} and {@code a == b + 5}); they are + * distinguished by this offset both in the pair hash key and in the targeted lookups. For plain (offset-free) + * congruence closure this is always {@link Rational#ZERO}. + */ + Rational mOffset; final SimpleList mEqlits; final Entry mLhsEntry, mRhsEntry; final SimpleList mCompareTriggers; // E-Matching @@ -46,6 +55,15 @@ Entry getOtherEntry() { ? mRhsEntry : mLhsEntry; } + /** + * The offset of the info as seen from the owner of this entry towards {@link #mOther}, i.e. + * {@code value(owner) - value(mOther)}. This is {@code +mOffset} for the lhs entry (owner {@code A}) and + * {@code -mOffset} for the rhs entry (owner {@code B}). + */ + Rational getOffsetToOther() { + return mLhsEntry == this ? mOffset : mOffset.negate(); + } + @Override public String toString() { return Info.this.toString(); @@ -53,8 +71,13 @@ public String toString() { } public Info(CCTerm l, CCTerm r) { + this(l, r, Rational.ZERO); + } + + public Info(CCTerm l, CCTerm r, Rational offset) { mLhsEntry = new Entry(r); mRhsEntry = new Entry(l); + mOffset = offset; l.mPairInfos.append(mLhsEntry); r.mPairInfos.append(mRhsEntry); mEqlits = new SimpleList(); @@ -63,17 +86,27 @@ public Info(CCTerm l, CCTerm r) { @Override public int hashCode() { - return pairHash(mRhsEntry.mOther, mLhsEntry.mOther); + return pairHash(mRhsEntry.mOther, mLhsEntry.mOther) + ^ offsetHash(mRhsEntry.mOther, mLhsEntry.mOther, mOffset); } - public final boolean equals(CCTerm lhs, CCTerm rhs) { - return (mRhsEntry.mOther == lhs && mLhsEntry.mOther == rhs) - || (mRhsEntry.mOther == rhs && mLhsEntry.mOther == lhs); + public final boolean equals(CCTerm lhs, CCTerm rhs, Rational offset) { + // A = mRhsEntry.mOther, B = mLhsEntry.mOther, mOffset = value(A) - value(B). + if (mRhsEntry.mOther == lhs && mLhsEntry.mOther == rhs) { + return mOffset.equals(offset); + } + if (mRhsEntry.mOther == rhs && mLhsEntry.mOther == lhs) { + return mOffset.equals(offset.negate()); + } + return false; } @Override public String toString() { - return "Info[" + mLhsEntry.mOther + "," + mRhsEntry.mOther + "]"; + if (mOffset.equals(Rational.ZERO)) { + return "Info[" + mLhsEntry.mOther + "," + mRhsEntry.mOther + "]"; + } + return "Info[" + mRhsEntry.mOther + "," + mLhsEntry.mOther + "+" + mOffset + "]"; } // public void addExtensionalityDiseq(ConvertFormula converter) { @@ -89,31 +122,65 @@ public boolean isEmpty() { } } - private Info getInfoStash(CCTerm lhs, CCTerm rhs) { - if (mStash != null && mStash.equals(lhs, rhs)) { + private Info getInfoStash(CCTerm lhs, CCTerm rhs, Rational offset) { + if (mStash != null && mStash.equals(lhs, rhs, offset)) { return mStash; } return null; } + /** + * Look up the offset-zero pair info between the two terms. Equivalent to {@link #getInfo(CCTerm, CCTerm, Rational)} + * with a zero offset; used by all call sites that only deal with plain (offset-free) equalities. + */ public Info getInfo(CCTerm lhs, CCTerm rhs) { - final int hash = hashJenkins(pairHash(lhs, rhs)); + return getInfo(lhs, rhs, Rational.ZERO); + } + + /** + * Look up the pair info for the relationship {@code value(lhs) == value(rhs) + offset}. Infos with the same + * endpoints but different offsets are distinct entries, so the offset is part of the lookup key. + */ + public Info getInfo(CCTerm lhs, CCTerm rhs, Rational offset) { + final int hash = hashJenkins(pairHash(lhs, rhs) ^ offsetHash(lhs, rhs, offset)); final int hash1 = hash1(hash); Info bucket = (Info) mBuckets[hash1]; - if (bucket != null && bucket.equals(lhs, rhs)) { + if (bucket != null && bucket.equals(lhs, rhs, offset)) { return bucket; } bucket = (Info) mBuckets[hash2(hash) ^ hash1]; - if (bucket != null && bucket.equals(lhs, rhs)) { + if (bucket != null && bucket.equals(lhs, rhs, offset)) { return bucket; } - return getInfoStash(lhs, rhs); + return getInfoStash(lhs, rhs, offset); } private static int pairHash(CCTerm lhs, CCTerm rhs) { return hashJenkins(lhs.hashCode()) + hashJenkins(rhs.hashCode()); } + /** + * A hash contribution for the offset of an info between {@code a} and {@code b} with {@code offset == value(a) - + * value(b)}. The offset is expressed in a canonical endpoint orientation (the term with the smaller + * {@link CCTerm#hashCode()} first) before hashing, so that the relationship {@code (a, b, off)} and its mirror + * {@code (b, a, -off)} hash identically, while {@code (a, b, off)} and {@code (a, b, -off)} (i.e. {@code a = b + off} + * versus {@code a = b - off}) get different hashes — important because cuckoo hashing degrades badly on structured + * collisions. We use {@code hashCode()} rather than identity hashes to keep the solver deterministic. + * + *

If the two endpoints have equal hash codes (about a one in four billion chance) the orientation is ambiguous, + * so we fall back to the negation-invariant {@code offset.abs()} hash. That keeps the mirror consistent (at the + * cost of {@code off}/{@code -off} colliding only in that rare case). + */ + static int offsetHash(CCTerm a, CCTerm b, Rational offset) { + final int ha = a.hashCode(); + final int hb = b.hashCode(); + if (ha == hb) { + return offset.abs().hashCode(); + } + final Rational canonical = ha < hb ? offset : offset.negate(); + return canonical.hashCode(); + } + public void removePairInfo(Info info) { // First remove this pair from the pairInfos-lists in the components info.mLhsEntry.removeFromList(); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CClosure.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CClosure.java index 756daf992..f32f21293 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CClosure.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CClosure.java @@ -21,14 +21,17 @@ import java.security.Signature; import java.util.ArrayDeque; import java.util.ArrayList; +import java.util.Collection; import java.util.Collections; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import de.uni_freiburg.informatik.ultimate.logic.ApplicationTerm; import de.uni_freiburg.informatik.ultimate.logic.ConstantTerm; import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.logic.Term; import de.uni_freiburg.informatik.ultimate.logic.Theory; import de.uni_freiburg.informatik.ultimate.smtinterpol.Config; @@ -53,6 +56,7 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.util.ScopedArrayList; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.SymmetricPair; import de.uni_freiburg.informatik.ultimate.util.datastructures.ScopedHashMap; +import de.uni_freiburg.informatik.ultimate.util.datastructures.UnifyHash; /** * This class implements the theory of equality, a.k.a. congruence closure. @@ -186,6 +190,12 @@ public class CClosure implements ITheory { * Todo list of deferred signatures. */ final SimpleList mSignatureTodo = new SimpleList(); + /** + * The master reverse triggers of this engine, one per (function symbol, argument position) pair, see + * {@link MasterReverseTrigger#of}. This must be per engine (not a global unifier): solver instances may share the + * theory and thus the function symbols, but each engine needs its own find trigger registration. + */ + private final UnifyHash mMasterReverseTriggers = new UnifyHash<>(); private long mInvertEdgeTime, mEqTime, mCcTime, mSetRepTime, mSigHashTime; private long mCcCount, mMergeCount; @@ -194,6 +204,10 @@ public CClosure(final Clausifier clausifier) { mClausifier = clausifier; } + UnifyHash getMasterReverseTriggers() { + return mMasterReverseTriggers; + } + public DPLLEngine getEngine() { return mClausifier.getEngine(); } @@ -206,6 +220,24 @@ public boolean isProofGenerationEnabled() { return getEngine().isProofGenerationEnabled(); } + /** + * Whether offset equalities are created. When enabled, arithmetic terms are turned into offset-free CCTerms (with + * the constant carried as an offset) and equalities between numeric terms become offset equalities. When disabled, + * the classic offset-free-less behaviour is used (every offset is zero). + * + * This is currently forced off while proof generation is enabled, until the proof machinery understands offset + * equalities (a later increment). + */ + private boolean mOffsetEqualities = true; + + public boolean createOffsetEqualities() { + return mOffsetEqualities;// && !isProofGenerationEnabled(); + } + + public void setOffsetEqualities(final boolean enabled) { + mOffsetEqualities = enabled; + } + public CCTerm createAnonTerm(final Term term) { final CCTerm ccTerm = new CCBaseTerm(term); mAllTerms.add(ccTerm); @@ -275,7 +307,12 @@ private int getMergeStackDepth(CCTerm t1, CCTerm t2) { } } - public CCTerm createAppTerm(FunctionSymbol func, final CCTerm[] args, final SourceAnnotation source) { + /** + * Create a function application CCTerm. Each argument is a {@link CCParameter}, i.e. a CCTerm together with a + * constant offset, so the actual argument is {@code args[i].getCCTerm() + args[i].getOffset()} (the offset carries + * e.g. the {@code +5} in {@code f(x+5)}). Offset-free arguments are bare {@link CCTerm}s. + */ + public CCTerm createAppTerm(FunctionSymbol func, final CCParameter[] args, final SourceAnnotation source) { assert args.length > 0; if (args.length > 0) { final CCAppTerm term = new CCAppTerm(func, args, this, source.isFromQuantTheory()); @@ -316,24 +353,124 @@ public List getAllFuncApps(final FunctionSymbol sym) { return parents; } + /** Key of a numeric clash slot: a (function symbol, argument position) pair. */ + private static final class ClashKey { + final FunctionSymbol mSym; + final int mPos; + + ClashKey(final FunctionSymbol sym, final int pos) { + mSym = sym; + mPos = pos; + } + + @Override + public int hashCode() { + return mSym.hashCode() * 31 + mPos; + } + + @Override + public boolean equals(final Object other) { + if (!(other instanceof ClashKey)) { + return false; + } + final ClashKey key = (ClashKey) other; + return mSym == key.mSym && mPos == key.mPos; + } + } + + /** + * Enumerate the numeric clash slots for model-based theory combination, on demand. A clash slot groups the + * {@link CCParameter}s occupying one (function symbol, argument position) whose argument sort is numeric. Two + * members of the same slot with equal value but in distinct congruence classes are an equality that MBTC may + * propose: merging them is what makes the two applications congruent at that position. Only members whose class + * has a linear-arithmetic value ({@code getRepresentative().getSharedTerm() != null}) are kept — a class + * without a shared term is free to receive a non-clashing value at model construction, so it cannot be forced to + * clash. + *

+ * MBTC runs only in {@code finalCheck}, so the slots are computed on demand here rather than maintained in a + * persistent, backtracked index; the signature todo queue is drained at every checkpoint, so all installed + * reverse triggers are in the signature map by then. + *

+ * Two sources are enumerated: the congruence source (every function-application argument position) and the + * reverse-trigger source — an installed reverse trigger (e.g. from e-matching) watches a (symbol, + * position) for applications whose argument has the watched value, so that value is a slot member, too: an MBTC + * merge that moves an argument onto it activates the trigger. The deferred datatype numeric-field feed is still + * future work. + * + * @return the slots as lists of members; each list holds the numeric, LA-valued members at one (symbol, position). + */ + public Collection> getNumericClashSlots() { + final Map> slots = new LinkedHashMap<>(); + for (final CCTerm term : mAllTerms) { + if (!(term instanceof CCAppTerm)) { + continue; + } + final CCAppTerm app = (CCAppTerm) term; + final FunctionSymbol sym = app.getFunctionSymbol(); + for (int pos = 0; pos < app.getArgCount(); pos++) { + addClashMember(slots, sym, pos, app.getArgParam(pos)); + } + } + for (final SignatureTrigger sigTrigger : mSignatureTriggers.values()) { + if (!(sigTrigger instanceof ReverseTriggerTrigger)) { + continue; + } + for (final ReverseTrigger trigger : ((ReverseTriggerTrigger) sigTrigger).getTriggers()) { + final CCParameter arg = trigger.getArgument(); + if (arg != null && trigger.getArgPosition() >= 0) { + addClashMember(slots, trigger.getFunctionSymbol(), trigger.getArgPosition(), arg); + } + } + } + return slots.values(); + } + + /** + * Add one member to its clash slot, filtering to numeric, LA-valued members (see {@link #getNumericClashSlots}). + */ + private static void addClashMember(final Map> slots, final FunctionSymbol sym, + final int pos, final CCParameter arg) { + if (!arg.getCCTerm().getFlatTerm().getSort().isNumericSort()) { + return; + } + if (arg.getRepresentative().getSharedTerm() == null) { + return; + } + List members = slots.get(new ClashKey(sym, pos)); + if (members == null) { + members = new ArrayList<>(); + slots.put(new ClashKey(sym, pos), members); + } + members.add(arg); + } + /** * Insert a Compare trigger that will be activated as soon as the two given - * CCTerms are equal. It is inserted into the pair hash tables and all - * intermediate pair infos. + * values are equal. It is inserted into the pair hash tables and all + * intermediate pair infos, keyed on the offset between the two base terms + * (mirroring {@link #insertEqualityEntry}). The two values may already be in + * the same congruence class at a different offset; the trigger is then parked + * in the merge-boundary pair info, which becomes live again when the merge is + * undone. * - * @param t1 the first CCTerm. - * @param t2 the second CCTerm. + * @param lhs the first value. + * @param rhs the second value. * @param trigger the Compare trigger. */ - public void insertCompareTrigger(CCTerm t1, CCTerm t2, final CompareTrigger trigger) { - assert t1.getRepresentative() != t2.getRepresentative(); + public void insertCompareTrigger(final CCParameter lhs, final CCParameter rhs, final CompareTrigger trigger) { + assert !lhs.sameValueAs(rhs); assert !trigger.inList(); + CCTerm t1 = lhs.getCCTerm(); + CCTerm t2 = rhs.getCCTerm(); + // The trigger fires when value(lhs) == value(rhs), i.e. value(t1) - value(t2) == offset. + Rational offset = rhs.getOffset().sub(lhs.getOffset()); while (true) { // make t1 the term that was merged before t2 was merged. if (t1.mMergeTime > t2.mMergeTime) { final CCTerm tmp = t1; t1 = t2; t2 = tmp; + offset = offset.negate(); } // if t1 is its own representative, then t2 should also be the representative @@ -341,9 +478,9 @@ public void insertCompareTrigger(CCTerm t1, CCTerm t2, final CompareTrigger trig if (t1.mRep == t1) { assert t2.mRep == t2; // Insert this entry into the pair hash, create it if necessary. - CCTermPairHash.Info info = mPairHash.getInfo(t1, t2); + CCTermPairHash.Info info = mPairHash.getInfo(t1, t2, offset); if (info == null) { - info = new CCTermPairHash.Info(t1, t2); + info = new CCTermPairHash.Info(t1, t2, offset); mPairHash.add(info); } info.mCompareTriggers.prependIntoJoined(trigger, true); @@ -351,24 +488,31 @@ public void insertCompareTrigger(CCTerm t1, CCTerm t2, final CompareTrigger trig } // find the pair info entry in the pair info list of t1 or create a new one. - assert t1.mRep != t2; + // isLast is set if t1 was merged with t2; in this case the compare trigger + // lists were not joined. + final boolean isLast = t1.mRep == t2; boolean found = false; for (final CCTermPairHash.Info.Entry pentry : t1.mPairInfos) { final CCTermPairHash.Info info = pentry.getInfo(); // info might have blocked compare triggers but no eqlits // assert (!info.eqlits.isEmpty()); - if (pentry.mOther == t2) { - info.mCompareTriggers.prependIntoJoined(trigger, false); + if (pentry.mOther == t2 && pentry.getOffsetToOther().equals(offset)) { + info.mCompareTriggers.prependIntoJoined(trigger, isLast); found = true; break; } } if (!found) { // we need to create a new entry. - final CCTermPairHash.Info info = new CCTermPairHash.Info(t1, t2); + final CCTermPairHash.Info info = new CCTermPairHash.Info(t1, t2, offset); info.mRhsEntry.unlink(); - info.mCompareTriggers.prependIntoJoined(trigger, false); + info.mCompareTriggers.prependIntoJoined(trigger, isLast); + } + if (isLast) { + break; } + // walk t1 up to its representative; value(t1) - value(t1.mRep) = t1.mOffsetToRep - t1.mRep.mOffsetToRep + offset = offset.sub(t1.getOffsetToRep()).add(t1.mRep.getOffsetToRep()); t1 = t1.mRep; } } @@ -377,8 +521,8 @@ public void insertCompareTrigger(CCTerm t1, CCTerm t2, final CompareTrigger trig * Remove a given Compare trigger. */ public void removeCompareTrigger(final CompareTrigger trigger) { - CCTerm t1 = trigger.getLhs(); - CCTerm t2 = trigger.getRhs(); + CCTerm t1 = trigger.getLhs().getCCTerm(); + CCTerm t2 = trigger.getRhs().getCCTerm(); if (!mAllTerms.contains(t1) || !mAllTerms.contains(t2)) { return; // FIXME This is a workaround for the problem that pop() first removes terms, // then triggers, as it @@ -386,41 +530,46 @@ public void removeCompareTrigger(final CompareTrigger trigger) { // where the // corresponding terms have already been removed. } + Rational offset = trigger.getRhs().getOffset().sub(trigger.getLhs().getOffset()); while (true) { // make t1 the term that was merged before t2 was merged. if (t1.mMergeTime > t2.mMergeTime) { final CCTerm tmp = t1; t1 = t2; t2 = tmp; + offset = offset.negate(); } // if t1 is its own representative, then t2 should also be the representative // because of merge time if (t1.mRep == t1) { assert t2.mRep == t2; - // Insert this entry into the pair hash, create it if necessary. - final CCTermPairHash.Info info = mPairHash.getInfo(t1, t2); + final CCTermPairHash.Info info = mPairHash.getInfo(t1, t2, offset); assert info != null; info.mCompareTriggers.undoPrependIntoJoined(trigger, true); break; } - // find the pair info entry in the pair info list of t1 or create a new one. - // isLast is set if t1 was merged with t2; in this case the equality entry lists - // were not joined. - assert t1.mRep != t2; + // find the pair info entry in the pair info list of t1. + // isLast is set if t1 was merged with t2; in this case the compare trigger + // lists were not joined. + final boolean isLast = t1.mRep == t2; boolean found = false; for (final CCTermPairHash.Info.Entry pentry : t1.mPairInfos) { final CCTermPairHash.Info info = pentry.getInfo(); // info might have blocked compare triggers but no eqlits // assert (!info.eqlits.isEmpty()); - if (pentry.mOther == t2) { - info.mCompareTriggers.undoPrependIntoJoined(trigger, false); + if (pentry.mOther == t2 && pentry.getOffsetToOther().equals(offset)) { + info.mCompareTriggers.undoPrependIntoJoined(trigger, isLast); found = true; break; } } assert found; + if (isLast) { + break; + } + offset = offset.sub(t1.getOffsetToRep()).add(t1.mRep.getOffsetToRep()); t1 = t1.mRep; } } @@ -457,15 +606,15 @@ public void removeSignatureBackRef(CCTerm arg, final SignatureBackRef backref) { /** * Insert a Reverse trigger that will be activated as soon as a new function - * application of the given function symbol with a given argument at a given - * position exists. + * application of the given function symbol with a given argument value at a + * given position exists. * * @param fSym the function symbol. - * @param arg the argument the new term should contain. + * @param arg the argument value the new term should contain; must equal {@code trigger.getArgument()}. * @param argPos the position of this argument. * @param trigger the Reverse trigger. */ - public void insertReverseTrigger(final FunctionSymbol sym, CCTerm arg, final int argPos, + public void insertReverseTrigger(final FunctionSymbol sym, final CCParameter arg, final int argPos, final ReverseTrigger trigger) { assert trigger.mSignatureTrigger == null; final MasterReverseTrigger masterTrigger = MasterReverseTrigger.of(this, sym, argPos); @@ -553,26 +702,30 @@ public void removeSignatureHash(SignatureTrigger signatureTrigger) { } /** - * Find the representative CCTerm for the given term. This function does not + * Find the representative value for the given term. This function does not * create new terms. If there is no equivalent CCTerm, it returns null. If a * term that is congruent to the given term already exists, it will return the - * representative of this congruent term. + * value of this congruent term, canonicalized as the class representative plus + * the value's offset to it. * * @param term The term which a representative is searched for. - * @return The representative, or null if no congruent term exists in the + * @return The representative value, or null if no congruent term exists in the * CClosure. */ - public CCTerm getCCTermRep(final Term term) { - if (mAnonTerms.containsKey(term)) { - return mAnonTerms.get(term).getRepresentative(); + public CCParameter getCCParamRep(final Term term) { + final Rational constant = mClausifier.getTermConstant(term); + final Term offsetFree = constant.equals(Rational.ZERO) ? term : mClausifier.getOffsetFreeTerm(term); + if (mAnonTerms.containsKey(offsetFree)) { + final CCTerm ccTerm = mAnonTerms.get(offsetFree); + return CCParameter.of(ccTerm.getRepresentative(), ccTerm.getOffsetToRep().add(constant)); } - if (term instanceof ApplicationTerm) { - final ApplicationTerm at = (ApplicationTerm) term; + if (offsetFree instanceof ApplicationTerm) { + final ApplicationTerm at = (ApplicationTerm) offsetFree; final FunctionSymbol funcSym = at.getFunction(); final Term[] params = at.getParameters(); - final CCTerm[] argReps = new CCTerm[params.length]; + final CCParameter[] argReps = new CCParameter[params.length]; for (int i = 0; i < params.length; i++) { - argReps[i] = getCCTermRep(params[i]); + argReps[i] = getCCParamRep(params[i]); if (argReps[i] == null) { return null; } @@ -580,30 +733,40 @@ public CCTerm getCCTermRep(final Term term) { final SignatureTrigger sig = new SignatureTrigger(funcSym, argReps); final CongruenceTrigger congTrigger = (CongruenceTrigger) mSignatureTriggers.get(sig); if (congTrigger != null) { - return congTrigger.getApp().getRepresentative(); + final CCAppTerm app = congTrigger.getApp(); + return CCParameter.of(app.getRepresentative(), app.getOffsetToRep().add(constant)); } } return null; } /** - * For two given CCTerms, check if the equality is set. + * For two given values, check if the equality is set. * - * @return true if the terms are in the same congruence class, false otherwise. + * @return true if the values are currently equal (same congruence class at the same offset), false otherwise. */ - public boolean isEqSet(final CCTerm first, final CCTerm second) { - return first.getRepresentative() == second.getRepresentative(); + public boolean isEqSet(final CCParameter first, final CCParameter second) { + return first.sameValueAs(second); } /** - * For two given CCTerms, check if the disequality is set. + * For two given values, check if the disequality is known to hold, either because a disequality literal is set or + * because the values are in the same congruence class at different offsets (so they provably differ by a non-zero + * constant). * - * @return true if the disequality is set, false otherwise. + * @return true if the disequality holds, false otherwise. */ - public boolean isDiseqSet(final CCTerm first, final CCTerm second) { + public boolean isDiseqSet(final CCParameter first, final CCParameter second) { final CCTerm firstRep = first.getRepresentative(); final CCTerm secondRep = second.getRepresentative(); - final CCTermPairHash.Info diseqInfo = mPairHash.getInfo(firstRep, secondRep); + final Rational offset = second.getOffsetToRep().sub(first.getOffsetToRep()); + if (firstRep == secondRep) { + // Same class: the value difference is the known constant offset. + return !offset.equals(Rational.ZERO); + } + // A diseq disproves value(first) == value(second), i.e. value(firstRep) - value(secondRep) == + // second.mOffsetToRep - first.mOffsetToRep. + final CCTermPairHash.Info diseqInfo = mPairHash.getInfo(firstRep, secondRep, offset); return diseqInfo != null && diseqInfo.mDiseq != null; } @@ -616,13 +779,15 @@ public boolean isDiseqSet(final CCTerm first, final CCTerm second) { * @param eqentry the equality entry that should be inserted into the pair * infos. */ - public void insertEqualityEntry(CCTerm t1, CCTerm t2, final CCEquality.Entry eqentry) { + public void insertEqualityEntry(CCTerm t1, CCTerm t2, Rational offset, final CCEquality.Entry eqentry) { + // offset == value(t1) - value(t2) while (true) { // make t1 the term that was merged before t2 was merged. if (t1.mMergeTime > t2.mMergeTime) { final CCTerm tmp = t1; t1 = t2; t2 = tmp; + offset = offset.negate(); } // if t1 is its own representative, then t2 should also be the representative @@ -630,9 +795,9 @@ public void insertEqualityEntry(CCTerm t1, CCTerm t2, final CCEquality.Entry eqe if (t1.mRep == t1) { assert t2.mRep == t2; // Insert this entry into the pair hash, create it if necessary. - CCTermPairHash.Info info = mPairHash.getInfo(t1, t2); + CCTermPairHash.Info info = mPairHash.getInfo(t1, t2, offset); if (info == null) { - info = new CCTermPairHash.Info(t1, t2); + info = new CCTermPairHash.Info(t1, t2, offset); mPairHash.add(info); } info.mEqlits.prependIntoJoined(eqentry, true); @@ -648,7 +813,7 @@ public void insertEqualityEntry(CCTerm t1, CCTerm t2, final CCEquality.Entry eqe final CCTermPairHash.Info info = pentry.getInfo(); // info might have blocked compare triggers but no eqlits // assert (!info.eqlits.isEmpty()); - if (pentry.mOther == t2) { + if (pentry.mOther == t2 && pentry.getOffsetToOther().equals(offset)) { info.mEqlits.prependIntoJoined(eqentry, isLast); found = true; break; @@ -656,18 +821,27 @@ public void insertEqualityEntry(CCTerm t1, CCTerm t2, final CCEquality.Entry eqe } if (!found) { // we need to create a new entry. - final CCTermPairHash.Info info = new CCTermPairHash.Info(t1, t2); + final CCTermPairHash.Info info = new CCTermPairHash.Info(t1, t2, offset); info.mRhsEntry.unlink(); info.mEqlits.prependIntoJoined(eqentry, isLast); } if (isLast) { break; } + // walk t1 up to its representative; value(t1) - value(t1.mRep) = t1.mOffsetToRep - t1.mRep.mOffsetToRep + offset = offset.sub(t1.getOffsetToRep()).add(t1.mRep.getOffsetToRep()); t1 = t1.mRep; } } - public CCEquality createCCEquality(final int stackLevel, CCTerm t1, CCTerm t2) { + public CCEquality createCCEquality(final int stackLevel, final CCTerm t1, final CCTerm t2) { + return createCCEquality(stackLevel, t1, t2, Rational.ZERO); + } + + /** + * Create a CCEquality stating {@code value(t1) == value(t2) + offset}. + */ + public CCEquality createCCEquality(final int stackLevel, CCTerm t1, CCTerm t2, Rational offset) { assert (t1 != t2); CCEquality eq = null; assert t1.invariant(); @@ -679,9 +853,10 @@ public CCEquality createCCEquality(final int stackLevel, CCTerm t1, CCTerm t2) { final CCTerm tmp = t2; t2 = t1; t1 = tmp; + offset = offset.negate(); } - eq = new CCEquality(stackLevel, t1, t2); - insertEqualityEntry(t1, t2, eq.getEntry()); + eq = new CCEquality(stackLevel, t1, t2, offset); + insertEqualityEntry(t1, t2, offset, eq.getEntry()); getEngine().addAtom(eq); assert t1.invariant(); @@ -690,18 +865,27 @@ public CCEquality createCCEquality(final int stackLevel, CCTerm t1, CCTerm t2) { assert t2.pairHashValid(this); if (t1.mRepStar == t2.mRepStar) { - if (getLogger().isDebugEnabled()) { - getLogger().debug("CC-Prop: " + eq + " repStar: " + t1.mRepStar); + // The two terms are already in the same class. The equality is implied true only if the offset matches the + // offset they already have; otherwise it is implied false but we leave it for the SAT solver (setLiteral + // detects the conflict if it is ever set true). + final Rational existingDiff = t1.getOffsetToRep().sub(t2.getOffsetToRep()); + if (existingDiff.equals(offset)) { + if (getLogger().isDebugEnabled()) { + getLogger().debug("CC-Prop: " + eq + " repStar: " + t1.mRepStar); + } + mPendingLits.add(eq); + mRecheckOnBacktrackLits.add(eq); } - mPendingLits.add(eq); - mRecheckOnBacktrackLits.add(eq); } else { - final CCEquality diseq = mPairHash.getInfo(t1.mRepStar, t2.mRepStar).mDiseq; + // value(t1Rep) - value(t2Rep) under the equality + final Rational repOffset = offset.sub(t1.getOffsetToRep()).add(t2.getOffsetToRep()); + final CCTermPairHash.Info repInfo = mPairHash.getInfo(t1.mRepStar, t2.mRepStar, repOffset); + final CCEquality diseq = repInfo == null ? null : repInfo.mDiseq; if (diseq != null) { if (getLogger().isDebugEnabled()) { getLogger().debug("CC-Prop: " + eq.negate() + " diseq: " + diseq); } - eq.mDiseqReason = diseq; + eq.setDiseqReason(diseq); mPendingLits.add(eq.negate()); mRecheckOnBacktrackLits.add(eq.negate()); } @@ -731,6 +915,8 @@ public Clause finalCheck() { public Literal getPropagatedLiteral() { final Literal lit = mPendingLits.poll(); assert (lit == null || checkPending(lit)); + // Lazy explanation: the reason for a propagated disequality is computed on demand in getUnitClause / + // computeAntiCycle (which no longer mutates the graph and orients the diseq from eq.mDiseqOrientation). return lit; } @@ -752,7 +938,7 @@ public Clause getUnitClause(final Literal lit) { } else { /* ComputeAntiCycle */ final CCEquality eq = (CCEquality) lit.negate(); - return computeAntiCycle(eq); + return computeAntiCycle(eq.mDiseqReason, eq.mDiseqOrientation, eq); } } @@ -794,10 +980,11 @@ public void moveToSignatureTodo(SignatureTrigger signatureTrigger) { * @param backRefs the list of (signature, listIndex, trigger) back-refs from * that representative. */ - void rehashSignatures(final CCTerm oldRep, final CCTerm newRep, final SimpleList backRefs) { + void rehashSignatures(final CCTerm oldRep, final CCTerm newRep, final Rational offsetDelta, + final SimpleList backRefs) { for (final SignatureBackRef backRef : backRefs) { final SignatureTrigger signatureTrigger = backRef.getSignatureTrigger(); - signatureTrigger.rehash(this, backRef.getArgPosition(), oldRep, newRep); + signatureTrigger.rehash(this, backRef.getArgPosition(), oldRep, newRep, offsetDelta); } } @@ -836,6 +1023,15 @@ public Clause setLiteral(final Literal literal) { if (conflict != null) { return conflict; } + } else { + // The terms are already in the same class. Asserting the equality is a conflict if their actual offset + // differs from the offset claimed by the equality (e.g. asserting x == x + 1). The conflict clause is + // {¬eq, ¬path}: eq is true, so ¬eq is false, and the path literals are all true, so the clause is + // falsified. (computeCycle would put eq positively and yield a satisfied clause.) + final Rational existingDiff = eq.getLhs().getOffsetToRep().sub(eq.getRhs().getOffsetToRep()); + if (!existingDiff.equals(eq.getOffset())) { + return computeAntiCycle(null, false, eq); + } } } else { final CCTerm left = eq.getLhs().mRepStar; @@ -843,12 +1039,16 @@ public Clause setLiteral(final Literal literal) { /* Check for conflict */ if (left == right) { - final Clause conflict = computeCycle(eq); - if (conflict != null) { - return conflict; + // They are in the same class. The disequality is a conflict only if they are equal at exactly the + // offset the equality claims; if their actual offset differs, the disequality already holds and there + // is nothing to separate. + final Rational existingDiff = eq.getLhs().getOffsetToRep().sub(eq.getRhs().getOffsetToRep()); + if (existingDiff.equals(eq.getOffset())) { + return computeCycle(eq); } + } else { + separate(left, right, eq); } - separate(left, right, eq); } final LAEquality laeq = eq.getLASharedData(); if (laeq != null) { @@ -885,7 +1085,7 @@ private void separate(final CCTerm lhs, final CCTerm rhs, final CCEquality diseq return; } } - final CCTermPairHash.Info info = mPairHash.getInfo(lhs, rhs); + final CCTermPairHash.Info info = mPairHash.getInfo(lhs, rhs, repOffset(diseq)); assert info.mDiseq == null; mUndoStack.push(new SepUndoInfo(diseq)); @@ -893,16 +1093,28 @@ private void separate(final CCTerm lhs, final CCTerm rhs, final CCEquality diseq /* Propagate inequalities */ for (final CCEquality.Entry eqentry : info.mEqlits) { final CCEquality eq = eqentry.getCCEquality(); - assert eq.getDecideStatus() == null || eq == diseq; + // eq cannot be decided true here: that would merge lhs and rhs, contradicting that they are distinct + // representatives. With offset equalities it may already be decided false (propagated by an earlier + // offset merge); such an eq keeps its existing reason and is simply skipped. + assert eq.getDecideStatus() != eq || eq == diseq; if (eq.getDecideStatus() == null) { - eq.mDiseqReason = diseq; + eq.setDiseqReason(diseq); addPending(eq.negate()); } } } + /** + * The offset of an equality as seen between the representatives of its two sides, i.e. + * {@code value(eq.getLhs().mRepStar) - value(eq.getRhs().mRepStar)} implied by the equality. + */ + private static Rational repOffset(final CCEquality eq) { + return eq.getOffset().sub(eq.getLhs().getOffsetToRep()).add(eq.getRhs().getOffsetToRep()); + } + private void undoSep(final CCEquality atom) { - final CCTermPairHash.Info destInfo = mPairHash.getInfo(atom.getLhs().mRepStar, atom.getRhs().mRepStar); + final CCTermPairHash.Info destInfo = + mPairHash.getInfo(atom.getLhs().mRepStar, atom.getRhs().mRepStar, repOffset(atom)); assert destInfo != null && destInfo.mDiseq == atom; destInfo.mDiseq = null; } @@ -914,30 +1126,71 @@ public Clause computeCycle(final CCEquality eq) { return res; } - public Clause computeCycle(final CCTerm lconstant, final CCTerm rconstant) { - final CongruencePath congPath = new CongruencePath(this); - return congPath.computeCycle(lconstant, rconstant, isProofGenerationEnabled()); - } - - public Clause computeAntiCycle(final CCEquality eq) { - final CCTerm left = eq.getLhs(); - final CCTerm right = eq.getRhs(); - final CCEquality diseq = eq.mDiseqReason; - assert left.mRepStar != right.mRepStar; - assert diseq.getLhs().mRepStar == left.mRepStar || diseq.getLhs().mRepStar == right.mRepStar; - assert diseq.getRhs().mRepStar == left.mRepStar || diseq.getRhs().mRepStar == right.mRepStar; - - left.invertEqualEdges(this); - left.mEqualEdge = right; - left.mOldRep = left.mRepStar; - assert left.mOldRep.mReasonLiteral == null; - left.mOldRep.mReasonLiteral = eq; - final Clause c = computeCycle(diseq); - assert left.mEqualEdge == right && left.mOldRep == left.mRepStar; - left.mOldRep.mReasonLiteral = null; - left.mOldRep = null; - left.mEqualEdge = null; - return c; + /** + * Compute the conflict clause for a shared-term clash detected during a merge (the merged values of the two classes' + * shared terms are provably distinct, e.g. an integer shared term forced to a non-integer value). See + * {@link CongruencePath#computeMergeConflictCycle}. + */ + public Clause computeSharedConflictCycle(final CCTerm lshared, final CCTerm rshared, final CCTerm lhs, + final CCTerm rhsTerm, final CCEquality reason, final Rational bridgeOff) { + return new CongruencePath(this).computeMergeConflictCycle(lhs, rhsTerm, bridgeOff, reason, lshared, rshared, + null, isProofGenerationEnabled()); + } + + /** + * Compute the conflict clause when a merge of two classes is forbidden by a disequality {@code diseq} registered + * between them at exactly the merge offset. {@code srcEnd}/{@code destEnd} are the two sides of {@code diseq}, + * oriented into the source class (reachable from {@code lhs}) and destination class (reachable from {@code rhsTerm}); + * the path crosses the freshly added (not-yet-united) merge bridge, so it is built as two single-class halves. See + * {@link CongruencePath#computeMergeConflictCycle}. + */ + public Clause computeMergeDiseqCycle(final CCTerm srcEnd, final CCTerm destEnd, final CCTerm lhs, + final CCTerm rhsTerm, final CCEquality reason, final Rational bridgeOff, final CCEquality diseq) { + return new CongruencePath(this).computeMergeConflictCycle(lhs, rhsTerm, bridgeOff, reason, srcEnd, destEnd, + diseq, isProofGenerationEnabled()); + } + + /** + * Compute a conflict explaining incompatibility of eq and diseq. Called when + * asserting eq would create a conflict on merge with the given diseq. + */ + public Clause computeAntiCycle(final CCEquality diseq, final boolean diseqOrientation, final CCEquality eq) { + final CCTerm lhsDiseq; + final CCTerm rhsDiseq; + if (diseq == null) { + // No separating disequality (e.g. x != x+5): the two sides are in the same class at a deviating offset. + assert eq.getLhs().mRepStar == eq.getRhs().mRepStar; + assert !eq.getLhs().getOffsetToRep().sub(eq.getRhs().getOffsetToRep()).equals(eq.getOffset()); + lhsDiseq = eq.getLhs(); + rhsDiseq = eq.getLhs(); + } else { + // A concrete disequality separates eq's two sides. This works whether they are + // still in different classes or were + // merged later: orient diseq from eq's stored orientation (live reps can no + // longer tell the two sides apart after + // a merge), then build the two single-class halves and stitch them across the + // eq bridge. + lhsDiseq = diseqOrientation ? diseq.getLhs() : diseq.getRhs(); + rhsDiseq = diseqOrientation ? diseq.getRhs() : diseq.getLhs(); + } + return new CongruencePath(this).computeMergeConflictCycle(eq.getLhs(), eq.getRhs(), eq.getOffset(), eq, lhsDiseq, + rhsDiseq, diseq, isProofGenerationEnabled()); + } + + /** + * Compute the conflict clause when a congruence merge finds its two function applications already in the same + * congruence class at an offset different from the zero offset that congruence implies (e.g. f(x) and f(y) are + * congruent but the class already records f(x) = f(y) + k for k != 0). A degenerate + * {@link CongruencePath#computeMergeConflictCycle}: the bridge is the congruence ({@code reason == null}, justified by + * the argument equalities) and the endpoints coincide ({@code srcEnd == destEnd == first}), so the destination half + * is the existing class path from {@code second} back to {@code first} and the trivial diseq + * {@code (first@0, first@existingDiff)} is EQ-discharged. + */ + public Clause computeCongruenceAntiCycle(final CCAppTerm first, final CCAppTerm second) { + assert first.getRepresentative() == second.getRepresentative(); + assert first.getFunctionSymbol() == second.getFunctionSymbol(); + return new CongruencePath(this).computeMergeConflictCycle(first, second, Rational.ZERO, null, first, first, null, + isProofGenerationEnabled()); } /** @@ -964,6 +1217,12 @@ private boolean checkPending(final Literal literal) { final CCEquality eq = (CCEquality) literal.negate(); final CCTerm left = eq.getLhs(); final CCTerm right = eq.getRhs(); + if (left.mRepStar == right.mRepStar) { + // Offset disequality: the two sides are in the same class at an offset different from eq's, so eq is + // false with no separating disequality atom (mDiseqReason stays null; the path is the reason). + assert !left.getOffsetToRep().sub(right.getOffsetToRep()).equals(eq.getOffset()); + return true; + } final CCEquality diseq = eq.mDiseqReason; assert left.mRepStar != right.mRepStar; assert diseq.getLhs().mRepStar == left.mRepStar || diseq.getLhs().mRepStar == right.mRepStar; @@ -990,14 +1249,35 @@ public Clause checkpoint() { return buildCongruence(); } - public CCEquality createEquality(final CCTerm t1, final CCTerm t2, final boolean createLAEquality) { - assert t1 != t2; - final EqualityProxy ep = mClausifier.createEqualityProxy(t1.getFlatTerm(), t2.getFlatTerm(), null); + public CCEquality createEquality(final CCParameter t1, final CCParameter t2, final boolean createLAEquality) { + // Use the structural offset (CCParameter.getOffset()), so the created literal is keyed like the original + // clause literals (see CCProofGenerator.collectClauseLiterals). The dynamic getOffsetToRep() must not be used: + // the two parameters need not be in the same congruence class when this literal is created. + return createEquality(t1.getCCTerm(), t2.getCCTerm(), t2.getOffset().sub(t1.getOffset()), createLAEquality); + } + + /** + * Create (and propagate) an equality {@code value(t1) == value(t2) + offset} between two shared terms. The offset is + * encoded into the right-hand term ({@code t2 + offset}) so that the resulting CCEquality carries the offset and the + * linked LAEquality carries the matching constant. With a zero offset this is the classic shared-term equality. + */ + public CCEquality createEquality(final CCTerm t1, final CCTerm t2, final Rational offset, + final boolean createLAEquality) { + assert t1 != t2 || !offset.equals(Rational.ZERO); + final Term lhsTerm = t1.getFlatTerm(); + Term rhsTerm = t2.getFlatTerm(); + if (!offset.equals(Rational.ZERO)) { + // Build the flattened polynomial term t2 + offset. A nested (+ t2 offset) would not be re-parsed + // correctly by Polynomial when t2 is itself a normalized sum (the inner sum is treated as one monomial). + rhsTerm = mClausifier.addConstantToTerm(rhsTerm, offset); + } + final EqualityProxy ep = mClausifier.createEqualityProxy(lhsTerm, rhsTerm, + SourceAnnotation.EMPTY_SOURCE_ANNOT); if (ep == EqualityProxy.getFalseProxy()) { return null; } if (!createLAEquality) { - final Literal res = ep.getLiteral(null); + final Literal res = ep.getLiteral(SourceAnnotation.EMPTY_SOURCE_ANNOT); if (res instanceof CCEquality) { final CCEquality eq = (CCEquality) res; if ((eq.getLhs() == t1 && eq.getRhs() == t2) || (eq.getLhs() == t2 && eq.getRhs() == t1)) { @@ -1005,7 +1285,12 @@ public CCEquality createEquality(final CCTerm t1, final CCTerm t2, final boolean } } } - return ep.createCCEquality(t1.getFlatTerm(), t2.getFlatTerm()); + // t1 and t2 are the offset-free CC nodes; pass them with this call's offset (which may differ from the proxy's + // canonical offset for a scaled equivalent) rather than round-tripping through the synthesized rhsTerm. + // This path creates an LAEquality, so it must only be taken for numeric equalities; a non-numeric equality + // always matches the literal above (a mismatch would indicate a stale atom referencing removed CCTerms). + assert lhsTerm.getSort().isNumericSort(); + return ep.createCCEquality(t1, t2, offset); } @Override @@ -1018,6 +1303,9 @@ public void dumpModel(final LogProxy logger) { String comma = ""; for (final CCTerm t2 : t.mMembers) { sb.append(comma).append(t2); + if (!t2.getOffsetToRep().equals(Rational.ZERO)) { + sb.append("[+").append(t2.getOffsetToRep()).append(']'); + } comma = "="; } logger.info(sb.toString()); @@ -1048,15 +1336,16 @@ private boolean checkCongruence() { if (t1.getRepresentative() != t2.getRepresentative() && t2 instanceof CCAppTerm) { final CCAppTerm a2 = (CCAppTerm) t2; if (a1.getFunctionSymbol() == a2.getFunctionSymbol()) { - final CCTerm[] args1 = a1.mArgs; - final CCTerm[] args2 = a2.mArgs; + final int arity = a1.mArgs.length; int i; - for (i = 0; i < args1.length; i++) { - if (args1[i].getRepresentative() != args2[i].getRepresentative()) { + for (i = 0; i < arity; i++) { + // congruent requires the arguments to have the same value, i.e. same representative AND + // same offset (offset-free arguments are compared as plain representatives). + if (!a1.getArgParam(i).sameValueAs(a2.getArgParam(i))) { break; } } - if (i == args1.length) { + if (i == arity) { getLogger().fatal("Should be congruent: " + t1 + " and " + t2); return false; } @@ -1129,11 +1418,14 @@ public Clause backtrackComplete() { /* check if literal is still implied by the graph */ boolean repropagate = false; if (l.getSign() > 0) { - repropagate = (lhs == rhs); + // implied true only if the terms are in the same class at the equality's offset + repropagate = (lhs == rhs + && eq.getLhs().getOffsetToRep().sub(eq.getRhs().getOffsetToRep()).equals(eq.getOffset())); } else { - final CCEquality diseq = mPairHash.getInfo(lhs, rhs).mDiseq; + final CCTermPairHash.Info info = mPairHash.getInfo(lhs, rhs, repOffset(eq)); + final CCEquality diseq = info == null ? null : info.mDiseq; if (diseq != null) { - eq.mDiseqReason = diseq; + eq.setDiseqReason(diseq); repropagate = true; } } @@ -1235,21 +1527,23 @@ public int getStackDepth() { public void removeAtom(final DPLLAtom atom) { if (atom instanceof CCEquality) { final CCEquality cceq = (CCEquality) atom; - removeCCEquality(cceq.getLhs(), cceq.getRhs(), cceq); + removeCCEquality(cceq.getLhs(), cceq.getRhs(), cceq.getOffset(), cceq); } } - private void removeCCEquality(CCTerm t1, CCTerm t2, final CCEquality eq) { + private void removeCCEquality(CCTerm t1, CCTerm t2, Rational offset, final CCEquality eq) { // TODO Need test for this!!! + // offset == value(t1) - value(t2) if (t1.mMergeTime > t2.mMergeTime) { final CCTerm tmp = t1; t1 = t2; t2 = tmp; + offset = offset.negate(); } if (t1.mRep == t1) { assert t2.mRep == t2; - final CCTermPairHash.Info info = mPairHash.getInfo(t1, t2); + final CCTermPairHash.Info info = mPairHash.getInfo(t1, t2, offset); if (info != null) { // Remove pair hash info info.mEqlits.prepareRemove(eq.getEntry()); @@ -1263,7 +1557,7 @@ private void removeCCEquality(CCTerm t1, CCTerm t2, final CCEquality eq) { boolean found = false; for (final CCTermPairHash.Info.Entry pentry : t1.mPairInfos) { final CCTermPairHash.Info info = pentry.getInfo(); - if (pentry.mOther == t2) { + if (pentry.mOther == t2 && pentry.getOffsetToOther().equals(offset)) { info.mEqlits.prepareRemove(eq.getEntry()); found = true; break; @@ -1273,7 +1567,7 @@ private void removeCCEquality(CCTerm t1, CCTerm t2, final CCEquality eq) { if (isLast) { eq.getEntry().removeFromList(); } else { - removeCCEquality(t1.mRep, t2, eq); + removeCCEquality(t1.mRep, t2, offset.sub(t1.getOffsetToRep()).add(t1.mRep.getOffsetToRep()), eq); } } if (eq.getLASharedData() != null) { @@ -1292,6 +1586,14 @@ private void removeTerm(final CCTerm t) { final CCAppTerm appTerm = (CCAppTerm) t; removeSignature(appTerm.mCongTrigger); removeSignature(appTerm.mFindTrigger); + if (appTerm.mReverseTriggers != null) { + // remove the reverse trigger signatures created for this application (all merges are already + // undone at pop time, so each application entry is back in its own signature). + for (final ReverseTriggerTrigger trigger : appTerm.mReverseTriggers) { + removeSignature(trigger); + } + appTerm.mReverseTriggers = null; + } } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CompareTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CompareTrigger.java index 4cdd0fcbf..4e15c843a 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CompareTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CompareTrigger.java @@ -21,12 +21,15 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.SimpleListable; /** + * A trigger that is activated when two values become equal. The two values are {@link CCParameter}s; the trigger fires + * when their congruence classes are merged such that the values coincide (same representative and same offset to it). + * * @author Tanja Schindler */ public abstract class CompareTrigger extends SimpleListable { - public abstract CCTerm getLhs(); + public abstract CCParameter getLhs(); - public abstract CCTerm getRhs(); + public abstract CCParameter getRhs(); public abstract void activate(); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruencePath.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruencePath.java index 55498ef30..300a8e23d 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruencePath.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruencePath.java @@ -25,6 +25,7 @@ import java.util.LinkedHashSet; import java.util.Set; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.Clause; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.Literal; import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.LeafNode; @@ -57,19 +58,59 @@ public class CongruencePath { public static class SubPath { ArrayList mTermsOnPath; - public SubPath(final CCTerm start) { + public SubPath(final CCParameter start) { this(start, true); } - public SubPath(final CCTerm start, final boolean produceProofs) { + public SubPath(final CCParameter start, final boolean produceProofs) { if (produceProofs) { mTermsOnPath = new ArrayList<>(); - mTermsOnPath.add(start); + mTermsOnPath.add(start.getCCTerm()); } } - public CCTerm[] getTerms() { - return mTermsOnPath.toArray(new CCTerm[mTermsOnPath.size()]); + /** + * The path nodes as {@link CCParameter}s, offset so that every node has the same value as {@code anchor}: + * {@code value(node) + offset == value(anchor) + anchor.getOffset()}. So if {@code x = y+4} and {@code y = z+2}, + * a path rendered with anchor {@code x+2} reads {@code x+2, y+6, z+8}. The relative offsets are intrinsic + * ({@link CCTerm#getOffsetToRep} differences), so the anchor only fixes the absolute base and the result is + * stable under {@link #addSubPath} concatenation. The anchor is typically one of the path's own nodes. + * + *

The {@code anchor} is treated as the start of the path: its term must be one of the path's two + * endpoints, and the result is oriented so that the anchor's term is first (the stored term list may run either + * way, since {@link #mVisited} keys paths by an undirected {@link SymmetricPair}). The anchor's term renders at + * {@code anchor.getOffset()} and the other endpoint follows from the intrinsic relative offsets. + * + *

{@link CCTerm#getOffsetToRep} is relative to each node's own representative, so for two nodes in different + * classes the difference mixes reference frames and yields garbage offsets — the bug behind several earlier + * offset conflicts (a path built over a freshly added, not-yet-united merge bridge). For numeric terms + * we therefore assert the anchor shares the representative of every node; the cross-class conflict builder + * ({@link #computeMergeConflictCycle}) avoids this by rendering each single-class half separately. The only paths + * that genuinely cross classes are non-numeric (weak-array + * paths over distinct strong classes), and a non-numeric term can never carry an offset, so the rendering is + * trivially correct (offset zero) regardless of frame. + */ + public CCParameter[] getParams(final CCParameter anchor) { + final int n = mTermsOnPath.size(); + assert mTermsOnPath.get(0) == anchor.getCCTerm() || mTermsOnPath.get(n - 1) == anchor.getCCTerm() + : "getParams anchor must be a path endpoint"; + // Orient the path so the anchor's term is first. The stored term list may run either way (mVisited keys + // paths undirected), so if the anchor is the stored last node, render in reverse. + final boolean reversed = mTermsOnPath.get(0) != anchor.getCCTerm(); + final CCParameter[] params = new CCParameter[n]; + for (int i = 0; i < n; i++) { + final CCTerm t = mTermsOnPath.get(reversed ? n - 1 - i : i); + assert !t.getFlatTerm().getSort().isNumericSort() + || anchor.getRepresentative() == t.getRepresentative() + : "getParams anchor must share the congruence class of every numeric node"; + params[i] = CCParameter.of(t, anchor.getOffsetToRep().sub(t.getOffsetToRep())); + } + return params; + } + + /** {@link #getParams(CCParameter)} self-anchored at the path's first node (offset 0). */ + public CCParameter[] getParams() { + return getParams(mTermsOnPath.get(0)); } public void addEntry(final CCTerm term, final CCEquality reason) { @@ -102,9 +143,21 @@ public String toString() { } } + /** + * Visited subpaths, keyed by the offset-free end terms. Two requested paths that differ only by a constant + * offset (e.g. {@code x+5 = y+7} and {@code x+7 = y+9}, both for the edge {@code x = y+2}) share one key, so only a + * single subpath is built; consumers (e.g. {@link CCProofGenerator}) absorb the per-use constant difference. + */ final HashMap,SubPath> mVisited; final ArrayDeque mAllPaths; final ArrayDeque> mTodo; + /** + * The subpaths already appended to {@link #mAllPaths}. Persistent across {@link #drainTodo} calls (unlike a + * per-drain set) so a subpath shared between several drains is collected exactly once. {@link WeakCongruencePath} + * drains once per weak/main path, and {@link #computeCongruence} re-enqueues argument pairs unconditionally, so the + * same dependency can resurface in a later drain; this set keeps it from being added twice. + */ + final Set> mCollected; final Set mAllLiterals; public CongruencePath(final CClosure closure) { @@ -113,9 +166,10 @@ public CongruencePath(final CClosure closure) { mAllLiterals = new LinkedHashSet<>(); mTodo = new ArrayDeque<>(); mAllPaths = new ArrayDeque<>(); + mCollected = new HashSet<>(); } - private CCAnnotation createAnnotation(final SymmetricPair diseq) { + private CCAnnotation createAnnotation(final SymmetricPair diseq) { return new CCAnnotation(diseq, mAllPaths, CCAnnotation.RuleKind.CONG); } @@ -148,11 +202,12 @@ private int computeDepth(CCTerm t) { * @param start one of the function application terms. * @param end the other function application term. */ - private void computeCCPath(CCAppTerm start, CCAppTerm end) { - CCTerm[] startArgs = start.getArguments(); - CCTerm[] endArgs = end.getArguments(); - for (int i = 0; i < startArgs.length; i++) { - mTodo.addFirst(new SymmetricPair<>(startArgs[i], endArgs[i])); + private void computeCongruence(CCAppTerm start, CCAppTerm end) { + // Pair the argument values (CCParameters), so the recorded subpath for each argument is anchored at the + // argument's offset, e.g. f(x+2) congruent f(z+8) yields a subpath from x+2 to z+8. This only enqueues the + // argument pairs; the surrounding drain loop ({@link #drainTodo}, run by {@link #computePath}) builds them. + for (int i = 0; i < start.getArgCount(); i++) { + mTodo.addFirst(new SymmetricPair<>(start.getArgParam(i).getCCTerm(), end.getArgParam(i).getCCTerm())); } } @@ -176,9 +231,10 @@ private void computeCCPath(CCAppTerm start, CCAppTerm end) { * @return the sub path from t to end, if proof production is enabled. * Without proof production, this returns null. */ - private SubPath computePathTo(CCTerm t, final CCTerm end) { + private SubPath computePathTo(final CCParameter startParam, final CCTerm end) { final SubPath path = - new SubPath(t, mClosure.isProofGenerationEnabled()); + new SubPath(startParam, mClosure.isProofGenerationEnabled()); + CCTerm t = startParam.getCCTerm(); CCTerm startCongruence = t; while (t != end) { if (t.mOldRep.mReasonLiteral != null) { @@ -187,7 +243,7 @@ private SubPath computePathTo(CCTerm t, final CCTerm end) { * Compute the paths for the func and arg parts and merge into the * interpolation info. */ - computeCCPath((CCAppTerm) startCongruence, (CCAppTerm) t); + computeCongruence((CCAppTerm) startCongruence, (CCAppTerm) t); path.addEntry(t, null); } /* Add the equality literal to conflict set */ @@ -217,22 +273,28 @@ private SubPath computePathTo(CCTerm t, final CCTerm end) { * @param tailNr this gives the (last) formula number of the equality after right. * @param left the left end of the congruence chain that should be evaluated. * @param right the right end of the congruence chain that should be evaluated. + * @return the built path, or {@code null} for a trivial path ({@code left} and {@code right} share the same term). + * + *

This is a pure builder: it does not consult or update the {@link #mVisited} cache. {@link #drainTodo} owns the + * cache — it looks up {@link #mVisited} before calling and stores the result afterwards, so a path requested as a + * standalone subpath is built once and collected through the re-enqueue discipline. Inline grafts (built directly, + * outside the drain, to be spliced into a weak/store path) call this without caching: a later standalone request for + * the same edge therefore rebuilds it through the drain rather than short-circuiting to the already-visited branch + * of {@link #drainTodo} (which would collect it ahead of its still-pending congruence dependencies). The graft's + * dependencies are enqueued either way, so the inlined congruences are still explained. */ - SubPath computePathNonRecursive(final CCTerm left, final CCTerm right) { - /* check for and ignore trivial paths */ - if (left == right) { + SubPath computePathNonRecursive(final CCParameter left, final CCParameter right) { + final CCTerm leftTerm = left.getCCTerm(); + final CCTerm rightTerm = right.getCCTerm(); + /* check for and ignore trivial paths (the offsets coincide for a genuine congruence) */ + if (leftTerm == rightTerm) { return null; } - final SymmetricPair key = new SymmetricPair<>(left, right); - if (mVisited.containsKey(key)) { - return mVisited.get(key); - } - - int leftDepth = computeDepth(left); - int rightDepth = computeDepth(right); - CCTerm ll = left; - CCTerm rr = right; + int leftDepth = computeDepth(leftTerm); + int rightDepth = computeDepth(rightTerm); + CCTerm ll = leftTerm; + CCTerm rr = rightTerm; CCTerm llWithReason = ll, rrWithReason = rr; while (leftDepth > rightDepth) { if (ll.mOldRep.mReasonLiteral != null) { @@ -261,12 +323,11 @@ SubPath computePathNonRecursive(final CCTerm left, final CCTerm right) { assert (ll != null); final SubPath path = computePathTo(left, llWithReason); if (llWithReason != rrWithReason) { - computeCCPath((CCAppTerm)llWithReason, (CCAppTerm)rrWithReason); + computeCongruence((CCAppTerm)llWithReason, (CCAppTerm)rrWithReason); path.addEntry(rrWithReason, null); } final SubPath pathBack = computePathTo(right, rrWithReason); path.addSubPath(pathBack); - mVisited.put(key, path); return path; } @@ -289,26 +350,50 @@ SubPath computePathNonRecursive(final CCTerm left, final CCTerm right) { * @param right * the right end of the congruence chain that should be evaluated. */ - public void computePath(final CCTerm left, final CCTerm right) { - final HashSet> added = new HashSet<>(); - mTodo.add(new SymmetricPair<>(left, right)); + public void computePath(final CCParameter left, final CCParameter right) { + // Only enqueue the path. The caller (a top-level compute*Cycle/Lemma method) calls drainTodo() once after all + // computePath/computeCongruence calls are queued, so a single shared drain builds them all (and dedups subpaths + // shared between several queued ends). + mTodo.add(new SymmetricPair<>(left.getCCTerm(), right.getCCTerm())); + } + + /** + * Process the work list {@link #mTodo} of (sub)paths to compute, building each one and collecting it into + * {@link #mAllPaths} (and its literals into {@link #mAllLiterals}). The top-level compute*Cycle/Lemma methods seed + * the work list via {@link #computePath} (a single pair) and {@link #computeCongruence} (argument pairs), then call + * this once. {@link WeakCongruencePath} drains once per weak/main path (a strong path to be inlined into a weak path + * is instead built directly via {@link #computePathNonRecursive} with {@code store == false}, which returns it + * without adding it to {@link #mVisited} or {@link #mAllPaths}), hence protected. Dedup against {@link #mCollected} + * is persistent, so a subpath shared between drains is appended only once. + * + *

Ordering invariant: a path is collected (appended to {@link #mAllPaths}) only after its congruence + * dependencies, so it precedes the paths explaining its congruences (as the proof generator requires). This holds + * because a freshly seen path takes the {@code path == null} branch: it re-enqueues itself behind the + * dependencies that {@link #computePathNonRecursive} pushes to the front, so those are collected first. The only way + * to bypass this would be to find the path already in {@link #mVisited} on its first pop — which is exactly why + * inline grafts are built with {@code store == false}: a standalone request for the same edge then rebuilds it + * through this branch instead of short-circuiting to the already-collected branch ahead of its dependencies. + */ + protected void drainTodo() { while (!mTodo.isEmpty()) { final SymmetricPair pathEnds = mTodo.removeFirst(); // don't do anything for trivial paths - if (pathEnds.getFirst() == pathEnds.getSecond()) { + if (pathEnds.getFirst().getCCTerm() == pathEnds.getSecond().getCCTerm()) { continue; } - // check if we already visited this path + // check if we already visited this path (keyed offset-free, so offset variants share one subpath) final SubPath path = mVisited.get(pathEnds); if (path == null) { - // if we did not visit it yet, enqueue again for later and visit the path + // if we did not visit it yet, enqueue again for later, build the path and cache it. drainTodo owns the + // mVisited cache; computePathNonRecursive is a pure builder. The pair is non-trivial (checked above), so + // the build never returns null here. mTodo.addFirst(pathEnds); - computePathNonRecursive(pathEnds.getFirst(), pathEnds.getSecond()); + mVisited.put(pathEnds, computePathNonRecursive(pathEnds.getFirst(), pathEnds.getSecond())); } else { // already visited it, so we just add the path now unless we did this earlier - if (added.add(pathEnds)) { + if (mCollected.add(pathEnds)) { mAllPaths.addFirst(path); } } @@ -319,6 +404,7 @@ public Clause computeCycle(final CCEquality eq, final boolean produceProofs) { final CCTerm lhs = eq.getLhs(); final CCTerm rhs = eq.getRhs(); computePath(eq.getLhs(), eq.getRhs()); + drainTodo(); final Literal[] cycle = new Literal[mAllLiterals.size() + 1]; int i = 0; cycle[i++] = eq; @@ -327,23 +413,118 @@ public Clause computeCycle(final CCEquality eq, final boolean produceProofs) { } final Clause c = new Clause(cycle); if (produceProofs) { - c.setProof(new LeafNode(LeafNode.THEORY_CC, createAnnotation(new SymmetricPair<>(lhs, rhs)))); + // The disequality being violated is eq, which states value(lhs) == value(rhs) + eq.getOffset(); carry that + // offset on the main diseq so the proof matches the (offset-aware) literal eq instead of a bare lhs == rhs. + final SymmetricPair diseq = new SymmetricPair<>(lhs, CCParameter.of(rhs, eq.getOffset())); + c.setProof(new LeafNode(LeafNode.THEORY_CC, createAnnotation(diseq))); } return c; } - public Clause computeCycle(final CCTerm lconstant, final CCTerm rconstant, final boolean produceProofs) { - mClosure.getLogger().debug("computeCycle for Constants"); - computePath(lconstant, rconstant); - final Literal[] cycle = new Literal[mAllLiterals.size()]; + /** + * The single conflict explainer for an offset cycle: a bridge equality + * {@code lhs = rhs + offset} together with the existing congruence-class + * path(s) implies a contradiction to a known disequality + * {@code lhsDiseq != rhsDiseq + k}. The bridge is justified by a real equality + * literal {@code equality} or, congruence ({@code equality == null}), by the + * argument equalities; {@code offset} is the bridge's offset, {@code 0} for a + * congruence). The disequality can stem from two sources: + *

    + *
  • Disequality clash ({@code diseq != null}): a registered + * disequality {@code diseq} forbids exactly the offset at which the bridge + * would unite. The disequality is between {@code lhsDiseq} and {@code rhsDiseq} + * with exactly the right offset. The clause carries {@code diseq} as its + * positive literal.
  • + *
  • Trivial clash ({@code diseqLit == null}): + * {@code lhsDiseq}/{@code rhsDiseq} are provable distinct by integer reasoning + * (an integer shared term forced to a non-integer value, + * {@code to_real a == 1/2}), or — in the same-class case — the same term at two + * offsets. The contradiction is a trivial axiom; the clause contains no + * positive literal.
  • + *
+ * + *

+ * This covers both the cross-class and same-class shapes uniformly. + *

    + *
  • Cross-class + * ({@code lhs.getRepresentative() != rhs.getRepresentative()}): this is most + * likely caused by a disequality (diseq != null), but it could also be an + * integer conflict.
  • + *
  • Same-class in that case most likely lhsDiseq=rhsDiseq = lhs (or + * rhs). and the equality closes a cycle within one class against the existing + * path from {@code rhs} back to {@code lhs} with a mismatched offset. The + * source half is empty and the trivial diseq is + * {@code lhs != lhs + (bridgeOff − pathOffset)}. The diseq is null.
  • + *
+ * + * @param lhs one side of equality. + * @param rhs one side of equality. + * @param offset offset for rhs in the equality. + * @param equality the equality that caused the conflicting merge (null for + * congruence). + * @param lhsDiseq the side of the diseq that matches lhs (not necessarily + * the diseq's lhs). + * @param rhsDiseq the side of the diseq that matches rhs. + * @param diseq the disequality (null if trivial arithmetic + * disequality). + * @param produceProofs true if proof production is enabled. + */ + public Clause computeMergeConflictCycle(final CCTerm lhs, final CCTerm rhs, final Rational offset, + final CCEquality equality, final CCTerm lhsDiseq, final CCTerm rhsDiseq, final CCEquality diseq, + final boolean produceProofs) { + assert lhs.getRepresentative() == lhsDiseq.getRepresentative(); + assert rhs.getRepresentative() == rhsDiseq.getRepresentative(); + // Justify the bridge edge lhs = rhs + offset. + if (equality != null) { + mAllLiterals.add(equality); + } else { + // Congruence bridge: the argument equalities justify lhs == rhs (and build the + // subpaths that the proof generator uses to resolve the bridge step). + assert offset.equals(Rational.ZERO); + computeCongruence((CCAppTerm) lhs, (CCAppTerm) rhs); + } + // Two single-class paths for lhs and rhs, each offset-correct on its own. These are merged later by hand into a + // single main path, so they are not standalone subpaths: computePathNonRecursive builds them without caching in + // mVisited, so they neither enter mAllPaths nor short-circuit a later standalone request for the same edge + // (their congruence dependencies are still enqueued and collected through the drain below). + final SubPath segSrc = computePathNonRecursive(lhsDiseq, lhs); + final SubPath segDest = computePathNonRecursive(rhs, rhsDiseq); + drainTodo(); + final Literal[] clause = new Literal[mAllLiterals.size() + (diseq != null ? 1 : 0)]; int i = 0; - for (final Literal l: mAllLiterals) { - cycle[i++] = l.negate(); + // The separating disequality (if any) is the cycle's only positive literal; the merge falsifies it. + if (diseq != null) { + clause[i++] = diseq; } - final Clause c = new Clause(cycle); + for (final Literal l : mAllLiterals) { + clause[i++] = l.negate(); + } + final Clause c = new Clause(clause); if (produceProofs) { - c.setProof(new LeafNode( - LeafNode.THEORY_CC, createAnnotation(new SymmetricPair<>(lconstant, rconstant)))); + // paramsSrc = [srcEnd@0, ..., lhs@offLhs] (anchored and oriented at srcEnd). Shift the destination half into + // the source half's frame: after the bridge edge (value(lhs) == value(rhsTerm) + bridgeOff), rhsTerm sits at + // lhs's offset plus bridgeOff; rendering the dest half anchored there yields it already shifted, so the main + // path is a plain concatenation. + final CCParameter[] paramsSrc = segSrc != null ? segSrc.getParams(lhsDiseq) : new CCParameter[] { lhsDiseq }; + assert paramsSrc[paramsSrc.length - 1].getCCTerm() == lhs : "src path must end at lhs + offset"; + final CCParameter destAnchor = + CCParameter.of(rhs, paramsSrc[paramsSrc.length - 1].getOffset().add(offset)); + final CCParameter[] paramsDest = + segDest != null ? segDest.getParams(destAnchor) : new CCParameter[] { destAnchor }; + final CCParameter[] mainPath = new CCParameter[paramsSrc.length + paramsDest.length]; + System.arraycopy(paramsSrc, 0, mainPath, 0, paramsSrc.length); + System.arraycopy(paramsDest, 0, mainPath, paramsSrc.length, paramsDest.length); + assert mainPath[0].getCCTerm() == lhsDiseq : "main path must start at lhs of diseq"; + assert mainPath[mainPath.length - 1].getCCTerm() == rhsDiseq : "main path must end at rhs of diseq"; + // The remaining subpaths (congruences within either half, plus the bridge's argument subpaths) keep deriving + // their offsets the usual way. segSrc/segDest were built off the work list and never entered mAllPaths, so + // mAllPaths is exactly the other paths (the constructor only iterates it, so no copy is needed). + // The clashing equality: a concrete disequality discharged against diseqLit, or (shared case) the trivially + // distinct shared values discharged by an EQ/LA lemma. + final SymmetricPair diseqPair = new SymmetricPair<>(mainPath[0], + mainPath[mainPath.length - 1]); + c.setProof(new LeafNode(LeafNode.THEORY_CC, + new CCAnnotation(diseqPair, mainPath, mAllPaths, CCAnnotation.RuleKind.CONG))); } return c; } @@ -353,6 +534,7 @@ public Clause computeDTLemma(final CCEquality propagatedEq, final DataTypeLemma for (final SymmetricPair reason : lemma.getReason()) { computePath(reason.getFirst(), reason.getSecond()); } + drainTodo(); final Literal[] negLits = new Literal[mAllLiterals.size() + (propagatedEq != null ? 1 : 0)]; int i = 0; @@ -364,8 +546,8 @@ public Clause computeDTLemma(final CCEquality propagatedEq, final DataTypeLemma } final Clause c = new Clause(negLits); if (produceProofs) { - final SymmetricPair diseq = lemma.getMainEquality(); - c.setProof(new LeafNode(LeafNode.THEORY_DT, new CCAnnotation(diseq, mAllPaths, lemma))); + // the main equality carries the offset of a numeric constructor field. + c.setProof(new LeafNode(LeafNode.THEORY_DT, new CCAnnotation(lemma.getMainEquality(), mAllPaths, lemma))); } return c; } @@ -382,6 +564,7 @@ public Clause computeDTLemma(final CCEquality propagatedEq, final DataTypeLemma */ public int computeDecideLevel(final CCTerm lhs, final CCTerm rhs) { computePath(lhs, rhs); + drainTodo(); int depth = 0; for (final Literal l : mAllLiterals) { depth = Math.max(depth, l.getAtom().getDecideLevel()); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruenceTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruenceTrigger.java index 589a9e1a2..b15d79c9b 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruenceTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruenceTrigger.java @@ -37,8 +37,12 @@ public final class CongruenceTrigger extends SignatureTrigger { * * @param app * the (kept) application term for this signature. + * @param func + * the function symbol (signature id). + * @param args + * the application's arguments as {@link CCParameter}s (shared with the app term). */ - public CongruenceTrigger(final CCAppTerm app, FunctionSymbol func, CCTerm[] args) { + public CongruenceTrigger(final CCAppTerm app, FunctionSymbol func, CCParameter[] args) { super(func, args); mApp = app; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DTReverseTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DTReverseTrigger.java index af3297f70..741070ede 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DTReverseTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DTReverseTrigger.java @@ -57,7 +57,7 @@ public DTReverseTrigger(final DataTypeTheory dtTheory, final Clausifier clausifi } @Override - public CCTerm getArgument() { + public CCParameter getArgument() { return mArg; } @@ -77,9 +77,12 @@ public void activate(final CCAppTerm appTerm, final boolean isFresh) { mClausifier.getLogger().debug("DTReverseTrigger: %s on %s", appTerm, mArg); final ApplicationTerm argAT = (ApplicationTerm) mArg.mFlatTerm; final SymmetricPair[] reason; - if (appTerm.getArgument(0) != mArg) { + // this trigger fires on a selector/tester, whose argument is the datatype value (never numeric, so offset-free); + // the cast asserts that no-offset assumption at runtime + final CCTerm appArg = (CCTerm) appTerm.getArgParam(0); + if (appArg != mArg) { reason = new SymmetricPair[] { - new SymmetricPair<>(appTerm.getArgument(0), mArg) + new SymmetricPair<>(appArg, mArg) }; } else { reason = new SymmetricPair[0]; @@ -94,7 +97,7 @@ public void activate(final CCAppTerm appTerm, final boolean isFresh) { } else { truthValue = mClausifier.getTheory().mFalse; } - final SymmetricPair mainEq = new SymmetricPair<>(appTerm, mClausifier.getCCTerm(truthValue)); + final SymmetricPair mainEq = new SymmetricPair<>(appTerm, mClausifier.getCCTerm(truthValue)); final DataTypeLemma lemma = new DataTypeLemma(RuleKind.DT_TESTER, mainEq, reason, mArg); mDTTheory.addPendingLemma(lemma); if (isFresh) { @@ -107,8 +110,10 @@ public void activate(final CCAppTerm appTerm, final boolean isFresh) { final Constructor c = argDT.getConstructor(argAT.getFunction().getName()); for (int i = 0; i < c.getSelectors().length; i++) { if (mFunctionSymbol.getName() == c.getSelectors()[i]) { - final SymmetricPair mainEq = new SymmetricPair<>(appTerm, - mClausifier.getCCTerm(argAT.getParameters()[i])); + // mArg is the constructor application; read field i as a CCParameter so a numeric field keeps + // its offset, making the propagated equality value(sel(u)) == value(field). + final CCParameter mainArg = ((CCAppTerm) mArg).getArgParam(i); + final SymmetricPair mainEq = new SymmetricPair<>(appTerm, mainArg); final DataTypeLemma lemma = new DataTypeLemma(RuleKind.DT_PROJECT, mainEq, reason, mArg); mDTTheory.addPendingLemma(lemma); if (isFresh) { diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeLemma.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeLemma.java index d2e1c644c..031cb8938 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeLemma.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeLemma.java @@ -9,7 +9,13 @@ public class DataTypeLemma { private final RuleKind mRule; - private final SymmetricPair mMainEquality; + /** + * The propagated equality as a pair of {@link CCParameter}s, i.e. the equality of the two values + * {@code first.getCCTerm() + first.getOffset() == second.getCCTerm() + second.getOffset()}. For all rules except + * dt-project and dt-injective both sides are offset-free (a bare {@link CCTerm}); those two carry the offset of a + * numeric constructor argument. {@code null} for conflict lemmas that prove no equality. + */ + private final SymmetricPair mMainEquality; private final SymmetricPair[] mReason; private final Object[] mAnnotation; @@ -20,7 +26,7 @@ public class DataTypeLemma { * @param mainEquality the propagated equality {@code u = C(s1(u),...,sn(u))}. * @param reason the equality isC(u) = true. */ - public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquality, + public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquality, final SymmetricPair[] reason) { mRule = rule; mReason = reason; @@ -37,7 +43,7 @@ public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquali * @param reason the equalities to prove u = consTerm * @param consTerm1 the constructor {@code C(a1,...,an)} to which u is equal. */ - public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquality, + public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquality, final SymmetricPair[] reason, final CCTerm consTerm) { assert rule == RuleKind.DT_PROJECT || rule == RuleKind.DT_TESTER; mRule = rule; @@ -55,7 +61,7 @@ public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquali * @param consTerm1 the first constructor {@code C(a1,...,an)} * @param consTerm2 the second constructor {@code C(b1,...,bn)} */ - public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquality, + public DataTypeLemma(final RuleKind rule, final SymmetricPair mainEquality, final SymmetricPair[] reason, final CCTerm consTerm1, final CCTerm consTerm2) { assert rule == RuleKind.DT_INJECTIVE; mRule = rule; @@ -102,7 +108,7 @@ public RuleKind getRule() { return mRule; } - public SymmetricPair getMainEquality() { + public SymmetricPair getMainEquality() { return mMainEquality; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeTheory.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeTheory.java index fc1e1f785..10ef75d55 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeTheory.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/DataTypeTheory.java @@ -36,6 +36,7 @@ import de.uni_freiburg.informatik.ultimate.logic.DataType; import de.uni_freiburg.informatik.ultimate.logic.DataType.Constructor; import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.logic.SMTLIBConstants; import de.uni_freiburg.informatik.ultimate.logic.Sort; import de.uni_freiburg.informatik.ultimate.logic.SortSymbol; @@ -113,13 +114,22 @@ public void addPendingLemma(final DataTypeLemma lemma) { */ private Clause processPendingLemmas() { for (final DataTypeLemma lemma : mPendingLemmas) { - final SymmetricPair eq = lemma.getMainEquality(); + final SymmetricPair eq = lemma.getMainEquality(); if (eq == null) { // this is a conflict lemma, not a lemma that proves an equality. return computeClause(null, lemma); } - if (eq.getFirst().mRepStar != eq.getSecond().mRepStar) { - final CCEquality eqAtom = mCClosure.createEquality(eq.getFirst(), eq.getSecond(), false); + final CCParameter first = eq.getFirst(); + final CCParameter second = eq.getSecond(); + // the lemma propagates value(first) == value(second); only act if that is not already the case + if (!first.sameValueAs(second)) { + if (first.getCCTerm() == second.getCCTerm()) { + // same CCTerm but different offset: value-equality is trivially unsatisfiable -> conflict + return computeClause(null, lemma); + } + // encode the offset into the equality: value(first.cc) == value(second.cc) + offset + final Rational offset = second.getOffset().sub(first.getOffset()); + final CCEquality eqAtom = mCClosure.createEquality(first.getCCTerm(), second.getCCTerm(), offset, false); if (eqAtom == null) { // this is a trivial disequality, so we need to create a conflict. return computeClause(null, lemma); @@ -176,11 +186,15 @@ private void computeInjectiveDisjointLemmas(CCTerm lhs, CCTerm rhs) { @SuppressWarnings("unchecked") final SymmetricPair[] reason = new SymmetricPair[] { new SymmetricPair<>(lhs, rhs) }; if (lhsApp.getFunction() == rhsApp.getFunction()) { + // lhs/rhs are constructor applications, hence the CCAppTerms; read each argument as a CCParameter so + // a numeric field keeps its offset (cons(y+5) = cons(z) propagates the offset equality y+5 = z). + final CCAppTerm lhsCons = (CCAppTerm) lhs; + final CCAppTerm rhsCons = (CCAppTerm) rhs; for (int i = 0; i < lhsApp.getParameters().length; i++) { - final CCTerm lhsArg = mClausifier.getCCTerm(lhsApp.getParameters()[i]); - final CCTerm rhsArg = mClausifier.getCCTerm(rhsApp.getParameters()[i]); - if (rhsArg.mRepStar != lhsArg.mRepStar) { - final SymmetricPair eqPair = new SymmetricPair<>(lhsArg, rhsArg); + final CCParameter lhsArg = lhsCons.getArgParam(i); + final CCParameter rhsArg = rhsCons.getArgParam(i); + if (!lhsArg.sameValueAs(rhsArg)) { + final SymmetricPair eqPair = new SymmetricPair<>(lhsArg, rhsArg); // dt_injective: cons(args1) != cons(args2) or args1[i] == args2[i] addPendingLemma(new DataTypeLemma(RuleKind.DT_INJECTIVE, eqPair, reason, lhs, rhs)); } @@ -213,7 +227,7 @@ public Clause checkpoint() { final ApplicationTerm at = (ApplicationTerm) t.mFlatTerm; final CCAppTerm trueIsApp = (CCAppTerm) t; if (at.getFunction().getName() == "is") { - final CCTerm argRep = trueIsApp.getArgument(0).getRepresentative(); + final CCTerm argRep = ((CCTerm) trueIsApp.getArgParam(0)).getRepresentative(); if (!visited.containsKey(argRep)) { visited.put(argRep, trueIsApp); addConstructorLemma(trueIsApp); @@ -228,8 +242,9 @@ public Clause checkpoint() { final ArrayList> reason = new ArrayList<>(); reason.add(new SymmetricPair<>(prevIsApp, trueCC)); reason.add(new SymmetricPair<>(trueIsApp, trueCC)); - if (prevIsApp.getArgument(0) != trueIsApp.getArgument(0)) { - reason.add(new SymmetricPair<>(prevIsApp.getArgument(0), trueIsApp.getArgument(0))); + if ((CCTerm) prevIsApp.getArgParam(0) != (CCTerm) trueIsApp.getArgParam(0)) { + reason.add(new SymmetricPair<>((CCTerm) prevIsApp.getArgParam(0), + (CCTerm) trueIsApp.getArgParam(0))); } final Term[] testers = new Term[2]; testers[0] = prevIsApp.mFlatTerm; @@ -252,7 +267,7 @@ public Clause checkpoint() { if (cct instanceof CCAppTerm) { final CCAppTerm appTerm = (CCAppTerm) cct; if (appTerm.getFunctionSymbol().getName().equals(SMTLIBConstants.IS)) { - final CCTerm arg = appTerm.getArgument(0); + final CCTerm arg = (CCTerm) appTerm.getArgParam(0); falseIsFuns.putIfAbsent(arg.getRepresentative(), new LinkedHashSet<>()); falseIsFuns.get(arg.getRepresentative()).add(appTerm); } @@ -279,7 +294,7 @@ public Clause checkpoint() { for (final Constructor consName : dt.getConstructors()) { final CCAppTerm isFun = isIndices.get(consName.getName()); testers[i++] = isFun.mFlatTerm; - final CCTerm arg = isFun.getArgument(0); + final CCTerm arg = (CCTerm) isFun.getArgParam(0); reason.add(new SymmetricPair<>(isFun, falseCC)); if (firstArg == null) { firstArg = arg; @@ -402,11 +417,11 @@ public Clause finalCheck() { mCClosure.getLogger().error("Unpropagated equality on different conses"); computeInjectiveDisjointLemmas(consTerm, member); } else { - // check we propagated all equalities between constructor arguments. + // check we propagated all equalities between constructor arguments (offset-aware). for (int i = 0; i < memberAt.getParameters().length; i++) { - final CCTerm consArg = mClausifier.getCCTerm(consAt.getParameters()[i]); - final CCTerm memArg = mClausifier.getCCTerm(memberAt.getParameters()[i]); - if (memArg.mRepStar != consArg.mRepStar) { + final CCParameter consArg = ((CCAppTerm) consTerm).getArgParam(i); + final CCParameter memArg = ((CCAppTerm) member).getArgParam(i); + if (!memArg.sameValueAs(consArg)) { mCClosure.getLogger().error("Unpropagated constructor argument equality"); computeInjectiveDisjointLemmas(consTerm, member); } @@ -423,7 +438,7 @@ public Clause finalCheck() { final CCAppTerm appTerm = (CCAppTerm) ccTerm; final FunctionSymbol fs = appTerm.getFunctionSymbol(); if (fs.isSelector() || fs.getName().equals(SMTLIBConstants.IS)) { - final CCTerm argTerm = appTerm.getArgument(0); + final CCTerm argTerm = (CCTerm) appTerm.getArgParam(0); final CCTerm consTerm = argTerm.getRepresentative().getSharedTerm(); if (consTerm != null) { final ApplicationTerm consApp = (ApplicationTerm) consTerm.getFlatTerm(); @@ -440,7 +455,7 @@ public Clause finalCheck() { @SuppressWarnings("unchecked") final SymmetricPair[] reason = consTerm == argTerm ? new SymmetricPair[0] : new SymmetricPair[] { new SymmetricPair<>(consTerm, argTerm) }; - final SymmetricPair mainEq = new SymmetricPair<>(ccTerm, truthCC); + final SymmetricPair mainEq = new SymmetricPair<>(ccTerm, truthCC); final DataTypeLemma lemma = new DataTypeLemma(RuleKind.DT_TESTER, mainEq, reason, consTerm); addPendingLemma(lemma); } @@ -450,13 +465,15 @@ public Clause finalCheck() { final String[] allSelectorNames = constructor.getSelectors(); for (int i = 0; i < allSelectorNames.length; i++) { if (allSelectorNames[i].equals(fs.getName())) { - final CCTerm consArg = ((CCAppTerm) consTerm).getArgument(i); - if (ccTerm.getRepresentative() != consArg.getRepresentative()) { + // the field may be numeric, so keep its offset; ccTerm (the selector application) is + // offset-free, and the propagated equality is value(ccTerm) == value(field). + final CCParameter consArg = ((CCAppTerm) consTerm).getArgParam(i); + if (!ccTerm.sameValueAs(consArg)) { mCClosure.getLogger().error("Unpropagated selector of constructor"); @SuppressWarnings("unchecked") final SymmetricPair[] reason = consTerm == argTerm ? new SymmetricPair[0] : new SymmetricPair[] { new SymmetricPair<>(consTerm, argTerm) }; - final SymmetricPair mainEq = new SymmetricPair<>(ccTerm, consArg); + final SymmetricPair mainEq = new SymmetricPair<>(ccTerm, consArg); final DataTypeLemma lemma = new DataTypeLemma(RuleKind.DT_PROJECT, mainEq, reason, consTerm); addPendingLemma(lemma); @@ -534,7 +551,8 @@ private List getAllDataTypeChildren(final CCTerm ccTerm, final Map getAllDataTypeChildren(final CCTerm ccTerm, final Map selectors = new LinkedHashSet<>(); FunctionSymbol trueTester = null; final Set falseTesters = new LinkedHashSet<>(); @@ -626,7 +644,7 @@ private Clause buildCycleConflict(final CCTerm currentTerm, final Deque // Get the corresponding tester or create it if it does not exists. // If it exists, the corresponding tester is true. final CCAppTerm selectTerm = (CCAppTerm) currentAsChild; - prevAsParent = selectTerm.getArgument(0); + prevAsParent = (CCTerm) selectTerm.getArgParam(0); final FunctionSymbol selectorFunc = selectTerm.getFunctionSymbol(); final Constructor cons = getConstructor(selectorFunc); final Term isTerm = mTheory.term(mTheory.getFunctionWithResult(SMTLIBConstants.IS, new String[] { cons.getName() }, @@ -729,7 +747,7 @@ public void recheckTrigger() { ApplicationTerm constructor = null; final CCAppTerm checkTerm = iter.next(); final FunctionSymbol selectorOrTester = checkTerm.getFunctionSymbol(); - final CCTerm selectOrIsArg = checkTerm.getArgument(0); + final CCTerm selectOrIsArg = (CCTerm) checkTerm.getArgParam(0); assert selectorOrTester.isSelector() || selectorOrTester.getName().equals(SMTLIBConstants.IS); for (final CCTerm ct : selectOrIsArg.getRepresentative().mMembers) { if (ct.mFlatTerm instanceof ApplicationTerm && ((ApplicationTerm) ct.mFlatTerm).getFunction().isConstructor()) { @@ -754,9 +772,9 @@ public void recheckTrigger() { assert c.getName().equals(constructor.getFunction().getName()); for (int i = 0; i < c.getSelectors().length; i++) { if (selName.equals(c.getSelectors()[i])) { - final CCTerm arg = mClausifier.getCCTerm(constructor.getParameters()[i]); - if (arg.mRepStar != checkTerm.mRepStar) { - final SymmetricPair provedEq = new SymmetricPair<>(checkTerm, arg); + final CCParameter arg = ((CCAppTerm) constructorCCTerm).getArgParam(i); + if (!checkTerm.sameValueAs(arg)) { + final SymmetricPair provedEq = new SymmetricPair<>(checkTerm, arg); final DataTypeLemma lemma = new DataTypeLemma(RuleKind.DT_PROJECT, provedEq, reason, constructorCCTerm); addPendingLemma(lemma); @@ -774,7 +792,7 @@ public void recheckTrigger() { } final CCTerm ccTruthValue = mClausifier.getCCTerm(truthValue); if (ccTruthValue.mRepStar != checkTerm.mRepStar) { - final SymmetricPair provedEq = new SymmetricPair<>(checkTerm, ccTruthValue); + final SymmetricPair provedEq = new SymmetricPair<>(checkTerm, ccTruthValue); final DataTypeLemma lemma = new DataTypeLemma(RuleKind.DT_TESTER, provedEq, reason, constructorCCTerm); addPendingLemma(lemma); @@ -831,7 +849,7 @@ public Object[] getStatistics() { */ private void addConstructorLemma(final CCAppTerm isTerm) { // check if there is already a constructor application equal to the argument - final CCTerm arg = isTerm.getArgument(0); + final CCTerm arg = (CCTerm) isTerm.getArgParam(0); if (arg.getRepresentative().getSharedTerm() != null) { // We don't care which constructor it is. If it's the wrong constructor // there should already be a trigger that set the isTerm to false. @@ -857,8 +875,8 @@ private void addConstructorLemma(final CCAppTerm isTerm) { // create a new constructor application like C(s1(x), s2(x), ..., sm(x)) final Sort consType = c.needsReturnOverload() ? dtTerm.getSort() : null; final Term consTerm = mTheory.term(consName, null, consType, selectorTerms); - final CCTerm consCCTerm = mClausifier.createCCTerm(consTerm, SourceAnnotation.EMPTY_SOURCE_ANNOT); - final SymmetricPair eq = new SymmetricPair<>(arg, consCCTerm); + final CCTerm consCCTerm = (CCTerm) mClausifier.createCCTerm(consTerm, SourceAnnotation.EMPTY_SOURCE_ANNOT); + final SymmetricPair eq = new SymmetricPair<>(arg, consCCTerm); @SuppressWarnings("unchecked") final DataTypeLemma lemma = new DataTypeLemma(RuleKind.DT_CONSTRUCTOR, eq, new SymmetricPair[] { new SymmetricPair<>(isTerm, mClausifier.getCCTerm(mTheory.mTrue)) }); @@ -940,9 +958,13 @@ private static boolean isConstructorApp(Term term) { private class ConstrTerm { FunctionSymbol mConstr; - CCTerm[] mArguments; + /** + * The constructor's arguments as {@link CCParameter}s, so a numeric field's offset (e.g. the {@code +5} in + * {@code cons(x+5)}) is kept and shows up in the model value. {@code null} entries are holes (no known value). + */ + CCParameter[] mArguments; - public ConstrTerm(FunctionSymbol constr, CCTerm[] args) { + public ConstrTerm(FunctionSymbol constr, CCParameter[] args) { mConstr = constr; mArguments = args; } @@ -978,11 +1000,15 @@ public void fillInModel(ModelBuilder modelBuilder, List sorts, LinkedHashM if (sharedTerm instanceof CCAppTerm) { final CCAppTerm constrAppTerm = (CCAppTerm) sharedTerm; final FunctionSymbol constr = constrAppTerm.getFunctionSymbol(); - final CCTerm[] args = constrAppTerm.getArguments(); + // keep each argument as a CCParameter so a numeric field's offset is preserved + final CCParameter[] args = new CCParameter[constrAppTerm.getArgCount()]; + for (int i = 0; i < args.length; i++) { + args[i] = constrAppTerm.getArgParam(i); + } valueMap.put(ct, new ConstrTerm(constr, args)); } else { final ApplicationTerm appTerm = (ApplicationTerm) sharedTerm.getFlatTerm(); - valueMap.put(ct, new ConstrTerm(appTerm.getFunction(), new CCTerm[0])); + valueMap.put(ct, new ConstrTerm(appTerm.getFunction(), new CCParameter[0])); } } else { final Map selectorsAndTester = getSelectorsAndTesters(ct); @@ -990,7 +1016,8 @@ public void fillInModel(ModelBuilder modelBuilder, List sorts, LinkedHashM // we can use any constructor for which no tester exists. We use the first one. final String[] selectors = constr.getSelectors(); final Sort[] argSorts = new Sort[selectors.length]; - final CCTerm[] args = new CCTerm[selectors.length]; + // a selector application's value already is the field value (no structural offset) + final CCParameter[] args = new CCParameter[selectors.length]; for (int i = 0; i < selectors.length; i++) { final FunctionSymbol selector = mTheory.getFunction(selectors[i], sort); argSorts[i] = selector.getReturnSort(); @@ -1028,14 +1055,17 @@ public void fillInModel(ModelBuilder modelBuilder, List sorts, LinkedHashM boolean undefined = false; boolean hasHole = false; for (int i = 0; i < argModels.length; i++) { - CCTerm arg = constrTerm.mArguments[i]; + final CCParameter arg = constrTerm.mArguments[i]; if (arg != null) { - arg = arg.getRepresentative(); + // offset-aware: a numeric field cons(.., y+5, ..) contributes value(y) + 5 argModels[i] = modelBuilder.getModelValue(arg); if (argModels[i] == null) { - assert valueMap.containsKey(arg); + // only datatype-typed fields can be unresolved here (numeric deps come first and are + // offset-free w.r.t. their own representative); recurse on the field's representative + final CCTerm argRep = arg.getCCTerm().getRepresentative(); + assert valueMap.containsKey(argRep); path.add(ct); - todoStack.addLast(arg); + todoStack.addLast(argRep); undefined = true; break; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/MasterReverseTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/MasterReverseTrigger.java index c28363459..2bc089e74 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/MasterReverseTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/MasterReverseTrigger.java @@ -18,6 +18,8 @@ */ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure; +import java.util.ArrayList; + import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; import de.uni_freiburg.informatik.ultimate.util.HashUtils; import de.uni_freiburg.informatik.ultimate.util.datastructures.UnifyHash; @@ -33,19 +35,21 @@ public final class MasterReverseTrigger extends ReverseTrigger { private final FunctionSymbol mFunctionSymbol; private final int mArgPosition; - static final UnifyHash sUnifier = new UnifyHash<>(); - public static MasterReverseTrigger of(final CClosure engine, final FunctionSymbol functionSymbol, final int argPosition) { + // The unifier must be per engine: solver instances may share the theory and thus the function + // symbols (e.g. the interpolant checking solver is a clone of the main solver), and each engine + // needs its own master trigger registered as find trigger. + final UnifyHash unifier = engine.getMasterReverseTriggers(); final int hash = HashUtils.hashJenkins(argPosition, functionSymbol); - for (final MasterReverseTrigger masterReverseTrigger : sUnifier.iterateHashCode(hash)) { + for (final MasterReverseTrigger masterReverseTrigger : unifier.iterateHashCode(hash)) { if (masterReverseTrigger.mFunctionSymbol == functionSymbol && masterReverseTrigger.mArgPosition == argPosition) { return masterReverseTrigger; } } final MasterReverseTrigger masterReverseTrigger = new MasterReverseTrigger(engine, functionSymbol, argPosition); - sUnifier.put(hash, masterReverseTrigger); + unifier.put(hash, masterReverseTrigger); engine.insertFindTrigger(functionSymbol, masterReverseTrigger); return masterReverseTrigger; } @@ -63,7 +67,7 @@ public FunctionSymbol getFunctionSymbol() { } @Override - public CCTerm getArgument() { + public CCParameter getArgument() { return null; } @@ -78,5 +82,10 @@ public void activate(final CCAppTerm appTerm, final boolean isFresh) { assert appTerm.getFunctionSymbol() == mFunctionSymbol; final ReverseTriggerTrigger reverseTriggerTrigger = new ReverseTriggerTrigger(this, appTerm, mArgPosition); mEngine.addSignature(reverseTriggerTrigger); + // remember the signature on the term, so it is removed with the term on pop. + if (appTerm.mReverseTriggers == null) { + appTerm.mReverseTriggers = new ArrayList<>(); + } + appTerm.mReverseTriggers.add(reverseTriggerTrigger); } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ModelBuilder.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ModelBuilder.java index 59ffb0b66..3d3d2bd26 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ModelBuilder.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ModelBuilder.java @@ -55,6 +55,14 @@ public class ModelBuilder { SharedTermEvaluator mEvaluator; Map mModelValues = new HashMap<>(); Theory mTheory; + /** + * For each representative of a numeric congruence class, the minimal/maximal offset at which any use site (a class + * member or a function application argument) references the class. The value at such a use site is the + * representative's value plus the offset, so a freshly chosen class value must keep the whole range + * {@code [value+min, value+max]} clear of other model values, not just the value itself. + */ + Map mMinOffset = new HashMap<>(); + Map mMaxOffset = new HashMap<>(); public ModelBuilder(final CClosure closure, final List terms, final Model model, final Theory t, final SharedTermEvaluator ste, @@ -64,6 +72,25 @@ public ModelBuilder(final CClosure closure, final List terms, final Mode mEvaluator = ste; mTheory = t; + // collect the offset range at which each numeric class is used: every member and every function application + // argument is a use site whose model value is the representative's value plus its offset to the representative. + for (final CCTerm term : terms) { + final Term flatTerm = term.getFlatTerm(); + if (flatTerm != null && flatTerm.getSort().isNumericSort()) { + updateOffsetRange(term.getRepresentative(), term.getOffsetToRep()); + } + if (term instanceof CCAppTerm) { + final CCAppTerm app = (CCAppTerm) term; + for (int i = 0; i < app.getArgCount(); i++) { + final CCParameter arg = app.getArgParam(i); + final Term argTerm = arg.getCCTerm().getFlatTerm(); + if (argTerm != null && argTerm.getSort().isNumericSort()) { + updateOffsetRange(arg.getRepresentative(), arg.getOffsetToRep()); + } + } + } + } + // create a map from sorts to representatives of that sort. final LinkedHashMap> repsBySort = new LinkedHashMap<>(); for (final CCTerm term : terms) { @@ -151,14 +178,52 @@ public SharedTermEvaluator getEvaluator() { return mEvaluator; } - public Term getModelValue(final CCTerm term) { - return mModelValues.get(term.getRepresentative()); + /** + * The model value of a {@link CCParameter} value {@code term + offset}: the representative's model value shifted by + * the parameter's offset to the representative. A bare {@link CCTerm} is an offset-free parameter, so this returns + * its representative's value; a numeric member additionally adds its offset to the representative. Returns + * {@code null} if the class has no model value yet (used as a "value computed?" check) — this only happens for + * non-numeric sorts, where no offset arithmetic is attempted on the missing value. + * + *

This is the only model-value accessor: there is deliberately no {@code getModelValue(CCTerm)}, since looking up + * a bare representative would silently drop a member's offset (returning the representative's value instead of the + * member's) — a subtle source of wrong models for numeric terms. + */ + public Term getModelValue(final CCParameter param) { + final Term repValue = mModelValues.get(param.getRepresentative()); + final Sort sort = param.getCCTerm().getFlatTerm().getSort(); + if (!sort.isNumericSort()) { + assert param.getOffsetToRep().equals(Rational.ZERO); + return repValue; + } + // the member's value is the representative's value shifted by the parameter's offset to the representative + // (which folds in both the class offset and the structural argument offset, e.g. the +5 in x+5). + return NumericSortInterpretation.toRational(repValue).add(param.getOffsetToRep()).toTerm(sort); + } + + private void updateOffsetRange(final CCTerm rep, final Rational offset) { + final Rational min = mMinOffset.get(rep); + if (min == null || offset.compareTo(min) < 0) { + mMinOffset.put(rep, offset); + } + final Rational max = mMaxOffset.get(rep); + if (max == null || offset.compareTo(max) > 0) { + mMaxOffset.put(rep, offset); + } } public void setModelValue(final CCTerm term, final Term value) { assert term == term.getRepresentative(); final Term old = mModelValues.put(term, value); mModel.provideSortInterpretation(value.getSort()).register(value); + if (value.getSort().isNumericSort()) { + // mark the largest use-site value of the class as used, so fresh values avoid the whole class range. + final Rational maxOffset = mMaxOffset.get(term); + if (maxOffset != null && maxOffset.signum() > 0) { + mModel.provideSortInterpretation(value.getSort()) + .register(NumericSortInterpretation.toRational(value).add(maxOffset).toTerm(value.getSort())); + } + } assert old == null || old == value; } @@ -172,7 +237,10 @@ public void fillInTermValues(final List terms, final CCTerm trueNode, fi if (sort.isNumericSort()) { Rational v; if (ccterm.getSharedTerm() != null) { - v = mEvaluator.evaluate(ccterm.getSharedTerm().getFlatTerm(), mTheory); + final CCTerm shared = ccterm.getSharedTerm(); + // evaluate gives the value of the (offset-free) shared term; the representative's value is that + // minus the shared term's offset to the representative. + v = mEvaluator.evaluate(shared.getFlatTerm(), mTheory).sub(shared.getOffsetToRep()); if (smtterm.getSort().getName().equals("Int") && !v.isIntegral()) { throw new AssertionError("Int term has non-integral value"); } @@ -216,14 +284,21 @@ public void fillInTermValues(final List terms, final CCTerm trueNode, fi // Handle all delayed elements // note that extendFresh must be called after all values in the model have been extended to the numeric sort. for (final CCTerm ccterm : delayed) { - final Term value = mModel.extendFresh(ccterm.getFlatTerm().getSort()); - setModelValue(ccterm, value); + final Sort sort = ccterm.getFlatTerm().getSort(); + Rational value = NumericSortInterpretation.toRational(mModel.extendFresh(sort)); + // shift by the class's smallest use-site offset, so even the smallest use-site value (value + minOffset) + // is the fresh one; setModelValue then marks the largest one (value + maxOffset) as used. + final Rational minOffset = mMinOffset.get(ccterm); + if (minOffset != null && minOffset.signum() < 0) { + value = value.sub(minOffset); + } + setModelValue(ccterm, value.toTerm(sort)); } } public void fillInFunctions(final List terms, final Model model, final Theory t) { for (final CCTerm term : terms) { - add(model, term, mModelValues.get(term.getRepresentative()), t); + add(model, term, getModelValue(term), t); } } @@ -267,10 +342,11 @@ private boolean isUndefinedFor(FunctionSymbol fs, Term[] args) { } private void addApp(final Model model, final CCAppTerm app, final Term value, final Theory t) { - CCTerm[] ccArgs = app.getArguments(); - Term[] args = new Term[ccArgs.length]; + Term[] args = new Term[app.getArgCount()]; for (int i = 0; i < args.length; i++) { - args[i] = mModelValues.get(ccArgs[i].getRepresentative()); + // the actual argument is the CCParameter's term + offset, evaluated at its model value + final CCParameter argParam = app.getArgParam(i); + args[i] = getModelValue(argParam); } final FunctionSymbol fs = app.getFunctionSymbol(); if (!fs.isIntern() || isUndefinedFor(fs, args)) { diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/OffsettedCCTerm.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/OffsettedCCTerm.java new file mode 100644 index 000000000..5dcd15aaa --- /dev/null +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/OffsettedCCTerm.java @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2026 University of Freiburg + * + * This file is part of SMTInterpol. + * + * SMTInterpol is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SMTInterpol is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SMTInterpol. If not, see . + */ +package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure; + +import de.uni_freiburg.informatik.ultimate.logic.Rational; +import de.uni_freiburg.informatik.ultimate.logic.Term; + +/** + * A {@link CCParameter} with a non-zero constant offset, i.e. the value {@code mTerm + mOffset}. Offset-free values are + * represented by a bare {@link CCTerm}, so this class is only ever created with a non-zero offset (use + * {@link CCParameter#of}). + * + *

{@code equals}/{@code hashCode} are structural over {@code (mTerm, mOffset)} (the term object and the offset), + * which is stable. This is not the value identity {@code (representative, offsetToRep)} (which changes on + * merge); for value comparison use {@link CCParameter#sameValueAs}. + * + * @author Jochen Hoenicke + */ +public final class OffsettedCCTerm implements CCParameter { + private final CCTerm mTerm; + private final Rational mOffset; + + OffsettedCCTerm(final CCTerm term, final Rational offset) { + assert !offset.equals(Rational.ZERO) : "use a bare CCTerm for a zero offset"; + mTerm = term; + mOffset = offset; + } + + @Override + public CCTerm getCCTerm() { + return mTerm; + } + + @Override + public Rational getOffset() { + return mOffset; + } + + @Override + public Term getFlatTerm() { + final Term term = mTerm.getFlatTerm(); + return CCParameter.addConstant(term, mOffset); + } + + @Override + public int hashCode() { + return mTerm.hashCode() * 31 + mOffset.hashCode(); + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof OffsettedCCTerm)) { + return false; + } + final OffsettedCCTerm o = (OffsettedCCTerm) other; + return mTerm == o.mTerm && mOffset.equals(o.mOffset); + } + + @Override + public String toString() { + return mTerm + "+" + mOffset; + } +} diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTrigger.java index 3f387785d..d180ac57e 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTrigger.java @@ -40,11 +40,12 @@ public abstract class ReverseTrigger extends SimpleListable { SignatureTrigger mSignatureTrigger; /** - * Get the argument on which the reverse trigger is installed. + * Get the argument value on which the reverse trigger is installed, as a {@link CCParameter} so the offset (if any) + * is part of the watched value. * - * @return the argument term. + * @return the argument value. */ - public abstract CCTerm getArgument(); + public abstract CCParameter getArgument(); /** * Get the position in the function application where the argument should be. diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTriggerTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTriggerTrigger.java index 5f0bc9604..f99eea015 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTriggerTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/ReverseTriggerTrigger.java @@ -35,15 +35,20 @@ public final class ReverseTriggerTrigger extends SignatureTrigger { public ReverseTriggerTrigger(MasterReverseTrigger masterTrigger, ReverseTrigger trigger) { - super(masterTrigger, new CCTerm[] { trigger.getArgument() }); + super(masterTrigger, new CCParameter[] { trigger.getArgument() }); mTriggers.append(trigger); } public ReverseTriggerTrigger(MasterReverseTrigger masterTrigger, CCAppTerm app, int argPosition) { - super(masterTrigger, new CCTerm[] { app.getArgument(argPosition) }); + // key the signature on the argument value (including its offset), so it matches the trigger's watched value + super(masterTrigger, new CCParameter[] { app.getArgParam(argPosition) }); mApplications.append(new AppTermEntry(app)); } + public SimpleList getTriggers() { + return mTriggers; + } + public SimpleList getApplications() { return mApplications; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/SignatureTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/SignatureTrigger.java index 9e0df9d78..7f9d2bf2b 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/SignatureTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/SignatureTrigger.java @@ -18,62 +18,72 @@ */ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure; -import java.util.Arrays; -import java.util.List; - +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.SimpleListable; import de.uni_freiburg.informatik.ultimate.util.HashUtils; /** - * A signature is a tuple of an opaque identifier and a non-empty array of CCTerms. It is used as the key for the - * global signature-to-trigger map in CClosure. Hash and equality are based on the representatives of the - * terms, so when representatives change (e.g. after a merge), the same term array yields a different signature key. + * A signature is a tuple of an opaque identifier and a non-empty array of {@link CCParameter}s (a CCTerm together with + * a constant offset). It is used as the key for the global signature-to-trigger map in CClosure. Hash and equality are + * based on each parameter's representative and its offset to that representative, so when + * representatives or offsets change (e.g. after a merge), the same parameter array yields a different signature key. * * @author Jochen Hoenicke */ public class SignatureTrigger extends SimpleListable { private final Object mId; - private final CCTerm[] mTerms; + /** + * The arguments of the signature as {@link CCParameter}s: argument {@code i} has value + * {@code mParams[i].getCCTerm() + mParams[i].getOffset()}. Offset-free arguments are bare {@link CCTerm}s, which + * keeps plain congruence closure free of any offset cost. The effective offset that the signature is keyed on is + * {@code mParams[i].getOffsetToRep()} (the parameter's offset relative to its representative; see + * {@link #effectiveOffset(int)}), so two applications are congruent only if their arguments have the same + * representative and the same effective offset. + */ + private final CCParameter[] mParams; private int mLastHashCode; private SignatureTrigger mMergedTrigger; private SignatureBackRef[] mBackrefs; + /** - * Create a signature with the given identifier and non-empty term array. The array may contain any CCTerms, not - * necessarily representatives. The array is copied defensively. + * Create a signature with the given identifier and non-empty parameter array. * * @param id * opaque identifier (e.g. function symbol for congruence, or trigger id for reverse triggers). - * @param terms - * non-empty array of CCTerms. + * @param params + * non-empty array of {@link CCParameter}s (bare {@link CCTerm}s for offset-free arguments). */ - public SignatureTrigger(final Object id, final CCTerm[] terms) { + public SignatureTrigger(final Object id, final CCParameter[] params) { mId = id; - mTerms = terms; + mParams = params; mLastHashCode = computeHashCode(); } + /** + * The effective offset of argument {@code index}: the parameter's offset relative to its representative. This is + * what determines, together with the representative, whether two signatures are congruent. + */ + private Rational effectiveOffset(final int index) { + return mParams[index].getOffsetToRep(); + } + public Object getId() { return mId; } - /** Returns the number of terms in this signature. */ + /** Returns the number of arguments in this signature. */ public int getTermCount() { - return mTerms.length; - } - - /** Returns the term at the given index. */ - public CCTerm getTerm(final int index) { - return mTerms[index]; + return mParams.length; } - /** Returns an unmodifiable list view of the terms. */ - public List getTerms() { - return Arrays.asList(mTerms); + /** Returns the argument at the given index. */ + public CCParameter getParam(final int index) { + return mParams[index]; } - public void rehash(CClosure engine, int argPosition, CCTerm oldRep, CCTerm newRep) { + public void rehash(CClosure engine, int argPosition, CCTerm oldRep, CCTerm newRep, Rational offsetDelta) { /* only if not merged */ if (mMergedTrigger == null) { assert mBackrefs != null; @@ -81,7 +91,7 @@ public void rehash(CClosure engine, int argPosition, CCTerm oldRep, CCTerm newRe assert mLastHashCode == computeHashCode(); engine.moveToSignatureTodo(this); } - recomputeHashCode(argPosition, oldRep, newRep); + recomputeHashCode(argPosition, oldRep, newRep, offsetDelta); } } @@ -119,17 +129,31 @@ public int hashCode() { public int computeHashCode() { int h = mId.hashCode(); - for (int i = 0; i < mTerms.length; i++) { - h ^= HashUtils.hashJenkins(i, mTerms[i].getRepresentative()); + for (int i = 0; i < mParams.length; i++) { + // Use disjoint salts (2*i for the representative, 2*i+1 for the offset) so the two contributions do not + // trivially cancel against each other. + h ^= HashUtils.hashJenkins(2 * i, mParams[i].getRepresentative()); + h ^= HashUtils.hashJenkins(2 * i + 1, effectiveOffset(i).hashCode()); } return h; } - public void recomputeHashCode(int argPos, CCTerm oldRep, CCTerm newRep) { - final int hOld = HashUtils.hashJenkins(argPos, oldRep.hashCode()); - final int hNew = HashUtils.hashJenkins(argPos, newRep.hashCode()); - // assert computeHashCode() == (mLastHashCode ^ hOld ^ hNew); - mLastHashCode ^= hOld ^ hNew; + /** + * Incrementally update the cached hash when argument {@code argPos}'s representative changes from {@code oldRep} to + * {@code newRep} and its offset to the representative changes by {@code offsetDelta}. This must be called before the + * term's {@code mOffsetToRep} is actually updated, so {@link #effectiveOffset(int)} still reflects the old value. + */ + public void recomputeHashCode(int argPos, CCTerm oldRep, CCTerm newRep, Rational offsetDelta) { + final int hOldRep = HashUtils.hashJenkins(2 * argPos, oldRep.hashCode()); + final int hNewRep = HashUtils.hashJenkins(2 * argPos, newRep.hashCode()); + mLastHashCode ^= hOldRep ^ hNewRep; + if (!offsetDelta.equals(Rational.ZERO)) { + final Rational oldEff = effectiveOffset(argPos); + final Rational newEff = oldEff.add(offsetDelta); + final int hOldOff = HashUtils.hashJenkins(2 * argPos + 1, oldEff.hashCode()); + final int hNewOff = HashUtils.hashJenkins(2 * argPos + 1, newEff.hashCode()); + mLastHashCode ^= hOldOff ^ hNewOff; + } } @Override @@ -141,11 +165,11 @@ public boolean equals(final Object obj) { return false; } final SignatureTrigger other = (SignatureTrigger) obj; - if (!mId.equals(other.mId) || mTerms.length != other.mTerms.length) { + if (!mId.equals(other.mId) || mParams.length != other.mParams.length) { return false; } - for (int i = 0; i < mTerms.length; i++) { - if (mTerms[i].getRepresentative() != other.mTerms[i].getRepresentative()) { + for (int i = 0; i < mParams.length; i++) { + if (!mParams[i].sameValueAs(other.mParams[i])) { return false; } } @@ -155,8 +179,8 @@ public boolean equals(final Object obj) { @Override public String toString() { final StringBuilder sb = new StringBuilder("Sig[").append(mId); - for (final CCTerm t : mTerms) { - sb.append(',').append(t); + for (final CCParameter p : mParams) { + sb.append(',').append(p); } return sb.append(']').toString(); } @@ -170,16 +194,16 @@ public boolean unmerge(CClosure cclosure) { } public void addBackrefs(CClosure cclosure) { - mBackrefs = new SignatureBackRef[mTerms.length]; - for (int i = 0; i < mTerms.length; i++) { + mBackrefs = new SignatureBackRef[mParams.length]; + for (int i = 0; i < mParams.length; i++) { mBackrefs[i] = new SignatureBackRef(this, i); - cclosure.addSignatureBackRef(mTerms[i], mBackrefs[i]); + cclosure.addSignatureBackRef(mParams[i].getCCTerm(), mBackrefs[i]); } } public void removeBackrefs(CClosure cclosure) { - for (int i = 0; i < mTerms.length; i++) { - cclosure.removeSignatureBackRef(mTerms[i], mBackrefs[i]); + for (int i = 0; i < mParams.length; i++) { + cclosure.removeSignatureBackRef(mParams[i].getCCTerm(), mBackrefs[i]); } mBackrefs = null; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/WeakCongruencePath.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/WeakCongruencePath.java index 350214bd0..6934ef5fe 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/WeakCongruencePath.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/WeakCongruencePath.java @@ -21,6 +21,7 @@ import java.util.HashSet; import java.util.LinkedHashSet; +import de.uni_freiburg.informatik.ultimate.logic.Rational; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.Clause; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.Literal; import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.LeafNode; @@ -56,24 +57,53 @@ public void update(final CCTerm term, final ArrayNode arrayNode) { } static class WeakSubPath extends SubPath { - private final CCTerm mIdx; - private final CCTerm mIdxRep; + private final CCParameter mIdx; + private final CCParameter mIdxRep; + /** + * The select/const edge justifying the single weak-congruence step on this path, if any: {@code mSelectLeft} is + * the select (or const value) on the {@code path[0]} side, {@code mSelectRight} the one on the {@code path[last]} + * side. Both {@code null} for plain weak paths (store/equality steps only), which is always the case for + * read-over-weakeq and read-const-weakeq. + */ + private CCParameter mSelectLeft; + private CCParameter mSelectRight; - public WeakSubPath(final CCTerm idx, final CCTerm start, final boolean produceProofs) { + public WeakSubPath(final CCParameter idx, final CCTerm start, final boolean produceProofs) { super(start, produceProofs); mIdx = idx; - mIdxRep = idx.getRepresentative(); + mIdxRep = idx.getValueKey(); + } + + public void setSelectEdge(final CCParameter selectLeft, final CCParameter selectRight) { + mSelectLeft = selectLeft; + mSelectRight = selectRight; } @Override public String toString() { return "Weakpath " + mIdx - + (mTermsOnPath == null ? "" : " " + mTermsOnPath.toString()); + + (mTermsOnPath == null ? "" : " " + mTermsOnPath.toString()) + + (mSelectLeft == null ? "" : " selectedge " + mSelectLeft + " <-> " + mSelectRight); } - public CCTerm getIndex() { + public CCParameter getIndex() { return mIdx; } + + public CCParameter[] getSelectEdge() { + if (mSelectLeft == null) { + return null; + } + return new CCParameter[] { mSelectLeft, mSelectRight }; + } + + public CCParameter getSelectLeft() { + return mSelectLeft; + } + + public CCParameter getSelectRight() { + return mSelectRight; + } } final ArrayTheory mArrayTheory; @@ -85,36 +115,42 @@ public WeakCongruencePath(final ArrayTheory arrayTheory) { public Clause computeSelectOverWeakEQ(final CCAppTerm select1, final CCAppTerm select2, final boolean produceProofs) { - final CCTerm i1 = ArrayTheory.getIndexFromSelect(select1); - final CCTerm i2 = ArrayTheory.getIndexFromSelect(select2); + final CCParameter i1 = ArrayTheory.getIndexFromSelect(select1); + final CCParameter i2 = ArrayTheory.getIndexFromSelect(select2); final CCTerm a = ArrayTheory.getArrayFromSelect(select1); final CCTerm b = ArrayTheory.getArrayFromSelect(select2); - computePath(i1, i2); + computePath(i1.getCCTerm(), i2.getCCTerm()); final WeakSubPath weakPath = computeWeakPath(a, b, i1, produceProofs); + drainTodo(); mAllPaths.addFirst(weakPath); - return generateClause(new SymmetricPair(select1, select2), produceProofs, RuleKind.READ_OVER_WEAKEQ); + return generateClause(new SymmetricPair(select1, select2), produceProofs, + RuleKind.READ_OVER_WEAKEQ); } public Clause computeSelectConstOverWeakEQ(final CCAppTerm select1, final CCAppTerm const2, final boolean produceProofs) { - final CCTerm value2 = ArrayTheory.getValueFromConst(const2); + final CCParameter value2 = ArrayTheory.getValueFromConst(const2); - final CCTerm i1 = ArrayTheory.getIndexFromSelect(select1); + final CCParameter i1 = ArrayTheory.getIndexFromSelect(select1); final CCTerm a = ArrayTheory.getArrayFromSelect(select1); final WeakSubPath weakPath = computeWeakPath(a, const2, i1, produceProofs); + drainTodo(); mAllPaths.addFirst(weakPath); return generateClause(new SymmetricPair<>(select1, value2), produceProofs, RuleKind.READ_CONST_WEAKEQ); } public Clause computeConstOverWeakEQ(final CCAppTerm const1, final CCAppTerm const2, final boolean produceProofs) { - final CCTerm value1 = ArrayTheory.getValueFromConst(const1); - final CCTerm value2 = ArrayTheory.getValueFromConst(const2); + final CCParameter value1 = ArrayTheory.getValueFromConst(const1); + final CCParameter value2 = ArrayTheory.getValueFromConst(const2); - final HashSet storeIndices = new HashSet<>(); + final HashSet storeIndices = new HashSet<>(); final Cursor start = new Cursor(const1, mArrayTheory.mCongRoots.get(const1.getRepresentative())); final Cursor dest = new Cursor(const2, mArrayTheory.mCongRoots.get(const2.getRepresentative())); final SubPath path = collectPathPrimary(start, dest, storeIndices, produceProofs); + // Drain the strong-path dependencies collectPathPrimary enqueued before prepending the main path, so the main + // path lands ahead of them in mAllPaths (the topological order the proof generator expects). + drainTodo(); mAllPaths.addFirst(path); return generateClause(new SymmetricPair<>(value1, value2), produceProofs, RuleKind.CONST_WEAKEQ); } @@ -134,14 +170,16 @@ public Clause computeConstOverWeakEQ(final CCAppTerm const1, final CCAppTerm con */ public Clause computeWeakeqExt(final CCTerm a, final CCTerm b, final boolean produceProofs) { assert a != b; - final LinkedHashSet storeIndices = new LinkedHashSet<>(); + final LinkedHashSet storeIndices = new LinkedHashSet<>(); final Cursor start = new Cursor(a, mArrayTheory.mCongRoots.get(a.getRepresentative())); final Cursor dest = new Cursor(b, mArrayTheory.mCongRoots.get(b.getRepresentative())); final SubPath path = collectPathPrimary(start, dest, storeIndices, produceProofs); - for (final CCTerm idx : storeIndices) { + for (final CCParameter idx : storeIndices) { final WeakSubPath weakpath = computeWeakCongruencePath(a, b, idx, produceProofs); + drainTodo(); mAllPaths.addFirst(weakpath); } + drainTodo(); mAllPaths.addFirst(path); return generateClause(new SymmetricPair<>(a, b), produceProofs, RuleKind.WEAKEQ_EXT); } @@ -160,10 +198,10 @@ public Clause computeWeakeqExt(final CCTerm a, final CCTerm b, final boolean pro * Proof production enabled? * @return Path information for this weak path. */ - private WeakSubPath computeWeakPath(final CCTerm array1, final CCTerm array2, final CCTerm index, final boolean produceProofs) { + private WeakSubPath computeWeakPath(final CCTerm array1, final CCTerm array2, final CCParameter index, final boolean produceProofs) { - final LinkedHashSet storeIndices = new LinkedHashSet<>(); - final CCTerm indexRep = index.getRepresentative(); + final LinkedHashSet storeIndices = new LinkedHashSet<>(); + final CCParameter indexRep = index.getValueKey(); final Cursor cursor1 = new Cursor(array1, mArrayTheory.mCongRoots.get(array1.getRepresentative())); final Cursor cursor2 = new Cursor(array2, mArrayTheory.mCongRoots.get(array2.getRepresentative())); final WeakSubPath sub1 = new WeakSubPath(index, array1, produceProofs); @@ -186,7 +224,7 @@ private WeakSubPath computeWeakPath(final CCTerm array1, final CCTerm array2, fi } sub1.addSubPath(collectPathPrimary(cursor1, cursor2, storeIndices, produceProofs)); sub1.addSubPath(sub2); - for (final CCTerm storeIdx : storeIndices) { + for (final CCParameter storeIdx : storeIndices) { computeIndexDiseq(index, storeIdx); } return sub1; @@ -206,9 +244,9 @@ private WeakSubPath computeWeakPath(final CCTerm array1, final CCTerm array2, fi * Proof production enabled? * @return Path information for this weak path. Other paths are added as side-effect to mAllPaths. */ - private WeakSubPath computeWeakCongruencePath(final CCTerm array1, final CCTerm array2, final CCTerm index, final boolean produceProofs) { + private WeakSubPath computeWeakCongruencePath(final CCTerm array1, final CCTerm array2, final CCParameter index, final boolean produceProofs) { - final CCTerm indexRep = index.getRepresentative(); + final CCParameter indexRep = index.getValueKey(); final ArrayNode node1 = mArrayTheory.mCongRoots.get(array1.getRepresentative()); final ArrayNode node2 = mArrayTheory.mCongRoots.get(array2.getRepresentative()); final ArrayNode rep1 = node1.getWeakIRepresentative(indexRep); @@ -217,7 +255,7 @@ private WeakSubPath computeWeakCongruencePath(final CCTerm array1, final CCTerm return computeWeakPath(array1, array2, index, produceProofs); } WeakSubPath path; - CCTerm select1, select2; + CCParameter select1, select2; // compute weak path from array1 to left-hand-side of select edge if (rep1.mConstTerm != null) { // const array on left-hand-side @@ -246,6 +284,9 @@ private WeakSubPath computeWeakCongruencePath(final CCTerm array1, final CCTerm path.addSubPath(computeWeakPath(selectArray, array2, index, produceProofs)); select2 = select; } + // Record the select/const edge so the proof/interpolation consumers need not search for it. select1 is on the + // array1 (path[0]) side, select2 on the array2 (path[last]) side, matching the emitted path order. + path.setSelectEdge(select1, select2); // compute the path between the selects or between select and constant value. assert select1.getRepresentative() == select2.getRepresentative(); // check for trivial select edge (select-const). @@ -255,7 +296,7 @@ private WeakSubPath computeWeakCongruencePath(final CCTerm array1, final CCTerm return path; } - public void collectPathOnePrimary(final Cursor cursor, final SubPath path, final HashSet storeIndices) { + public void collectPathOnePrimary(final Cursor cursor, final SubPath path, final HashSet storeIndices) { final ArrayNode node = cursor.mArrayNode; final CCAppTerm store = node.mPrimaryStore; CCTerm t1 = store; @@ -268,9 +309,12 @@ public void collectPathOnePrimary(final Cursor cursor, final SubPath path, final assert t1.mRepStar == node.mTerm; assert t2.mRepStar == node.mPrimaryEdge.mTerm; if (cursor.mTerm != t1) { - computePath(cursor.mTerm, t1); - final SubPath subpath = mAllPaths.removeFirst(); - path.addSubPath(subpath); + // Build the strong path directly and inline it into the weak path. computePathNonRecursive is a pure + // builder: the result is not cached in mVisited (only drainTodo caches), so it is inlined here rather than + // becoming a standalone subpath, while its congruence dependencies are enqueued for the enclosing lemma's + // drainTodo to collect. Not caching it lets a later standalone request for the same edge rebuild it through + // the drain, so it is collected after its dependencies. + path.addSubPath(computePathNonRecursive(cursor.mTerm, t1)); } path.addEntry(t2, null); cursor.update(t2, node.mPrimaryEdge); @@ -286,7 +330,7 @@ public void collectPathOnePrimary(final Cursor cursor, final SubPath path, final * @param produceProofs true, if path must be recorded. * @returns the path connecting the arrays. */ - private SubPath collectPathPrimary(final Cursor start, final Cursor dest, final HashSet storeIndices, + private SubPath collectPathPrimary(final Cursor start, final Cursor dest, final HashSet storeIndices, final boolean produceProofs) { final SubPath path1 = new SubPath(start.mTerm, produceProofs); final SubPath path2 = new SubPath(dest.mTerm, produceProofs); @@ -305,9 +349,10 @@ private SubPath collectPathPrimary(final Cursor start, final Cursor dest, final collectPathOnePrimary(dest, path2, storeIndices); } if (start.mTerm != dest.mTerm) { - computePath(start.mTerm, dest.mTerm); - final SubPath subpath = mAllPaths.removeFirst(); - path1.addSubPath(subpath); + // Build the strong path directly and inline it (see collectPathOnePrimary): computePathNonRecursive is a + // pure builder whose result is not cached in mVisited, so it is inlined here rather than becoming a + // standalone subpath, while its dependencies are enqueued for the enclosing drainTodo. + path1.addSubPath(computePathNonRecursive(start.mTerm, dest.mTerm)); } path1.addSubPath(path2); return path1; @@ -326,7 +371,7 @@ private SubPath collectPathPrimary(final Cursor start, final Cursor dest, final * @param produceProofs * true, if path must be recorded. */ - private void collectPathOneSecondary(final Cursor cursor, final WeakSubPath path, final HashSet storeIndices, + private void collectPathOneSecondary(final Cursor cursor, final WeakSubPath path, final HashSet storeIndices, final boolean produceProofs) { final ArrayNode selector = cursor.mArrayNode.findSecondaryNode(path.mIdxRep); final CCAppTerm store = selector.mSecondaryStore; @@ -356,15 +401,27 @@ private void collectPathOneSecondary(final Cursor cursor, final WeakSubPath path * @param idx The index for a weak equality path. * @param idxFromStore The index of an edge in the weakeq graph. */ - private void computeIndexDiseq(final CCTerm idx, final CCTerm idxFromStore) { - final CCEquality eqlit = mArrayTheory.getCClosure().createEquality(idx, idxFromStore, false); + private void computeIndexDiseq(final CCParameter idx, final CCParameter idxFromStore) { + if (idx.getCCTerm() == idxFromStore.getCCTerm()) { + // the two index values share the same CCTerm but differ by a constant offset, so they can never be equal; + // the disequality holds unconditionally and contributes no literal to the lemma. + return; + } + final Rational offset = idxFromStore.getOffset().sub(idx.getOffset()); + final CCEquality eqlit = mArrayTheory.getCClosure().createEquality(idx.getCCTerm(), idxFromStore.getCCTerm(), + offset, false); if (eqlit != null) { mAllLiterals.add(eqlit.negate()); } } - private Clause generateClause(final SymmetricPair diseq, final boolean produceProofs, final RuleKind rule) { + private Clause generateClause(final SymmetricPair diseq, final boolean produceProofs, + final RuleKind rule) { if (diseq != null) { + // Pass the CCParameters (with offsets), not the bare CCTerms: a propagated array equality may be an offset + // equality (e.g. select(a,j) == select(a,j)+1 in trivialdiseqarray). Dropping the offset would build the + // degenerate select(a,j) == select(a,j) and trip the createEquality assertion; keeping it yields the false + // proxy (eq == null), so the lemma becomes the pure conflict clause over its premises. final CCEquality eq = mArrayTheory.getCClosure().createEquality(diseq.getFirst(), diseq.getSecond(), false); if (eq != null) { // Note that it can actually happen that diseq is already in @@ -386,7 +443,7 @@ private Clause generateClause(final SymmetricPair diseq, final boolean p return c; } - private CCAnnotation createAnnotation(final SymmetricPair diseq, final RuleKind rule) { + private CCAnnotation createAnnotation(final SymmetricPair diseq, final RuleKind rule) { return new CCAnnotation(diseq, mAllPaths, rule); } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/linar/LinArSolve.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/linar/LinArSolve.java index 01686fbd7..766612ed8 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/linar/LinArSolve.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/linar/LinArSolve.java @@ -22,6 +22,7 @@ import java.util.ArrayDeque; import java.util.ArrayList; import java.util.BitSet; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; @@ -56,7 +57,9 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.model.SharedTermEvaluator; import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.LeafNode; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCEquality; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CClosure; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.ScopedArrayList; import de.uni_freiburg.informatik.ultimate.smtinterpol.util.SymmetricPair; import de.uni_freiburg.informatik.ultimate.util.datastructures.ScopedHashMap; @@ -700,12 +703,99 @@ public Clause finalCheck() { } } if (mSuggestions.isEmpty() && mProplist.isEmpty()) { + if (mClausifier.createOffsetEqualities()) { + // Offset equalities: shared terms are offset-free, so whole-term mbtc (which groups by value) + // would wrongly merge terms that share an offset-free value but differ by a constant. MBTC instead + // ranges over numeric clash slots and proposes per-argument offset equalities. + return mbtcClashSlots(); + } return mbtc(cong); } assert compositesSatisfied(); return null; } + /** + * The value of a numeric clash-slot member as an exact infinitesimal number. The member's congruence class carries + * its linear-arithmetic value on the representative's shared term ({@code rep.getSharedTerm()}), which need not be + * the representative itself, so its offset-to-rep is folded back in: + * + *

+	 *   value(param) = sharedTermValue(rep.mSharedTerm)   // LA value of the offset-free shared term
+	 *                - rep.mSharedTerm.getOffsetToRep()    // shift back to the representative
+	 *                + param.getOffsetToRep()              // shift out to the member
+	 * 
+ * + * The caller guarantees {@code param.getRepresentative().getSharedTerm() != null} (clash slots drop LA-free + * members). + */ + private ExactInfinitesimalNumber clashMemberValue(final CCParameter param) { + final CCTerm rep = param.getRepresentative(); + final CCTerm sharedCC = rep.getSharedTerm(); + final LASharedTerm laShared = mClausifier.getLATerm(sharedCC.getFlatTerm()); + final ExactInfinitesimalNumber repValue = + sharedTermValue(laShared).sub(new ExactInfinitesimalNumber(sharedCC.getOffsetToRep())); + return repValue.add(new ExactInfinitesimalNumber(param.getOffsetToRep())); + } + + /** + * Model-based theory combination over numeric clash slots (offset-equality mode). For each slot + * (see {@link CClosure#getNumericClashSlots()}) the members are grouped by value; two members with equal value but + * in distinct affine classes get an offset equality proposed, encoded as an offset CCEquality between their base + * CCTerms ({@code value(param) == value(other)}). The equality is propagated when already implied or suggested as a + * decision otherwise, mirroring {@link #mbtc}. + */ + private Clause mbtcClashSlots() { + final Collection> slots = mClausifier.getCClosure().getNumericClashSlots(); + for (final List slot : slots) { + if (slot.size() <= 1) { + continue; + } + final Map byValue = new HashMap<>(); + for (final CCParameter param : slot) { + final ExactInfinitesimalNumber value = clashMemberValue(param); + final CCParameter other = byValue.get(value); + if (other == null) { + byValue.put(value, param); + } else if (!param.sameValueAs(other)) { + // Equal value but distinct affine classes: propose value(param) == value(other) as an offset + // CCEquality between the two base CCTerms (offset = other.getOffset() - param.getOffset()). + final CCEquality cceq = mClausifier.getCClosure().createEquality(param, other, true); + if (cceq == null) { + // false proxy: the two terms can never be equal; cannot happen for equal-valued members. + continue; + } + final Clause conflict = suggestOrPropagate(cceq); + if (conflict != null) { + return conflict; + } + } + } + } + return null; + } + + /** + * Hand a model-based equality to the engine: propagate it if its truth value is already implied (or report the + * conflict if it was decided false), otherwise suggest the linked LA equality as a decision. Shared by clash-slot + * MBTC and the legacy whole-term {@link #mbtc}. + */ + private Clause suggestOrPropagate(final CCEquality cceq) { + if (cceq.getLASharedData().getDecideStatus() != null) { + if (cceq.getDecideStatus() == cceq.negate()) { + return generateEqualityClause(cceq); + } else if (cceq.getDecideStatus() == null) { + mProplist.add(cceq); + } else { + mClausifier.getLogger().debug("already set: %s", cceq.getAtom().getDecideStatus()); + } + } else { + mClausifier.getLogger().debug("MBTC: Suggesting literal %s", cceq); + mSuggestions.add(cceq.getLASharedData()); + } + return null; + } + @Override public int checkCompleteness() { if (mHasNonLinearVar >= 0) { @@ -1192,22 +1282,62 @@ public Map fingerprintSharedVar(LASharedTerm shared) { } } fpr.add(shared.getOffset()); - return fpr.getSummands(); + final Map fingerprint = fpr.getSummands(); + if (mClausifier.createOffsetEqualities()) { + // Ignore the constant part (accumulated under the null key from the offset and from any fixed + // variables): two shared terms then collide when their non-constant parts agree, i.e. they are + // provably equal up to a constant. That constant is an offset equality congruence closure can use; the + // exact offset is recovered from the value difference at the collision site in + // propagateSharedEqualities. + fingerprint.remove(null); + } + return fingerprint; } public Clause propagateSharedEquality(LASharedTerm lhs, LASharedTerm rhs, HashSet> propagated) { - final CCTerm lhsCC = mClausifier.getCCTerm(lhs.getTerm()).getRepresentative(); - final CCTerm rhsCC = mClausifier.getCCTerm(rhs.getTerm()).getRepresentative(); - if (lhsCC == rhsCC || !propagated.add(new SymmetricPair(lhsCC, rhsCC))) { + return propagateSharedEquality(lhs, rhs, Rational.ZERO, propagated); + } + + /** + * Propagate that two shared terms have values differing by a constant, i.e. {@code value(lhs) == value(rhs) + + * offset}. With a zero offset this is the classic shared-term equality; with a non-zero offset (only created when + * offset equalities are enabled) it states {@code lhs == rhs + offset} as an offset CCEquality, so congruence + * closure can merge the two terms at that offset. + */ + public Clause propagateSharedEquality(LASharedTerm lhs, LASharedTerm rhs, Rational offset, + HashSet> propagated) { + final CCTerm lhsCCTerm = mClausifier.getCCTerm(lhs.getTerm()); + final CCTerm rhsCCTerm = mClausifier.getCCTerm(rhs.getTerm()); + final CCTerm lhsCC = lhsCCTerm.getRepresentative(); + final CCTerm rhsCC = rhsCCTerm.getRepresentative(); + if (lhsCC == rhsCC && lhsCCTerm.getOffsetToRep().sub(rhsCCTerm.getOffsetToRep()).equals(offset)) { + // lhs and rhs are already in the same affine class at exactly this offset: nothing to propagate. + return null; + } + if (!propagated.add(new SymmetricPair(lhsCC, rhsCC))) { + // Already handled this (representative) pair in this round. This also deduplicates the inconsistent + // same-class/different-offset case (a self-pair (rep, rep)), so the offset conflict that the CCEquality + // propagation below triggers in congruence closure is created only once. + return null; + } + // Encode the offset into the right-hand term so the EqualityProxy and resulting CCEquality carry it. + Term rhsTerm = rhs.getTerm(); + if (!offset.equals(Rational.ZERO)) { + rhsTerm = mClausifier.addConstantToTerm(rhsTerm, offset); + } + final EqualityProxy eq = mClausifier.createEqualityProxy(lhs.getTerm(), rhsTerm, null); + if (eq == EqualityProxy.getTrueProxy()) { + // lhs and rhs + offset are the same term: the offset equality is a tautology between two distinct constant + // terms (offset-equivalent non-constant terms already share a CCTerm and hit the lhsCC == rhsCC return + // above). There is nothing for congruence closure to merge. return null; } - final EqualityProxy eq = mClausifier.createEqualityProxy(lhs.getTerm(), rhs.getTerm(), null); - assert eq != EqualityProxy.getTrueProxy(); if (eq == EqualityProxy.getFalseProxy()) { // We found a conflict while trying to propagate a shared equality. // This can happen if the difference between the shared terms cannot be an integer. // We insert the difference as new basic in the tableau and let bound propagation do the rest. + // The constant offset does not affect integrality, so the difference variable is lhs - rhs. final MutableAffineTerm at = new MutableAffineTerm(); at.addMap(Rational.ONE, lhs.getSummands()); at.addMap(Rational.MONE, rhs.getSummands()); @@ -1216,7 +1346,9 @@ public Clause propagateSharedEquality(LASharedTerm lhs, LASharedTerm rhs, assert mDirty.get(var.mMatrixpos); return null; } - final CCEquality cceq = eq.createCCEquality(lhs.getTerm(), rhs.getTerm()); + // The CCEquality is between the offset-free CCTerms of lhs and rhs at the known offset; the synthesized + // rhsTerm above only carries the offset for the EqualityProxy/atom identity, not the CC node lookup. + final CCEquality cceq = eq.createCCEquality(lhsCCTerm, rhsCCTerm, offset); final LAEquality laeq = cceq.getLASharedData(); mClausifier.getLogger().debug("Propagate: %s (laeq: %s) %s %s", cceq, laeq, cceq.getDecideStatus(), laeq.getDecideStatus()); @@ -1249,22 +1381,44 @@ public Clause propagateSharedEqualities() { mLastNumFixed = numFix; mClausifier.getLogger().debug("Shared Terms: %d, Matrix Size: %d + %d, Num Fixed: %d", mSharedVars.size(), mLinvars.size() - mBasics.size(), mBasics.size(), mLastNumFixed); + // Snapshot the shared terms: propagating an offset equality synthesizes a term (rhs + offset) and shares + // it, which appends to mSharedVars. The snapshot avoids a ConcurrentModificationException; the newly shared + // term is offset-free-equivalent to an existing one and is picked up on the next call (gated by mLastNumFixed), + // where the offset-aware guard in propagateSharedEquality recognizes it as already known. + final List sharedSnapshot = new ArrayList<>(mSharedVars); // TODO: store sharedVars already separated by sorts. final Set sharedVarSorts = new LinkedHashSet(); - for (final LASharedTerm shared : mSharedVars) { + for (final LASharedTerm shared : sharedSnapshot) { sharedVarSorts.add(shared.getTerm().getSort()); } + // When offset equalities are enabled the fingerprint ignores the constant part (see fingerprintSharedVar), + // so two shared terms collide when their non-constant parts agree, i.e. they are provably equal up to a + // constant. That constant is the offset of the propagated (offset) CCEquality. When offset equalities are + // disabled the fingerprint keeps the constant, terms collide only when provably equal, and the offset is zero. for (final Sort sort : sharedVarSorts) { final Map, LASharedTerm> fingerprints = new HashMap<>(); final HashSet> propagated = new HashSet<>(); - for (final LASharedTerm shared : mSharedVars) { + for (final LASharedTerm shared : sharedSnapshot) { if (shared.getTerm().getSort() == sort) { - final Map fingerprint = fingerprintSharedVar(shared); + final Map fingerprint = fingerprintSharedVar(shared); final LASharedTerm other = fingerprints.get(fingerprint); if (other == null) { fingerprints.put(fingerprint, shared); } else { - final Clause conflict = propagateSharedEquality(other, shared, propagated); + // other and shared have equal non-constant parts (the fingerprints collided), so they + // differ by a fixed constant: value(other) == value(shared) + offset. The non-constant + // parts cancel, so the value difference is exact and model-independent. + // The propagated offset is the difference of the terms' full SMT values. With offset + // equalities the LASharedTerm value is offset-free (the constant is dropped from both the value + // and the fingerprint), so the term constants are added back here; getTermConstant is zero when + // offset equalities are disabled, where sharedTermValue already carries the constant. + final Rational constDiff = mClausifier.getTermConstant(other.getTerm()) + .sub(mClausifier.getTermConstant(shared.getTerm())); + final ExactInfinitesimalNumber diff = sharedTermValue(other).sub(sharedTermValue(shared)) + .add(new ExactInfinitesimalNumber(constDiff)); + assert diff.getEpsilon().signum() == 0; + final Clause conflict = + propagateSharedEquality(other, shared, diff.getRealValue(), propagated); if (conflict != null || !mDirty.isEmpty()) { return conflict; } @@ -1370,6 +1524,21 @@ private void prepareModel() { } confl.add(new ExactInfinitesimalNumber(value.getRealValue())); } + // Also keep the clash-slot members (offset-shifted use-site values) from accidentally coinciding: the + // offset-free shared terms above no longer carry the per-argument constant once offset equalities are on. + for (final ModelSharedPoint pt : clashModelPoints()) { + ExactInfinitesimalNumber value = new ExactInfinitesimalNumber(pt.mOffset); + for (final Entry entry : pt.mSummands.entrySet()) { + value = value.add(entry.getKey().getValue().mul(entry.getValue())); + } + final Rational eps = value.getEpsilon(); + Set confl = sharedPoints.get(eps); + if (confl == null) { + confl = new TreeSet<>(); + sharedPoints.put(eps, confl); + } + confl.add(new ExactInfinitesimalNumber(value.getRealValue())); + } // If we cannot choose the current value since we would violate a // disequality, choose a different number. while (prohibitions.contains(mEps) || hasSharing(sharedPoints, new ExactInfinitesimalNumber(mEps))) { @@ -1786,6 +1955,74 @@ private ExactInfinitesimalNumber[] freedom(final LinVar var) { return new ExactInfinitesimalNumber[] { min, max }; } + /** + * A model "shared point": a value {@code offset + sum(summands)} whose accidental coincidence with another such + * point must be avoided during model construction. Besides the registered shared terms these include the numeric + * clash-slot members (function arguments): with offset equalities a function argument's use-site value carries a + * structural offset that the offset-free shared term no longer holds, so the offset is reattached here. + */ + private static final class ModelSharedPoint { + final Map mSummands; + final Rational mOffset; + + ModelSharedPoint(final Map summands, final Rational offset) { + mSummands = summands; + mOffset = offset; + } + } + + /** + * Collect the numeric clash-slot members as model shared points carrying their offset-shifted use-site value + * ({@code value(member) == value(rep) + (member.offsetToRep - sharedTerm.offsetToRep) + sum(sharedTerm summands)}). + * Empty unless offset equalities are enabled: without offsets the shared terms already carry their full use-site + * value, so whole-term collision avoidance suffices. + */ + private List clashModelPoints() { + final List points = new ArrayList<>(); + if (!mClausifier.createOffsetEqualities()) { + return points; + } + for (final List slot : mClausifier.getCClosure().getNumericClashSlots()) { + for (final CCParameter param : slot) { + final CCTerm rep = param.getRepresentative(); + final CCTerm sharedCC = rep.getSharedTerm(); + final LASharedTerm laShared = mClausifier.getLATerm(sharedCC.getFlatTerm()); + if (laShared == null) { + continue; + } + // laShared is offset-free, so its own value is sum(summands); the structural part of value(member) + // relative to it is (member.offsetToRep - sharedTerm.offsetToRep). laShared.getOffset() is zero here. + final Rational offset = param.getOffsetToRep().sub(sharedCC.getOffsetToRep()).add(laShared.getOffset()); + points.add(new ModelSharedPoint(laShared.getSummands(), offset)); + } + } + return points; + } + + /** + * Add one model shared point (its current value and its sensitivity to the mutating variable) to the slope-keyed + * {@code sharedPoints} map used by {@link #choose}. + */ + private void addModelSharedPoint(final Map summands, final Rational offset, + final Map basicFactors, final Map> sharedPoints) { + Rational sharedCoeff = Rational.ZERO; + ExactInfinitesimalNumber sharedCurVal = new ExactInfinitesimalNumber(offset, Rational.ZERO); + for (final Entry entry : summands.entrySet()) { + final LinVar lv = entry.getKey(); + final Rational factor = entry.getValue(); + if (basicFactors.containsKey(lv)) { + sharedCoeff = sharedCoeff.addmul(basicFactors.get(lv), factor); + } + sharedCurVal = sharedCurVal.add(lv.getValue().mul(factor)); + } + Set set = sharedPoints.get(sharedCoeff); + if (set == null) { + set = new TreeSet<>(); + sharedPoints.put(sharedCoeff, set); + } + set.add(sharedCurVal); + } + /** * Mutate a model such that less variables have the same value. * @@ -1795,6 +2032,7 @@ private ExactInfinitesimalNumber[] freedom(final LinVar var) { private void mutate() { final Map> sharedPoints = new TreeMap<>(); final Set prohib = new TreeSet<>(); + final List clashPoints = clashModelPoints(); for (final LinVar mutatingLV : mLinvars) { if (mutatingLV.mBasic || mutatingLV.getTightUpperBound().equals(mutatingLV.getTightLowerBound())) { // variable is basic or is fixed by its own constraints @@ -1844,25 +2082,14 @@ private void mutate() { } } - // Do not merge two shared variables + // Do not merge two shared variables (offset-free) or two clash-slot members (offset-shifted use-site + // values). The clash points restore the per-argument distinctness that whole-term sharing provided + // before offset equalities; without them a function would be modelled inconsistently. for (final LASharedTerm sharedVar : mSharedVars) { - Rational sharedCoeff = Rational.ZERO; - ExactInfinitesimalNumber sharedCurVal = new ExactInfinitesimalNumber(sharedVar.getOffset(), - Rational.ZERO); - for (final Entry entry : sharedVar.getSummands().entrySet()) { - final LinVar lv = entry.getKey(); - final Rational factor = entry.getValue(); - if (basicFactors.containsKey(lv)) { - sharedCoeff = sharedCoeff.addmul(basicFactors.get(lv), factor); - } - sharedCurVal = sharedCurVal.add(lv.getValue().mul(factor)); - } - Set set = sharedPoints.get(sharedCoeff); - if (set == null) { - set = new TreeSet<>(); - sharedPoints.put(sharedCoeff, set); - } - set.add(sharedCurVal); + addModelSharedPoint(sharedVar.getSummands(), sharedVar.getOffset(), basicFactors, sharedPoints); + } + for (final ModelSharedPoint pt : clashPoints) { + addModelSharedPoint(pt.mSummands, pt.mOffset, basicFactors, sharedPoints); } // If there is no integer constraint for the non-basic manipulate // it by eps, otherwise incrementing by a multiple of gcd.inverse() @@ -1876,6 +2103,17 @@ private void mutate() { } } + /** + * Compute the current value of a shared term: its constant offset plus the weighted values of its summands. + */ + private ExactInfinitesimalNumber sharedTermValue(final LASharedTerm shared) { + ExactInfinitesimalNumber value = new ExactInfinitesimalNumber(shared.getOffset()); + for (final Entry entry : shared.getSummands().entrySet()) { + value = value.add(entry.getKey().getValue().mul(entry.getValue())); + } + return value; + } + /** * Compute the value of each shared variable as exact infinite number. * @@ -1886,12 +2124,7 @@ Map> getSharedCongruences() { mClausifier.getLogger().debug("Shared Vars:"); final Map> result = new HashMap<>(); for (final LASharedTerm shared : mSharedVars) { - ExactInfinitesimalNumber value = new ExactInfinitesimalNumber(shared.getOffset()); - for (final Entry entry : shared.getSummands().entrySet()) { - final LinVar lv = entry.getKey(); - final Rational factor = entry.getValue(); - value = value.add(lv.getValue().mul(factor)); - } + final ExactInfinitesimalNumber value = sharedTermValue(shared); mClausifier.getLogger().debug("%s = %s", shared, value); List slot = result.get(value); if (slot == null) { @@ -2094,17 +2327,9 @@ private Clause mbtc(final Map> cong assert eq != EqualityProxy.getTrueProxy(); assert eq != EqualityProxy.getFalseProxy(); cceq = eq.createCCEquality(lhs, rhs); - if (cceq.getLASharedData().getDecideStatus() != null) { // NOPMD - if (cceq.getDecideStatus() == cceq.negate()) { - return generateEqualityClause(cceq); - } else if (cceq.getDecideStatus() == null) { - mProplist.add(cceq); - } else { - mClausifier.getLogger().debug("already set: %s", cceq.getAtom().getDecideStatus()); - } - } else { - mClausifier.getLogger().debug("MBTC: Suggesting literal %s", cceq); - mSuggestions.add(cceq.getLASharedData()); + final Clause conflict = suggestOrPropagate(cceq); + if (conflict != null) { + return conflict; } } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/InstantiationManager.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/InstantiationManager.java index 5f8bc60f5..c945c5bee 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/InstantiationManager.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/InstantiationManager.java @@ -42,6 +42,7 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.Config; import de.uni_freiburg.informatik.ultimate.smtinterpol.convert.Clausifier; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.Literal; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.epr.util.Pair; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.linar.InfinitesimalNumber; @@ -550,8 +551,8 @@ private Set> computeSubstitutionsForAge(final List[] sortedSubs * @return the age of the CCTerm if the term has a CCTerm, 0 else. */ private int getTermAge(final Term t) { - final CCTerm cc = mClausifier.getCCTerm(t); - return cc != null ? cc.getAge() : 0; + final CCParameter cc = mClausifier.getCCParameter(t); + return cc != null ? cc.getCCTerm().getAge() : 0; } /** @@ -756,9 +757,8 @@ private Dawg computeEMatchingLitDawg(final QuantLiteral private SubstitutionInfo mapToFirstChecked(final SubstitutionInfo first, final SubstitutionInfo second) { if (Config.EXPENSIVE_ASSERTS) { assert first.getEquivalentCCTerms().keySet().equals(second.getEquivalentCCTerms().keySet()); - for (final Entry equi : first.getEquivalentCCTerms().entrySet()) { - assert first.getEquivalentCCTerms().get(equi.getKey()).getRepresentative() == second - .getEquivalentCCTerms().get(equi.getKey()).getRepresentative(); + for (final Entry equi : first.getEquivalentCCTerms().entrySet()) { + assert equi.getValue().sameValueAs(second.getEquivalentCCTerms().get(equi.getKey())); } } return first; @@ -768,10 +768,10 @@ private List getTermSubsFromSubsInfo(final QuantLiteral qLit, final Substi final int length = qLit.getClause().getVars().length; final List termSubs = new ArrayList<>(); if (!info.equals(mEMatching.getEmptySubs())) { - final List ccSubs = info.getVarSubs(); + final List ccSubs = info.getVarSubs(); assert ccSubs.size() == length; for (int i = 0; i < length; i++) { - final CCTerm ccTerm = ccSubs.get(i); + final CCParameter ccTerm = ccSubs.get(i); termSubs.add(ccTerm == null ? null : ccTerm.getFlatTerm()); } } @@ -842,15 +842,15 @@ private InstantiationInfo evaluateLitForPartialEMatchingSubsInfo(final QuantLite final List clauseVarSubs = clauseInstInfo.getSubs(); if (clauseInstInfo.getInstValue() != InstanceValue.IRRELEVANT && !clauseVarSubs.isEmpty()) { // Complete the equivalent term map from info by adding the variable substitution from the key - final Map equivalentTerms = new HashMap<>(); + final Map equivalentTerms = new HashMap<>(); equivalentTerms.putAll(litSubsInfo.getEquivalentCCTerms()); for (int i = 0; i < clauseVars.length; i++) { - final CCTerm clauseSubs = mClausifier.getCCTerm(clauseVarSubs.get(i)); + final CCParameter clauseSubs = mClausifier.getCCParameter(clauseVarSubs.get(i)); if (clauseSubs != null) { - final CCTerm litSubs = + final CCParameter litSubs = litSubsInfo.equals(mEMatching.getEmptySubs()) ? null : litSubsInfo.getVarSubs().get(i); if (litSubs != null) { // If the substitutionInfo has a substitution for the variable, keep it. - assert litSubs.getRepresentative().equals(clauseSubs.getRepresentative()); + assert litSubs.sameValueAs(clauseSubs); } else { // Use the subs from the clause. equivalentTerms.put(clauseVars[i], clauseSubs); } @@ -1066,10 +1066,10 @@ private Collection getRelevantSubsFromDawg(final QuantClause /** * Helper method to build a map from Term to Term, given a map from Term to CCTerm. */ - private Map buildSharedMapFromCCMap(final Map ccMap) { + private Map buildSharedMapFromCCMap(final Map ccMap) { final Map sharedMap = new HashMap<>(); - for (final Entry entry : ccMap.entrySet()) { - final CCTerm ccTerm = entry.getValue(); + for (final Entry entry : ccMap.entrySet()) { + final CCParameter ccTerm = entry.getValue(); final Term term = ccTerm.getFlatTerm(); sharedMap.put(entry.getKey(), term); } @@ -1356,15 +1356,16 @@ private InstClause computeClauseInstance(final QuantClause clause, final List equivalentCCTerms) { - final CCTerm leftCC, rightCC; + private InstanceValue evaluateCCEqualityKnownShared(final QuantEquality qEq, + final Map equivalentCCTerms) { + final CCParameter leftCC, rightCC; if (qEq.getLhs().getFreeVars().length == 0) { - leftCC = mClausifier.getCCTerm(qEq.getLhs()); + leftCC = mClausifier.getCCParameter(qEq.getLhs()); } else { leftCC = equivalentCCTerms.get(qEq.getLhs()); } if (qEq.getRhs().getFreeVars().length == 0) { - rightCC = mClausifier.getCCTerm(qEq.getRhs()); + rightCC = mClausifier.getCCParameter(qEq.getRhs()); } else { rightCC = equivalentCCTerms.get(qEq.getRhs()); } @@ -1395,8 +1396,8 @@ private InstanceValue evaluateCCEquality(final QuantEquality qEq, final List= 0; i = pos.nextSetBit(i + 1)) { - for (CCAppTerm appTerm : mQuantTheory.getCClosure().getAllFuncApps(func)) { - interestingTerms.add(appTerm.getArgument(i).getFlatTerm()); + for (final CCAppTerm appTerm : mQuantTheory.getCClosure().getAllFuncApps(func)) { + interestingTerms.add(appTerm.getArgParam(i).getFlatTerm()); } } } @@ -460,7 +461,8 @@ private void updateInterestingTermsForFuncArgs(final TermVariable var, final int final String funcName = func.getName(); final Term[] args = arrayFuncTerm.getParameters(); final Term array = args[0]; - final CCTerm arrayCC = mQuantTheory.getCClosure().getCCTermRep(array); + // arrays are not numeric, so the value is a bare CCTerm (the cast checks this) + final CCTerm arrayCC = (CCTerm) mQuantTheory.getCClosure().getCCParamRep(array); final CCTerm weakRep = arrayCC == null ? null : mQuantTheory.getClausifier().getArrayTheory().getWeakRep(arrayCC); if (args[1] == var) { // The variable is an array index @@ -482,7 +484,7 @@ private void updateInterestingTermsForFuncArgs(final TermVariable var, final int final CCTerm stArr = ArrayTheory.getArrayFromStore(st); if (weakRep == null ? stArr.getFlatTerm().getSort() == array.getSort() : weakRep == mQuantTheory.getClausifier().getArrayTheory().getWeakRep(stArr)) { - final Term indexTerm = ArrayTheory.getIndexFromStore((CCAppTerm) st).getFlatTerm(); + final Term indexTerm = ArrayTheory.getIndexFromStore(st).getFlatTerm(); interestingTerms.add(indexTerm); if (funcName == "select" && var.getSort().getName() == "Int" && !QuantUtil.isLambda(indexTerm)) { @@ -506,7 +508,7 @@ private void updateInterestingTermsForFuncArgs(final TermVariable var, final int final CCTerm selArr = ArrayTheory.getArrayFromSelect(sel); if (weakRep == null ? selArr.getFlatTerm().getSort() == array.getSort() : weakRep == mQuantTheory.getClausifier().getArrayTheory().getWeakRep(selArr)) { - final CCTerm index = ArrayTheory.getIndexFromSelect((CCAppTerm) sel); + final CCParameter index = ArrayTheory.getIndexFromSelect(sel); interestingTerms.add(index.getFlatTerm()); } } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/QuantifierTheory.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/QuantifierTheory.java index 158f59263..2d28568ca 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/QuantifierTheory.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/QuantifierTheory.java @@ -47,6 +47,7 @@ import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.ITheory; import de.uni_freiburg.informatik.ultimate.smtinterpol.dpll.Literal; import de.uni_freiburg.informatik.ultimate.smtinterpol.proof.SourceAnnotation; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CClosure; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.linar.LinArSolve; @@ -927,16 +928,18 @@ private Clause addInstClausesToPending(final Collection instances) { } /** - * Get the term that is the current CC representative of the given term, if such term exists. + * Get the term that canonically denotes the current value of the given term in CC, if the term has a CC value. With + * offset equalities this is the flat term of the class representative plus the value's offset to it, so two terms + * yield the same representative term exactly if CC knows them to be equal (same class at the same offset). * * @param term * a term. - * @return the the term corresponding to the current CC representative of the given term, if it exists, the input + * @return the term corresponding to the current CC representative value of the given term, if it exists, the input * term else. */ Term getRepresentativeTerm(final Term term) { - final CCTerm ccTerm = getClausifier().getCCTerm(term); - return ccTerm == null ? term : ccTerm.getRepresentative().getFlatTerm(); + final CCParameter ccParam = getClausifier().getCCParameter(term); + return ccParam == null ? term : ccParam.getValueKey().getFlatTerm(); } private void addGroundCCTerms(final Term term, final SourceAnnotation source) { @@ -947,9 +950,11 @@ private void addGroundCCTerms(final Term term, final SourceAnnotation source) { final Term subTerm = todo.pop(); if (subTerm instanceof ApplicationTerm && seen.add(subTerm)) { if (subTerm.getFreeVars().length == 0) { - final CCTerm ccTerm = mClausifier.getCCTerm(subTerm); - if (ccTerm == null && (Clausifier.needCCTerm(subTerm) || subTerm.getSort().isArraySort())) { - mClausifier.createCCTerm(subTerm, source); + // a ground term with a constant summand is represented by the CCTerm of its offset-free part + final Term offsetFree = mClausifier.getOffsetFreeTerm(subTerm); + final CCTerm ccTerm = mClausifier.getCCTerm(offsetFree); + if (ccTerm == null && (Clausifier.needCCTerm(offsetFree) || offsetFree.getSort().isArraySort())) { + mClausifier.createCCTerm(offsetFree, source); } } else { for (final Term arg : ((ApplicationTerm) subTerm).getParameters()) { diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/CompareCode.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/CompareCode.java index d136614f9..3dc9c8945 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/CompareCode.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/CompareCode.java @@ -18,12 +18,12 @@ */ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.ematching; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; /** - * Code to compare two CCTerms. If the terms are equal, the remaining code can be executed. If they are not yet equal, + * Code to compare two values. If the values are equal, the remaining code can be executed. If they are not yet equal, * executing the Compare code will install a trigger in the CClosure. - * + * * @author Tanja Schindler */ public class CompareCode implements ICode { @@ -41,17 +41,18 @@ public CompareCode(final EMatching eMatching, final int firstRegIndex, final int } @Override - public void execute(final CCTerm[] register, final int decisionLevel) { - final CCTerm firstTerm = register[mFirstRegIndex]; - final CCTerm secondTerm = register[mSecondRegIndex]; - if (mEMatching.getQuantTheory().getCClosure().isEqSet(firstTerm, secondTerm)) { - final int eqDecisionLevel = - mEMatching.getQuantTheory().getCClosure().getDecideLevelForPath(firstTerm, secondTerm); + public void execute(final CCParameter[] register, final int decisionLevel) { + final CCParameter firstTerm = register[mFirstRegIndex]; + final CCParameter secondTerm = register[mSecondRegIndex]; + if (firstTerm.sameValueAs(secondTerm)) { + final int eqDecisionLevel = mEMatching.getQuantTheory().getCClosure() + .getDecideLevelForPath(firstTerm.getCCTerm(), secondTerm.getCCTerm()); mEMatching.addCode(mRemainingCode, register, eqDecisionLevel > decisionLevel ? eqDecisionLevel : decisionLevel); - } else { + } else if (firstTerm.getCCTerm() != secondTerm.getCCTerm()) { mEMatching.installCompareTrigger(firstTerm, secondTerm, mRemainingCode, register, decisionLevel); } + // else: the same base term at two different offsets; the values can never become equal. } @Override diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMCompareTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMCompareTrigger.java index e34393895..4f7238dbd 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMCompareTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMCompareTrigger.java @@ -18,26 +18,25 @@ */ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.ematching; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; /** - * A trigger for comparing two CCTerms. It has to be installed into the CClosure. Upon activation, the remaining + * A trigger for comparing two values. It has to be installed into the CClosure. Upon activation, the remaining * E-Matching code can be executed with the given register. - * + * * @author Tanja Schindler */ public class EMCompareTrigger extends de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CompareTrigger { private final EMatching mEMatching; private final ICode mRemainingCode; - private final CCTerm mLhs; - private final CCTerm mRhs; - private final CCTerm[] mRegister; + private final CCParameter mLhs; + private final CCParameter mRhs; + private final CCParameter[] mRegister; private final int mDecisionLevel; - public EMCompareTrigger(final EMatching eMatching, final CCTerm lhs, final CCTerm rhs, final ICode remainingCode, - final CCTerm[] register, - final int decisionLevel) { + public EMCompareTrigger(final EMatching eMatching, final CCParameter lhs, final CCParameter rhs, + final ICode remainingCode, final CCParameter[] register, final int decisionLevel) { mEMatching = eMatching; mLhs = lhs; mRhs = rhs; @@ -46,11 +45,13 @@ public EMCompareTrigger(final EMatching eMatching, final CCTerm lhs, final CCTer mDecisionLevel = decisionLevel; } - public CCTerm getLhs() { + @Override + public CCParameter getLhs() { return mLhs; } - public CCTerm getRhs() { + @Override + public CCParameter getRhs() { return mRhs; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMReverseTrigger.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMReverseTrigger.java index f5318347c..a02bdd502 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMReverseTrigger.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMReverseTrigger.java @@ -22,7 +22,7 @@ import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCAppTerm; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; /** * A trigger to find new function applications. It has to be installed into the CClosure. Upon activation, the remaining @@ -36,13 +36,13 @@ public class EMReverseTrigger extends de.uni_freiburg.informatik.ultimate.smtint private final ICode mRemainingCode; private final FunctionSymbol mFunc; private final int mArgPos; - private final CCTerm mArg; - private final CCTerm[] mRegister; + private final CCParameter mArg; + private final CCParameter[] mRegister; private final int mOutRegIndex; private final int mDecisionLevel; public EMReverseTrigger(final EMatching eMatching, final ICode remainingCode, final FunctionSymbol func, - final int argPos, final CCTerm arg, final CCTerm[] register, final int outRegIndex, + final int argPos, final CCParameter arg, final CCParameter[] register, final int outRegIndex, final int decisionLevel) { mEMatching = eMatching; mRemainingCode = remainingCode; @@ -55,7 +55,7 @@ public EMReverseTrigger(final EMatching eMatching, final ICode remainingCode, fi } @Override - public CCTerm getArgument() { + public CCParameter getArgument() { return mArg; } @@ -71,13 +71,14 @@ public FunctionSymbol getFunctionSymbol() { @Override public void activate(final CCAppTerm appTerm, final boolean isFresh) { - final CCTerm[] updatedRegister = Arrays.copyOf(mRegister, mRegister.length); + final CCParameter[] updatedRegister = Arrays.copyOf(mRegister, mRegister.length); updatedRegister[mOutRegIndex] = appTerm; if (mArg != null) { // Reverse - final CCTerm candArg = appTerm.getArgument(mArgPos); + // the values match by signature; the decide level comes from the path between the base terms + final CCParameter candArg = appTerm.getArgParam(mArgPos); final int termDecisionLevel = mEMatching.getQuantTheory().getCClosure() - .getDecideLevelForPath(mArg, candArg); + .getDecideLevelForPath(mArg.getCCTerm(), candArg.getCCTerm()); mEMatching.addCode(mRemainingCode, updatedRegister, termDecisionLevel > mDecisionLevel ? termDecisionLevel : mDecisionLevel); diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMatching.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMatching.java index 925ad5794..d21f8b0c5 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMatching.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/EMatching.java @@ -36,7 +36,7 @@ import de.uni_freiburg.informatik.ultimate.logic.Term; import de.uni_freiburg.informatik.ultimate.logic.TermVariable; import de.uni_freiburg.informatik.ultimate.smtinterpol.Config; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.epr.util.Pair; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.epr.util.Triple; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.QuantBoundConstraint; @@ -58,7 +58,7 @@ public class EMatching { private final QuantifierTheory mQuantTheory; - private Deque> mTodoStack; + private Deque> mTodoStack; private final Map mUndoInformation; /** @@ -66,7 +66,7 @@ public class EMatching { * corresponding SubstitutionInfo */ private final Map> mAtomSubsDawgs; - private final Map>> mClauseCodes; + private final Map>> mClauseCodes; private final Set mEmatchingAtoms, mPartialEmatchingAtoms; final SubstitutionInfo mEmptySubs; @@ -76,7 +76,7 @@ public EMatching(final QuantifierTheory quantifierTheory) { mAtomSubsDawgs = new HashMap<>(); mClauseCodes = new HashMap<>(); mUndoInformation = new LinkedHashMap<>(); - mEmptySubs = new SubstitutionInfo(new ArrayList(), new LinkedHashMap<>()); + mEmptySubs = new SubstitutionInfo(new ArrayList(), new LinkedHashMap<>()); mEmatchingAtoms = new HashSet<>(); mPartialEmatchingAtoms = new HashSet<>(); } @@ -90,7 +90,7 @@ public EMatching(final QuantifierTheory quantifierTheory) { */ public void addClause(final QuantClause qClause) { assert !mClauseCodes.containsKey(qClause); - final ArrayList> clauseCodes = new ArrayList<>(); + final ArrayList> clauseCodes = new ArrayList<>(); for (final QuantLiteral qLit : qClause.getQuantLits()) { final QuantLiteral qAtom = qLit.getAtom(); if (!qLit.isArithmetical() && QuantUtil.containsArithmeticOnQuantOnlyAtTopLevel(qAtom)) { @@ -125,7 +125,7 @@ public void addClause(final QuantClause qClause) { } if (!patterns.isEmpty()) { - final Pair newCode = + final Pair newCode = new PatternCompiler(mQuantTheory, qAtom, patterns.toArray(new Term[patterns.size()])) .compile(); addCode(newCode.getFirst(), newCode.getSecond(), 0); @@ -169,7 +169,7 @@ public void reAddClauses(final Iterable clauses) { assert mTodoStack.isEmpty() && mUndoInformation.isEmpty(); for (final QuantClause qClause : clauses) { assert mClauseCodes.containsKey(qClause); - for (final Triple code : mClauseCodes.get(qClause)) { + for (final Triple code : mClauseCodes.get(qClause)) { mTodoStack.add(code); } } @@ -197,7 +197,7 @@ public void run() { time = System.nanoTime(); } while (!mTodoStack.isEmpty() && !mQuantTheory.getEngine().isTerminationRequested()) { - final Triple code = mTodoStack.pop(); + final Triple code = mTodoStack.pop(); mQuantTheory.getLogger().debug("EM-Code: %s", code); assert code.getThird() <= mQuantTheory.getEngine().getDecideLevel(); code.getFirst().execute(code.getSecond(), code.getThird()); @@ -232,8 +232,8 @@ public void undo(final int decisionLevel) { } } mQuantTheory.getLogger().debug("Remaining levels: %s", mUndoInformation.keySet()); - final Deque> undoneTodoStack = new ArrayDeque<>(); - for (final Triple todo : mTodoStack) { + final Deque> undoneTodoStack = new ArrayDeque<>(); + for (final Triple todo : mTodoStack) { if (todo.getThird() <= decisionLevel) { undoneTodoStack.add(todo); } @@ -270,31 +270,31 @@ public QuantifierTheory getQuantTheory() { * @param code * the remaining code. * @param register - * the candidate CCTerms for this execution. + * the candidate values for this execution. * @param decisionLevel * the decision level that is relevant for this execution. */ - void addCode(final ICode code, final CCTerm[] register, final int decisionLevel) { - final Triple todo = + void addCode(final ICode code, final CCParameter[] register, final int decisionLevel) { + final Triple todo = new Triple<>(code, register, decisionLevel); assert decisionLevel <= mQuantTheory.getEngine().getDecideLevel(); mTodoStack.add(todo); } /** - * Add a new interesting substitution for a quantified literal, together with the corresponding CCTerms. + * Add a new interesting substitution for a quantified literal, together with the corresponding values. * * @param qLit * the quantified Literal * @param varSubs * the variable substitution ordered as the variables in the clause. * @param equivalentCCTerms - * the corresponding CCTerms for the EUTerms in the literal. + * the corresponding values for the EUTerms in the literal. * @param decisionLevel * the decision level relevant for this substitution. */ - void addInterestingSubstitution(final QuantLiteral qLit, final List varSubs, - final Map equivalentCCTerms, final int decisionLevel) { + void addInterestingSubstitution(final QuantLiteral qLit, final List varSubs, + final Map equivalentCCTerms, final int decisionLevel) { final long time = System.nanoTime(); assert mAtomSubsDawgs.containsKey(qLit); Dawg subsDawg = mAtomSubsDawgs.get(qLit); @@ -312,12 +312,12 @@ void addInterestingSubstitution(final QuantLiteral qLit, final List varS } /** - * Install a trigger into the CClosure that compares two CCTerms. + * Install a trigger into the CClosure that compares two values. * * @param lhs - * the first CCTerm. + * the first value. * @param rhs - * the other CCTerm it should be compared with. + * the other value it should be compared with. * @param remainingCode * the remaining E-Matching code. * @param register @@ -325,8 +325,8 @@ void addInterestingSubstitution(final QuantLiteral qLit, final List varS * @param decisionLevel * the decision level relevant for the compare trigger. */ - void installCompareTrigger(final CCTerm lhs, final CCTerm rhs, final ICode remainingCode, - final CCTerm[] register, final int decisionLevel) { + void installCompareTrigger(final CCParameter lhs, final CCParameter rhs, final ICode remainingCode, + final CCParameter[] register, final int decisionLevel) { assert decisionLevel <= mQuantTheory.getClausifier().getEngine().getDecideLevel(); final EMCompareTrigger trigger = new EMCompareTrigger(this, lhs, rhs, remainingCode, register, decisionLevel); mQuantTheory.getCClosure().insertCompareTrigger(lhs, rhs, trigger); @@ -348,7 +348,7 @@ void installCompareTrigger(final CCTerm lhs, final CCTerm rhs, final ICode remai * the decision level relevant for the find trigger. */ void installFindTrigger(final FunctionSymbol func, final int regIndex, final ICode remainingCode, - final CCTerm[] register, final int decisionLevel) { + final CCParameter[] register, final int decisionLevel) { mQuantTheory.getLogger().debug("Install Find Trigger: FIND %s (decide@%d)", func, decisionLevel); final EMReverseTrigger trigger = new EMReverseTrigger(this, remainingCode, func, -1, null, register, regIndex, decisionLevel); @@ -374,8 +374,8 @@ void installFindTrigger(final FunctionSymbol func, final int regIndex, final ICo * @param decisionLevel * the decision level relevant for the reverse trigger. */ - void installReverseTrigger(final FunctionSymbol func, final CCTerm arg, final int argPos, - final int regIndex, final ICode remainingCode, final CCTerm[] register, final int decisionLevel) { + void installReverseTrigger(final FunctionSymbol func, final CCParameter arg, final int argPos, + final int regIndex, final ICode remainingCode, final CCParameter[] register, final int decisionLevel) { mQuantTheory.getLogger().debug("Install Reverse Trigger: REV %s,%d on %s (decide@%d)", func, argPos, arg, decisionLevel); assert decisionLevel <= mQuantTheory.getClausifier().getEngine().getDecideLevel(); @@ -467,26 +467,26 @@ public boolean isPartiallyUsingEmatching(final QuantLiteral qLit) { /** * This class stores information about a substitution found by the E-Matching. That is, the variable substitutions, - * as well as for each pattern the CCTerm that is equivalent to the ground term that would result from applying the + * as well as for each pattern the value that is equivalent to the ground term that would result from applying the * substitution to the pattern. * * @author Tanja Schindler * */ public class SubstitutionInfo { - final List mVarSubs; - final Map mEquivalentCCTerms; + final List mVarSubs; + final Map mEquivalentCCTerms; - SubstitutionInfo(final List varSubs, final Map equivalentCCTerms) { + SubstitutionInfo(final List varSubs, final Map equivalentCCTerms) { mVarSubs = varSubs; mEquivalentCCTerms = equivalentCCTerms; } - public List getVarSubs() { + public List getVarSubs() { return mVarSubs; } - public Map getEquivalentCCTerms() { + public Map getEquivalentCCTerms() { return mEquivalentCCTerms; } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/FindCode.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/FindCode.java index bdf97a8bc..80cc65d3f 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/FindCode.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/FindCode.java @@ -23,7 +23,7 @@ import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCAppTerm; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CClosure; /** @@ -51,13 +51,13 @@ public FindCode(final EMatching eMatching, final CClosure cclosure, final Functi } @Override - public void execute(final CCTerm[] register, final int decisionLevel) { + public void execute(final CCParameter[] register, final int decisionLevel) { if (mFunc.getParameterSorts().length > 0) { mEMatching.installFindTrigger(mFunc, mOutRegIndex, mRemainingCode, register, decisionLevel); } final List funcApps = mCClosure.getAllFuncApps(mFunc); for (final CCAppTerm cand : funcApps) { - final CCTerm[] updatedReg = Arrays.copyOf(register, register.length); + final CCParameter[] updatedReg = Arrays.copyOf(register, register.length); updatedReg[mOutRegIndex] = cand; mEMatching.addCode(mRemainingCode, updatedReg, decisionLevel > 0 ? decisionLevel : 0); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/GetArgCode.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/GetArgCode.java index 5ca37f6d7..b33870af6 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/GetArgCode.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/GetArgCode.java @@ -22,7 +22,7 @@ import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCAppTerm; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; /** * Code to get the specified argument of the candidate for a specified term. @@ -48,12 +48,12 @@ public GetArgCode(final EMatching eMatching, final int appTermRegIndex, final Fu } @Override - public void execute(final CCTerm[] register, final int decisionLevel) { - final CCTerm appTerm = register[mAppTermRegIndex]; - assert appTerm instanceof CCAppTerm; - CCAppTerm partialApp = (CCAppTerm) appTerm; - CCTerm arg = partialApp.getArgument(mArgPos); - final CCTerm[] updatedRegister = Arrays.copyOf(register, register.length); + public void execute(final CCParameter[] register, final int decisionLevel) { + // a candidate for a function application is always a bare CCAppTerm (offset 0) + assert register[mAppTermRegIndex] instanceof CCAppTerm; + final CCAppTerm partialApp = (CCAppTerm) register[mAppTermRegIndex]; + final CCParameter arg = partialApp.getArgParam(mArgPos); + final CCParameter[] updatedRegister = Arrays.copyOf(register, register.length); updatedRegister[mOutRegIndex] = arg; mEMatching.addCode(mRemainingCode, updatedRegister, decisionLevel); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ICode.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ICode.java index 0ce7f2f5d..2c8a79ade 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ICode.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ICode.java @@ -18,23 +18,23 @@ */ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.ematching; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; /** * This represents a piece of code for E-matching which can be executed for a register containing candidate terms and a * decision level depending on those candidate terms. - * + * * @author Tanja Schindler */ public interface ICode { /** * Execute this piece of code with the given register. - * + * * @param register - * the relevant CCTerms for this execution. + * the relevant candidate values for this execution. * @param decisionLevel * the relevant decisionLevel for this execution. */ - public void execute(final CCTerm[] register, final int decisionLevel); + public void execute(final CCParameter[] register, final int decisionLevel); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/PatternCompiler.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/PatternCompiler.java index 12ccf0e2c..9b1c372ce 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/PatternCompiler.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/PatternCompiler.java @@ -29,7 +29,7 @@ import de.uni_freiburg.informatik.ultimate.logic.Term; import de.uni_freiburg.informatik.ultimate.logic.TermVariable; import de.uni_freiburg.informatik.ultimate.smtinterpol.convert.Clausifier; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.epr.util.Pair; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.QuantLiteral; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.QuantifierTheory; @@ -64,10 +64,10 @@ public PatternCompiler(final QuantifierTheory quantTheory, final QuantLiteral qA * * @return the resulting code and the corresponding register. */ - public Pair compile() { + public Pair compile() { collectTermInfos(mPatterns); final ICode code = generateCode(); - final CCTerm[] register = new CCTerm[getNextFreeRegIndex()]; + final CCParameter[] register = new CCParameter[getNextFreeRegIndex()]; for (final TermInfo termInfo : mTermInfos.values()) { if (termInfo.mGroundTerm != null) { register[termInfo.mRegIndex] = termInfo.mGroundTerm; @@ -271,7 +271,7 @@ private boolean hasProcessedOrGroundOrNonVarSubterms(final Term term) { */ class TermInfo { - CCTerm mGroundTerm; + CCParameter mGroundTerm; /** * The number of occurrences of the subterm. */ diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ReverseCode.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ReverseCode.java index 6bf42d6c2..83ed4b6fe 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ReverseCode.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/ReverseCode.java @@ -19,7 +19,7 @@ package de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.ematching; import de.uni_freiburg.informatik.ultimate.logic.FunctionSymbol; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; /** * Code to find a function application for a given function symbol that has the given term as argument at the given @@ -49,8 +49,8 @@ public ReverseCode(final EMatching eMatching, final int argRegIndex, final Funct } @Override - public void execute(final CCTerm[] register, final int decisionLevel) { - final CCTerm arg = register[mArgRegIndex]; + public void execute(final CCParameter[] register, final int decisionLevel) { + final CCParameter arg = register[mArgRegIndex]; mEMatching.installReverseTrigger(mFunc, arg, mArgPos, mOutRegIndex, mRemainingCode, register, decisionLevel); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/YieldCode.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/YieldCode.java index 8f6057557..6981a0f45 100644 --- a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/YieldCode.java +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/quant/ematching/YieldCode.java @@ -26,7 +26,7 @@ import de.uni_freiburg.informatik.ultimate.logic.Term; import de.uni_freiburg.informatik.ultimate.logic.TermVariable; -import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCTerm; +import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.cclosure.CCParameter; import de.uni_freiburg.informatik.ultimate.smtinterpol.theory.quant.QuantLiteral; /** @@ -66,8 +66,8 @@ public YieldCode(final EMatching eMatching, final QuantLiteral qLit, final TermV } @Override - public void execute(CCTerm[] register, final int decisionLevel) { - final List varSubs = new ArrayList(mVarOrder.length); + public void execute(final CCParameter[] register, final int decisionLevel) { + final List varSubs = new ArrayList<>(mVarOrder.length); for (int i = 0; i < mVarOrder.length; i++) { if (mVarPos.containsKey(mVarOrder[i])) { varSubs.add(register[mVarPos.get(mVarOrder[i])]); @@ -75,7 +75,7 @@ public void execute(CCTerm[] register, final int decisionLevel) { varSubs.add(null); } } - final Map equivalentCCTerms = new HashMap<>(); + final Map equivalentCCTerms = new HashMap<>(); for (final Entry pos : mEquivCCTermPos.entrySet()) { equivalentCCTerms.put(pos.getKey(), register[pos.getValue()]); } diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/util/OffsetEqKey.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/util/OffsetEqKey.java new file mode 100644 index 000000000..3993ec6c0 --- /dev/null +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/util/OffsetEqKey.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2009-2026 University of Freiburg + * + * This file is part of SMTInterpol. + * + * SMTInterpol is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SMTInterpol is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SMTInterpol. If not, see . + */ +package de.uni_freiburg.informatik.ultimate.smtinterpol.util; + +import de.uni_freiburg.informatik.ultimate.logic.Rational; +import de.uni_freiburg.informatik.ultimate.logic.Term; + +/** + * A lookup key identifying an (offset) equality by the affine fact it expresses: + * the offset-free parts of its two sides (see {@link OffsetTerm}) together with + * the constant offset {@code constant(lhs) - constant(rhs)} between them. Two + * equalities with the same key denote the same fact {@code lhs = rhs}, which is + * what lets a needed equality like {@code (= (+ x 5) (+ y 7))} be matched against + * the clause literal {@code (= x (+ y 2))}. The two offset-free parts are kept + * separate (rather than subtracted into one difference polynomial) so + * that unrelated edges whose difference polynomials coincide up to sign — e.g. + * {@code x+y = z+w+2} and {@code z-y = x-w-2} — do not collide. + */ +public final class OffsetEqKey { + private final Term mLhs; + private final Term mRhs; + private final Rational mLhsOffset; + private final Rational mOffset; + private final int mHash; + + public OffsetEqKey(final Term lhs, final Term rhs) { + if (lhs.getSort().isNumericSort()) { + final OffsetTerm lhsSplit = new OffsetTerm(lhs); + final OffsetTerm rhsSplit = new OffsetTerm(rhs); + mLhs = lhsSplit.getTerm(); + mRhs = rhsSplit.getTerm(); + mLhsOffset = lhsSplit.getOffset(); + mOffset = lhsSplit.getOffset().sub(rhsSplit.getOffset()); + } else { + mLhs = lhs; + mRhs = rhs; + mLhsOffset = Rational.ZERO; + mOffset = Rational.ZERO; + } + final int lhsHash = mLhs.hashCode(); + final int rhsHash = mRhs.hashCode(); + if (lhsHash == rhsHash) { + mHash = lhsHash * 31 + mOffset.abs().hashCode(); + } else if (lhsHash < rhsHash) { + mHash = lhsHash * 37 + rhsHash * 31 + mOffset.hashCode(); + } else { + mHash = rhsHash * 37 + lhsHash * 31 + mOffset.negate().hashCode(); + } + } + + public Term getLhs() { + return mLhs; + } + + public Term getRhs() { + return mRhs; + } + + public Rational getOffset() { + return mOffset; + } + + /** + * The absolute offset of the lhs term this key was built from, i.e. {@code lhs = getLhs() + getLhsOffset()}. + */ + public Rational getLhsOffset() { + return mLhsOffset; + } + + /** + * The absolute offset of the rhs term this key was built from, i.e. {@code rhs = getRhs() + getRhsOffset()}. + */ + public Rational getRhsOffset() { + return mLhsOffset.sub(mOffset); + } + + /** + * Check whether this key matches the given equal key with swapped sides, i.e. whether the lhs this key was built + * from corresponds to the rhs of {@code other}. + * + * @param other a key that is {@code equals} to this key. + */ + public boolean isSwapped(final OffsetEqKey other) { + assert equals(other); + return !(mLhs == other.mLhs && mOffset.equals(other.mOffset)); + } + + /** + * Compute the constant shift between the equality this key was built from and the equality the given equal key + * was built from. If {@code other} was built from {@code l = r}, then this key was built from an equality + * {@code l + shift = r + shift} (modulo swapping of sides). + * + * @param other a key that is {@code equals} to this key. + * @return the shift such that the sides of this key's equality are the sides of {@code other}'s equality plus + * the shift. + */ + public Rational getShift(final OffsetEqKey other) { + assert equals(other); + return mLhsOffset.sub(isSwapped(other) ? other.getRhsOffset() : other.getLhsOffset()); + } + + @Override + public int hashCode() { + return mHash; + } + + @Override + public boolean equals(final Object other) { + if (this == other) { + return true; + } + if (!(other instanceof OffsetEqKey)) { + return false; + } + final OffsetEqKey o = (OffsetEqKey) other; + return (mOffset.equals(o.mOffset) && mLhs == o.mLhs && mRhs == o.mRhs) + || (mOffset.equals(o.mOffset.negate()) && mLhs == o.mRhs && mRhs == o.mLhs); + } +} diff --git a/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/util/OffsetTerm.java b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/util/OffsetTerm.java new file mode 100644 index 000000000..37a78bcdf --- /dev/null +++ b/SMTInterpol/src/de/uni_freiburg/informatik/ultimate/smtinterpol/util/OffsetTerm.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2009-2026 University of Freiburg + * + * This file is part of SMTInterpol. + * + * SMTInterpol is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * SMTInterpol is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with SMTInterpol. If not, see . + */ +package de.uni_freiburg.informatik.ultimate.smtinterpol.util; + +import java.util.Arrays; + +import de.uni_freiburg.informatik.ultimate.logic.ApplicationTerm; +import de.uni_freiburg.informatik.ultimate.logic.ConstantTerm; +import de.uni_freiburg.informatik.ultimate.logic.Rational; +import de.uni_freiburg.informatik.ultimate.logic.SMTLIBConstants; +import de.uni_freiburg.informatik.ultimate.logic.Term; + +/** + * A numeric term split syntactically into its offset-free part and a constant + * offset: a trailing constant summand of a {@code +} application is the offset, + * and dropping it yields the offset-free part; a term that is entirely constant + * splits into the base {@code 0} and its value (matching the {@code 0} CCTerm + * that a plain-numeral parameter is based on). This is the exact inverse of + * {@code CCParameter.addConstant} and {@code TermCompiler.unifyPolynomial}, which + * both place the constant last behind the canonic offset-free term, so equal + * values always split into byte-identical offset-free parts. A constant summand + * in any other position (possible only when offset equalities are disabled and + * constants never move between equality sides) is deliberately left inside the + * offset-free part. + */ +public class OffsetTerm { + private final Term mTerm; + private final Rational mOffset; + + public OffsetTerm(final Term term) { + Term base = term; + Rational offset = Rational.ZERO; + if (term.getSort().isNumericSort()) { + if (base instanceof ApplicationTerm + && ((ApplicationTerm) base).getFunction().getName().equals(SMTLIBConstants.PLUS)) { + final Term[] params = ((ApplicationTerm) base).getParameters(); + final Rational last = Polynomial.parseConstant(params[params.length - 1]); + if (last != null) { + offset = last; + base = params.length == 2 ? params[0] + : term.getTheory().term(SMTLIBConstants.PLUS, Arrays.copyOf(params, params.length - 1)); + } + } else if (base instanceof ConstantTerm) { + // a plain constant is all offset; its offset-free part is the 0 base term + final Rational baseConstant = Polynomial.parseConstant(base); + if (baseConstant != null) { + offset = offset.add(baseConstant); + base = Rational.ZERO.toTerm(term.getSort()); + } + } + } + mTerm = base; + mOffset = offset; + } + + public Term getTerm() { + return mTerm; + } + + public Rational getOffset() { + return mOffset; + } +} diff --git a/SMTInterpolTest/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruentAddTest.java b/SMTInterpolTest/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruentAddTest.java index cda6a256f..f114e6437 100644 --- a/SMTInterpolTest/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruentAddTest.java +++ b/SMTInterpolTest/src/de/uni_freiburg/informatik/ultimate/smtinterpol/theory/cclosure/CongruentAddTest.java @@ -89,7 +89,7 @@ private void createterms() { for (int i = 0; i < 6; ++i) {// NOCHECKSTYLE final String xi = "x" + i; mTheory.declareFunction(xi, Script.EMPTY_SORT_ARRAY, sort); - mTerms[i] = mClausifier.createCCTerm(mTheory.term(xi), null); + mTerms[i] = (CCTerm) mClausifier.createCCTerm(mTheory.term(xi), null); } mTheory.declareFunction("d", Script.EMPTY_SORT_ARRAY, sort); mTheory.declareFunction("c", Script.EMPTY_SORT_ARRAY, sort); @@ -99,10 +99,10 @@ private void createterms() { final Term termc = mTheory.term("c"); final Term termb = mTheory.term("b"); final Term terma = mTheory.term("a"); - mD = mClausifier.createCCTerm(termd, null); - mC = mClausifier.createCCTerm(termc, null); - mB = mClausifier.createCCTerm(termb, null); - mA = mClausifier.createCCTerm(terma, null); + mD = (CCTerm) mClausifier.createCCTerm(termd, null); + mC = (CCTerm) mClausifier.createCCTerm(termc, null); + mB = (CCTerm) mClausifier.createCCTerm(termb, null); + mA = (CCTerm) mClausifier.createCCTerm(terma, null); final EqualityProxy eqab = mClausifier.createEqualityProxy(terma, termb, mSource); Assert.assertNotSame(EqualityProxy.getTrueProxy(), eqab); Assert.assertNotSame(EqualityProxy.getFalseProxy(), eqab); diff --git a/SMTInterpolTest/test/datatype/offset_lemmas.smt2 b/SMTInterpolTest/test/datatype/offset_lemmas.smt2 new file mode 100644 index 000000000..976afb5ee --- /dev/null +++ b/SMTInterpolTest/test/datatype/offset_lemmas.smt2 @@ -0,0 +1,15 @@ +; Datatype lemmas over a numeric constructor field must respect offset equalities. +; dt-project: from u = mk(y+5) the selector f(u) must equal y+5 (not y). +; dt-injective: from mk(y+5) = mk(z) the argument equality y+5 = z must be propagated. +(set-info :smt-lib-version 2.6) +(set-logic QF_UFDTLIA) +(set-info :status unsat) +(declare-datatype P ((mk (f Int)))) +(declare-fun u () P) +(declare-fun y () Int) +(declare-fun z () Int) +(assert (= u (mk (+ y 5)))) +(assert (= (mk (+ y 5)) (mk z))) +; f(u) = y+5 (dt-project) and z = y+5 (dt-injective); both contradict the disjunction below +(assert (or (not (= (f u) (+ y 5))) (not (= z (+ y 5))))) +(check-sat) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset001.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset001.smt2 new file mode 100644 index 000000000..b25aeb339 --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset001.smt2 @@ -0,0 +1,16 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const i Int) +(declare-const j Int) +(declare-const v Int) +;; read-over-weakeq with shifted indices: store at i+1, read at j+2, i+1 != j+2 +(assert (! (= b (store a (+ i 1) v)) :named A)) +(assert (! (and (not (= (select b (+ j 2)) (select a (+ j 2)))) (not (= i (+ j 1)))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset002.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset002.smt2 new file mode 100644 index 000000000..295dd991c --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset002.smt2 @@ -0,0 +1,15 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const i Int) +(declare-const j Int) +(declare-const v Int) +(assert (! (and (= b (store a (+ i 1) v)) (not (= i (+ j 1)))) :named A)) +(assert (! (not (= (select b (+ j 2)) (select a (+ j 2)))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset003.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset003.smt2 new file mode 100644 index 000000000..6d5628bdc --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset003.smt2 @@ -0,0 +1,15 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const x Int) +(declare-const y Int) +(declare-const k Int) +;; read-const-weakeq: const array with offset value, read shifted through y = x+2 +(assert (! (= a ((as const (Array Int Int)) (+ x 1))) :named A)) +(assert (! (and (= y (+ x 2)) (not (= (select a k) (+ y (- 1))))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset004.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset004.smt2 new file mode 100644 index 000000000..82cb631f8 --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset004.smt2 @@ -0,0 +1,18 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const c (Array Int Int)) +(declare-const i Int) +(declare-const j Int) +(declare-const v Int) +(declare-const w Int) +;; weakeq-ext: both stores hit the same cell (i+1 = j+2) with the same value +(assert (! (and (= a (store c (+ i 1) v)) (= i (+ j 1))) :named A)) +(assert (! (and (= b (store c (+ j 2) w)) (= v w) (not (= a b))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset005.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset005.smt2 new file mode 100644 index 000000000..b8ef643b2 --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset005.smt2 @@ -0,0 +1,16 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const i Int) +(declare-const k Int) +(declare-const v Int) +;; read-over-store through equality k = i+1, stored value carries an offset v+1 +(assert (! (= b (store a (+ i 1) (+ v 1))) :named A)) +(assert (! (and (= k (+ i 1)) (not (= (select b k) (+ v 1)))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset006.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset006.smt2 new file mode 100644 index 000000000..b7aff1cb5 --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset006.smt2 @@ -0,0 +1,17 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const i Int) +(declare-const j Int) +(declare-const k Int) +(declare-const v Int) +;; index equality derived via LA: i = k+1 (A), j = k+2 (B), so j = i+1 = store index +(assert (! (and (= b (store a (+ i 1) v)) (<= i (+ k 1)) (>= i (+ k 1))) :named A)) +(assert (! (and (<= j (+ k 2)) (>= j (+ k 2)) (not (= (select b j) v))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset007.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset007.smt2 new file mode 100644 index 000000000..81ef4ef0c --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset007.smt2 @@ -0,0 +1,17 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const i Int) +(declare-const j Int) +(declare-const k Int) +(declare-const v Int) +;; read-over-weakeq between two selects with shifted indices i+1 = j+2 +(assert (! (and (= b (store a (+ k 1) v)) (not (= k i))) :named A)) +(assert (! (and (= i (+ j 1)) (not (= (select b (+ i 1)) (select a (+ j 2))))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset008.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset008.smt2 new file mode 100644 index 000000000..74bf9dc9c --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset008.smt2 @@ -0,0 +1,16 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const i Int) +(declare-const j Int) +(declare-const v Int) +;; weakeq-ext: store position patched by a select equality at shifted index j+2 = i+1 +(assert (! (= a (store b (+ i 1) v)) :named A)) +(assert (! (and (= i (+ j 1)) (= (select a (+ j 2)) (select b (+ j 2))) (not (= a b))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/arrayoffset009.smt2 b/SMTInterpolTest/test/interpolation/arrayoffset009.smt2 new file mode 100644 index 000000000..4a8f01276 --- /dev/null +++ b/SMTInterpolTest/test/interpolation/arrayoffset009.smt2 @@ -0,0 +1,16 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) +(set-logic QF_ALIA) +(set-info :status unsat) +(declare-const a (Array Int Int)) +(declare-const b (Array Int Int)) +(declare-const x Int) +(declare-const y Int) +(declare-const k Int) +;; const-weakeq: store writes the const value through the shifted equality y = x-1 +(assert (! (= a ((as const (Array Int Int)) (+ x 1))) :named A)) +(assert (! (and (= b (store a k (+ y 2))) (= y (- x 1)) (not (= b ((as const (Array Int Int)) (+ x 1))))) :named B)) +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/datatype/dt_injective_offset001.smt2 b/SMTInterpolTest/test/interpolation/datatype/dt_injective_offset001.smt2 new file mode 100644 index 000000000..399f0bb78 --- /dev/null +++ b/SMTInterpolTest/test/interpolation/datatype/dt_injective_offset001.smt2 @@ -0,0 +1,30 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) + +(set-info :smt-lib-version 2.6) +(set-logic QF_UFDTLIA) + +(set-info :category "crafted") +(set-info :status unsat) + + +(declare-datatypes ( (D 0) ) ( + ( (mk (val Int)) ) +)) + +(declare-const a Int) +(declare-const b Int) +(declare-const s D) +(declare-fun p (Int) Bool) + +;; injectivity on shifted constructor arguments: mk(a+5) = s = mk(b+7) +;; gives the offset equality a+5 = b+7 (canonically b = a-2), which then +;; justifies the congruence p(a+3) = p(b+5) with shift -2. + +(assert (! (and (= (mk (+ a 5)) s) (p (+ a 3))) :named A )) +(assert (! (and (= s (mk (+ b 7))) (not (p (+ b 5)))) :named B )) + +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/interpolation/datatype/dt_project_offset001.smt2 b/SMTInterpolTest/test/interpolation/datatype/dt_project_offset001.smt2 new file mode 100644 index 000000000..d3cf69c19 --- /dev/null +++ b/SMTInterpolTest/test/interpolation/datatype/dt_project_offset001.smt2 @@ -0,0 +1,30 @@ +(set-option :produce-interpolants true) +(set-option :interpolant-check-mode true) + +(set-info :smt-lib-version 2.6) +(set-logic QF_UFDTLIA) + +(set-info :category "crafted") +(set-info :status unsat) + + +(declare-datatypes ( (D 0) ) ( + ( (mk (val Int)) ) +)) + +(declare-const a Int) +(declare-const s D) +(declare-const bs D) +(declare-fun p (Int) Bool) + +;; projection on a shifted constructor argument: bs = s = mk(a+5) gives via +;; dt-project the offset equality val(bs) = a+5, which then justifies the +;; congruence p(a+10) = p(val(bs)+5) with shift 5. + +(assert (! (and (= (mk (+ a 5)) s) (p (+ a 10))) :named A )) +(assert (! (and (= s bs) (not (p (+ (val bs) 5)))) :named B )) + +(check-sat) +(get-interpolants A B) +(get-interpolants B A) +(exit) diff --git a/SMTInterpolTest/test/model/offset_datatype.smt2 b/SMTInterpolTest/test/model/offset_datatype.smt2 new file mode 100644 index 000000000..0213d5096 --- /dev/null +++ b/SMTInterpolTest/test/model/offset_datatype.smt2 @@ -0,0 +1,14 @@ +; Model construction with an offset equality inside a datatype constructor: +; the numeric field of (mk (+ y 5) 0) must keep its +5 offset in the model, so +; (fst p) evaluates to y + 5 = 8 rather than y = 3. +(set-option :produce-models true) +(set-option :model-check-mode true) +(set-info :status sat) +(set-logic QF_UFDTLIA) +(declare-datatype Pair ((mk (fst Int) (snd Int)))) +(declare-fun p () Pair) +(declare-fun y () Int) +(assert (= p (mk (+ y 5) 0))) +(assert (= y 3)) +(check-sat) +(get-model) diff --git a/SMTInterpolTest/test/model/offset_fresh001.smt2 b/SMTInterpolTest/test/model/offset_fresh001.smt2 new file mode 100644 index 000000000..c12a72a96 --- /dev/null +++ b/SMTInterpolTest/test/model/offset_fresh001.smt2 @@ -0,0 +1,18 @@ +; Fresh-value collision with an offset use-site: x is CC-local (never enters +; LA), so the model builder assigns it a "fresh" value larger than all used +; values. But "used" only tracks values registered for CCTerm representatives; +; the use-site value of the argument a+1 (= value(a) + offset 1) is never +; registered. With a = 2 the fresh value for x is 3 = a+1, so f is evaluated +; at the same point twice with different results. +(set-option :produce-models true) +(set-option :model-check-mode true) +(set-info :status sat) +(set-logic QF_UFLIA) +(declare-fun a () Int) +(declare-fun x () Int) +(declare-fun f (Int) Int) +(assert (>= a 2)) +(assert (not (= (f (+ a 1)) (f x)))) +(check-sat) +(get-model) +(get-value (a x (f (+ a 1)) (f x))) diff --git a/SMTInterpolTest/test/model/offset_fresh002.smt2 b/SMTInterpolTest/test/model/offset_fresh002.smt2 new file mode 100644 index 000000000..b22e4b99a --- /dev/null +++ b/SMTInterpolTest/test/model/offset_fresh002.smt2 @@ -0,0 +1,17 @@ +; Fresh-value collision, predicate variant: no numeric function values enter +; LA, so the only registered numeric value is a = 2. The CC-local x gets the +; fresh value 3, which collides with the unregistered use-site value a+1 = 3, +; making p(3) both true and false. +(set-option :produce-models true) +(set-option :model-check-mode true) +(set-info :status sat) +(set-logic QF_UFLIA) +(declare-fun a () Int) +(declare-fun x () Int) +(declare-fun p (Int) Bool) +(assert (>= a 2)) +(assert (p (+ a 1))) +(assert (not (p x))) +(check-sat) +(get-model) +(get-value (a x (p (+ a 1)) (p x))) diff --git a/SMTInterpolTest/test/model/offset_fresh003.smt2 b/SMTInterpolTest/test/model/offset_fresh003.smt2 new file mode 100644 index 000000000..5c2897860 --- /dev/null +++ b/SMTInterpolTest/test/model/offset_fresh003.smt2 @@ -0,0 +1,16 @@ +; Fresh-value collision with the offset on the CC-local side: the fresh value +; chosen for x must keep the use-site value x-1 distinct from all other +; f-argument values, i.e. the fresh value must account for the parameter's +; negative offset. Here LA mutates a to 1 and the fresh x = 2 gives x-1 = 1 = a. +(set-option :produce-models true) +(set-option :model-check-mode true) +(set-info :status sat) +(set-logic QF_UFLIA) +(declare-fun a () Int) +(declare-fun x () Int) +(declare-fun f (Int) Int) +(assert (>= a 0)) +(assert (not (= (f a) (f (- x 1))))) +(check-sat) +(get-model) +(get-value (a x (f a) (f (- x 1)))) diff --git a/SMTInterpolTest/test/model/offset_lia.smt2 b/SMTInterpolTest/test/model/offset_lia.smt2 new file mode 100644 index 000000000..716b519d2 --- /dev/null +++ b/SMTInterpolTest/test/model/offset_lia.smt2 @@ -0,0 +1,12 @@ +; Model construction with a congruence offset equality: f(y+5) shares the +; offset-free CCTerm y plus a +5 offset, and y is fixed to 3 in arithmetic. +(set-option :produce-models true) +(set-option :model-check-mode true) +(set-info :status sat) +(set-logic QF_UFLIA) +(declare-fun y () Int) +(declare-fun f (Int) Int) +(assert (= (f (+ y 5)) 0)) +(assert (= y 3)) +(check-sat) +(get-model) diff --git a/SMTInterpolTest/test/nia/divaxiom2.smt2 b/SMTInterpolTest/test/nia/divaxiom2.smt2 index 875265c65..9348a2b98 100644 --- a/SMTInterpolTest/test/nia/divaxiom2.smt2 +++ b/SMTInterpolTest/test/nia/divaxiom2.smt2 @@ -1,3 +1,4 @@ +(set-option :produce-proofs true) (set-logic QF_UFNIA) (declare-const x Int) diff --git a/SMTInterpolTest/test/quantified/offsetmatch001.smt2 b/SMTInterpolTest/test/quantified/offsetmatch001.smt2 new file mode 100644 index 000000000..027649a23 --- /dev/null +++ b/SMTInterpolTest/test/quantified/offsetmatch001.smt2 @@ -0,0 +1,11 @@ +(set-option :print-success false) +(set-logic UFLIA) +(set-info :source |E-matching with offset equalities: matching f(x) against the ground +term f(a+3) must bind x := a+3 (not the offset-free x := a).|) +(set-info :status unsat) +(declare-fun f (Int) Int) +(declare-fun a () Int) +(assert (forall ((x Int)) (= (f x) x))) +(assert (not (= (f (+ a 3)) (+ a 3)))) +(check-sat) +(exit) diff --git a/SMTInterpolTest/test/quantified/offsetmatch002.smt2 b/SMTInterpolTest/test/quantified/offsetmatch002.smt2 new file mode 100644 index 000000000..2a3006a5c --- /dev/null +++ b/SMTInterpolTest/test/quantified/offsetmatch002.smt2 @@ -0,0 +1,14 @@ +(set-option :print-success false) +(set-logic UFLIA) +(set-info :source |E-matching with offset equalities: a and b share a congruence class +at offset 1 (b = a+1). The substitutions x := a and x := b are different values and +must not be conflated by the representative-based dedup; both instances are needed.|) +(set-info :status unsat) +(declare-fun f (Int) Int) +(declare-fun a () Int) +(declare-fun b () Int) +(assert (= b (+ a 1))) +(assert (forall ((x Int)) (>= (f x) 0))) +(assert (< (+ (f a) (f b)) 0)) +(check-sat) +(exit) diff --git a/SMTInterpolTest/test/quantified/offsetmatch003.smt2 b/SMTInterpolTest/test/quantified/offsetmatch003.smt2 new file mode 100644 index 000000000..133d39d06 --- /dev/null +++ b/SMTInterpolTest/test/quantified/offsetmatch003.smt2 @@ -0,0 +1,13 @@ +(set-option :print-success false) +(set-logic UFLIA) +(set-info :source |E-matching with offset equalities: binding x := a (offset dropped) +from the candidate f(a+1) would produce the instance f(a)=7 => a=a+1 with f(a) +wrongly identified with f(a+1), i.e. a spurious unsat. The correct binding +x := a+1 gives a trivially true instance.|) +(set-info :status sat) +(declare-fun f (Int) Int) +(declare-fun a () Int) +(assert (= (f (+ a 1)) 7)) +(assert (forall ((x Int)) (=> (= (f x) 7) (= x (+ a 1))))) +(check-sat) +(exit)