Skip to content

Quantifier and Heap Reasoning Improvements - #3961

Open
unp1 wants to merge 14 commits into
mainfrom
bubel/quantifier-shape-fallback
Open

Quantifier and Heap Reasoning Improvements#3961
unp1 wants to merge 14 commits into
mainfrom
bubel/quantifier-shape-fallback

Conversation

@unp1

@unp1 unp1 commented Aug 12, 2026

Copy link
Copy Markdown
Member

Second of three of the last AI supported work

  • Improves the quantifier heuristics and heap reasoning by providing triggers if array access is an affine integer expression.
    The following problem
\programVariables {
    int[] a;
    int[] b;
    int k;
    int m;
    int n;
}

\problem {
       \forall int t; (0 <= t & t < n -> b[k + t] = a[m + t])
   ==> \forall int x; (k <= x & x < k + n -> b[x] = a[x - k + m])
}

can now be proven automatically.

  • It also makes inequality of objects explicit in the sequents enabling more heap simplification rules to be applied automatically. The added taclet has been proven sound and the proof has been added.

  • This PR also addresses issue Incompleteness of quantifier treatment #3972 using the reworked infrastructure of this PR.

Type of pull request

  • Refactoring (behaviour should not change or only minimally change)
  • New feature (non-breaking change which adds functionality)

Ensuring quality

  • I made sure that introduced/changed code is well documented (javadoc and inline comments).
  • I added new test case(s) for new functionality. New problem for RAP being able to close an Adjacency list problem which stayed open before.
  • I have tested the feature as follows: RAP, testing
  • I have checked that runtime performance has not deteriorated. No regressions on RAP, can solve more proofs now. In the worst case the Strategy Options allow to switch back to the old behavior (select GOOD)

Additional information and contact(s)

Created with AI tooling support

The contributions within this pull request are licensed under GPLv2 (only) for inclusion in KeY.

@unp1 unp1 self-assigned this Aug 12, 2026
@unp1
unp1 force-pushed the bubel/quantifier-shape-fallback branch from 63f3f22 to ad12fe2 Compare August 12, 2026 10:34
@unp1 unp1 added the Feature New feature or request label Aug 13, 2026
@unp1
unp1 force-pushed the bubel/quantifier-shape-fallback branch 5 times, most recently from ca5429c to dc363c4 Compare August 20, 2026 20:35
@unp1
unp1 force-pushed the bubel/quantifier-shape-fallback branch from dc363c4 to cc9beac Compare August 21, 2026 11:12
unp1 added 8 commits August 21, 2026 20:58
Improves treatment of quantified formulas involving arrays with affine integer indexes.

Trigger matching compares the structure of two terms, so a fact about
b[srcStart + t] cannot be used on a term about a[x]: the witness
x - srcStart occurs nowhere in the proof. Re-indexing therefore had to be
written as a lemma and discharged by SMT.

A theory can now solve a trigger subterm against the term it should match,
through a new method on QuantifierTheorySupport. Integer arithmetic solves
k*t + rest = s for t by exact division, for a pattern affine in one unbound
variable. A wrong solution costs one instantiation, since instantiating a
universal with any term is sound.
(created with AI tooling support)
Applying a method contract anonymises the heap. A quantified formula from
before the call has its triggers over the old heap, so trigger matching finds
no instance and the property cannot be used afterwards.

For an array read select(h,a,arr(i)) the heap theory now adds the trigger
select(H,a,arr(i)) with a metavariable H for the heap, which matches the read
over any heap. Such a trigger is matched by unification and by basic matching.
An instantiation that basic matching gets by solving a subterm costs 10000
more, so it is tried after the ones from ordinary matches.

The heap theory also supplies the array indices the formula writes as instance
candidates, through a new method on QuantifierTheorySupport. Such an index is
ground, so no trigger contains it, although instantiating with it collapses
the read it is written to.

AdjacencyStore.storeValidList, added here, needs this across its call to
storeList. runAllProofs: 101 of 672 proofs change, 793103 against 803745 nodes.

(created with AI tooling supported)
…alues then o != u

Some heap simplification rules rely in their assumes on the fact that two objects are different, i.e., \assumes (==> o = u). this change makes it more likely for that formula to be actually present
Approval should only be called on taclets whose assumes clause has been matched.
…antiation

QuantifierTheorySupport bundled two unrelated concerns: which subterms of a
theory make a trigger, and what the theory answers about terms and literals.
Trigger selection depends on the terms a profile builds, the answers about
terms do not.

TriggerSupport and TheoryReasoning now hold the two halves, and each caller
takes the one it uses: TriggersSet and Instantiation the first, BasicMatching,
Congruence and PredictCostProver the second. The list of theories moves from
TriggersSet to Profile.getTheorySupports, so a profile over other terms
registers its own.

Instantiation reads its instance candidates from that list too, which retires
the check for the classic treatment it did before: the classic list holds no
heap support, so it yields no candidates.

runAllProofs is node-identical, 675 proofs.

(created with AI tooling support)
The theory modules move to theory, the metavariable and unification
machinery to constraint, and the tie-break strategies to tiebreak. Pure
moves and visibility adjustments; tests of moved package-private classes
move along.
A theory answered two questions about a trigger candidate, rejectsAsTrigger
and prefersEnclosingTrigger, and the selection consulted them at different
points of its descent. The heap theory needed both to express one rule: the
trigger for an array access is the read. Both hooks become one verdict,
ACCEPTABLE, FORBIDDEN or PREFER_ENCLOSING, so a theory selects its triggers
in one method and the descent interprets the verdict in one place. The
created field is recognised by its function symbol instead of a name suffix.

A uni-trigger carried three booleans, onlyUnify, matchByUnification and
isElementOfMultitrigger, and the matcher was chosen by a two-condition test
over the first two. The first two become the kind of the trigger, PATTERN,
GENERALIZED, NEEDS_UNIFY or GENERALIZED_UNIFY, fixed at registration; the
matching switches on the kind. The element flag is a role, not a matching
mode, and stays a flag.

Each clause is read once into a ClauseAnalysis value, literals stripped of
negations and if-then-else expanded, and the descent returns a Search value,
SATISFIED or OPEN, instead of a boolean that meant three things.

runAllProofs is node-identical, 674 proofs, 792744 nodes.

(created with AI tooling support)
Why an instance costs what it costs was encoded in which loop of
addInstances found it, with the surcharge constants at the call sites.
Every instance now carries an Origin, OWN_PATTERN, SOLVED_POSITION,
THEORY_UNIFIED, THEORY_MATCHED or THEORY_DIRECT, and one method states the
surcharge per origin. A trigger treatment becomes the set of origins it
admits, next to its choice of theory supports; the predicate for basic
matching of theory triggers becomes the admission of THEORY_MATCHED, and a
treatment that admits no direct theory instances does not pay for the
matrix walk that would find none.

The instances of a sequent live in an InstanceTable that keeps cost and
origin per instance. The table normalizes a cast-wrapped query in one
place, comparing against the cast symbol; the three copies of the cast
test by name in the cost lookup, the generation rank and the polarity
count are gone, and the tie-break scorers receive the normalized instance.

runAllProofs is node-identical, 674 proofs.

(created with AI tooling support)
@unp1
unp1 force-pushed the bubel/quantifier-shape-fallback branch from 07e26a4 to 1ba8242 Compare August 21, 2026 18:58
@unp1 unp1 linked an issue Aug 21, 2026 that may be closed by this pull request
unp1 added 5 commits August 21, 2026 21:05
Some clauses yield no trigger at all: every literal holding the quantified
variable is forbidden, and what remains covers no variable.

After the covering multi-triggers of a clause are built, the selection now
asks each theory for fallback triggers if the clause got no covering
trigger. A fallback is registered as theory-provided, its instances carry
the FALLBACK origin, and only the most informed treatment admits them. The
treatment decides at the instance, not at registration: the trigger set is
cached per formula and shared by the non-classic treatments, so a set built
under one treatment must serve the other.

No theory offers fallback triggers yet, so this changes no proof.

runAllProofs is node-identical, 674 proofs.

(created with AI tooling support)
Comments across the package let terms and rules speak: a read said which
access is meant, a wrapper said everything through its argument, a term was
never named by the formula. Each such phrase now states its relation: a read
determines the accessed array, a wrapper adds no information to its
argument, a term does not occur in the formula.

Comment change only.

(created with AI tooling support)
Whether a clause had a covering trigger was read off the growth of the
shared trigger collection, and which literal contributed what was not
recorded at all. That encoding missed a trigger an earlier clause had
already registered for the same term, and it left a theory asked for
fallback triggers without any view of the literals.

Selection now produces a ClauseTriggers value: per literal the covering
triggers and the elements it yielded, and whether a covering multi-trigger
was built. A clause is covered if the value says so. The derived triggers
of a subterm are registered once and counted for every later literal that
holds the subterm, classified against that literal's clause.
fallbackTriggers receives the value, so a theory sees which literals yielded
nothing. No theory offers fallback triggers yet, so no proof changes.

(created with AI tooling support)
The fallback triggers were asked for every clause without a covering
trigger. The instantiation binds the first quantified variable, and every
covering trigger binds it, so a formula is instantiated exactly if some of
its clauses is covered. A clause without a covering trigger next to one
that covers is no gap, and asking there only added triggers to a formula
that was never incomplete.

The selection now collects the result of every clause and asks the theories
only if none is covered, still clause by clause, so a theory keeps the
clause's variables and literals in view. After the fallbacks of a clause are
registered the cover search runs once more for it: a fallback that binds
only some of the clause's universal variables is an element and can now
combine into a covering multi-trigger. No clause of the formula had a cover
before, so the second search finds only covers that use a fallback, and
registers no cover twice.

No theory offers fallback triggers yet, so no proof changes.

(created with AI tooling support)
@unp1
unp1 force-pushed the bubel/quantifier-shape-fallback branch from 1ba8242 to 5bf097e Compare August 21, 2026 19:05
@unp1
unp1 enabled auto-merge August 21, 2026 19:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Incompleteness of quantifier treatment

1 participant