Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0112a27
Express generic program matching as VM match instructions
unp1 Jun 14, 2026
ba86013
Express context-block program matching as VM match instructions
unp1 Jun 14, 2026
9a84e11
Add a cursor-free compiled taclet find-matcher
unp1 Jun 14, 2026
889f664
Parallelize the runAllProofs testRAP task
unp1 Jun 14, 2026
3d3ba8a
test(dev): matcher differential test + isolated benchmarks [DROP BEFO…
unp1 Jun 14, 2026
a9c9339
matcher: introduce key.ncore.compiler match-plan framework
unp1 Jun 14, 2026
0940397
matcher: build Java-DL find-matchers on the match-plan framework
unp1 Jun 14, 2026
49b33a0
matcher: route VMTacletMatcher through the match-plan framework
unp1 Jun 14, 2026
7d16e62
test(dev): differential + benchmark cover the match-plan framework [D…
unp1 Jun 14, 2026
4916ca4
matcher: collapse the hand-written matchers into the framework (singl…
unp1 Jun 15, 2026
5b11b82
test(dev): drop the oracle-based differential from the PR; framework-…
unp1 Jun 15, 2026
f263517
matcher: add a "compiled matcher" feature flag (GUI toggle)
unp1 Jun 15, 2026
5158a47
perf(matcher): enable the compiled find-matcher by default
unp1 Jun 17, 2026
aa9bbad
perf(strategy): cost reuse across createFurtherApps re-expansion
unp1 Jun 16, 2026
d5a5024
fix(strategy): make introductionTime deterministic (do not cache -1)
unp1 Jun 16, 2026
6a86b3a
refactor(strategy): name the cost-reuse INELIGIBLE sentinel
unp1 Jun 16, 2026
cac2896
perf(strategy): make goal age a first-class container-level cost term
unp1 Jun 16, 2026
1590ff8
perf(strategy): precise op-indexed parking of assumes-incomplete bases
unp1 Jun 16, 2026
4e8cccd
fix(rules): drop redundant order-fragile lenOfSeqSubEQ from automode
unp1 Jun 16, 2026
7d99ba5
perf(label): skip rebuilding unchanged term trees in removeIrrelevant…
unp1 Jun 16, 2026
5dd9819
perf(util): compute Pair.hashCode without a varargs array
unp1 Jun 16, 2026
701da96
perf(strategy): walk the find-position by index in RewriteTacletExecutor
unp1 Jun 16, 2026
da74aaa
perf(loader): release the ANTLR parser DFA caches after loading
unp1 Jun 16, 2026
2387c8e
perf(checkPrefix): skip the prefix walk when the formula has no trans…
unp1 Jun 15, 2026
2271f36
test(perf): add perfTest measurement group (opt-in via -Dkey.runallpr…
unp1 Jun 13, 2026
1d9806a
perf(matcher): compile \assumes formula matching (incl. Java programs…
unp1 Jun 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions key.core/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ dependencies {
api project(':key.util')
api project(':key.ncore')
api project(':key.ncore.calculus')
api project(':key.ncore.compiler')

antlr4 "org.antlr:antlr4:4.13.2"
api "org.antlr:antlr4-runtime:4.13.2"
Expand Down Expand Up @@ -235,14 +236,21 @@ tasks.register("testRAP", Test) {
dependsOn('generateRAPUnitTests', 'testClasses')

forkEvery = 1
maxParallelForks = 2
// run the regression proofs on up to 10 parallel JVMs (overridable with -PrapForks=N);
// for clean perfTest timing reproduction use -PrapForks=1
maxParallelForks = (project.findProperty('rapForks') ?: '10') as int
useJUnitPlatform()
it.filter {
it.includeTestsMatching "de.uka.ilkd.key.proof.runallproofs.gen.*"
}
// set heap size for the test JVM(s)
// forward the compiled-matcher switch to the (in-process) proof runs in each fork; default
// off, enable with -Pmatcher.compiled=true (or -Dkey.matcher.compiled=true)
systemProperty 'key.matcher.compiled',
(project.findProperty('matcher.compiled')
?: System.getProperty('key.matcher.compiled', 'false'))
// set heap size for the test JVM(s) (overridable with -PrapHeap=8g for the large examples)
minHeapSize = "1g"
maxHeapSize = "3g"
maxHeapSize = (project.findProperty('rapHeap') ?: '3g')

// set JVM arguments for the test JVM(s)
//jvmArgs('-XX:MaxPermSize=1g')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,12 @@
import de.uka.ilkd.key.rule.MatchConditions;
import de.uka.ilkd.key.rule.inst.SVInstantiations;

import org.key_project.logic.IntIterator;
import org.key_project.prover.rules.matcher.vm.ProgramChildrenMatcher;
import org.key_project.util.collection.ImmutableArray;
import org.key_project.util.collection.ImmutableSLList;

import org.jspecify.annotations.Nullable;

/**
* In the DL-formulae description of Taclets the program part can have the following form
* {@code < pi
Expand Down Expand Up @@ -130,6 +132,28 @@ public boolean compatibleBlockSize(int pos, int max) {
}

public MatchConditions match(SourceData source, MatchConditions matchCond) {
return match(source, matchCond, null);
}

/**
* Matches this context block against the given source. Phases (1) prefix descent to the active
* statement, (2) inner execution context matching and (4) completion of the context
* instantiation (prefix/suffix positions) are always performed here. Phase (3), the matching of
* the active statements (this block's children from the active offset), is delegated to the
* supplied {@code activeStatements} matcher when one is given (and the located source position
* is a regular child offset); otherwise the built-in {@link #matchChildren} is used. All three
* yield identical results; the {@code activeStatements} matcher (a VM sub-program or a compiled
* matcher) simply matches the active-statement subtree by direct navigation instead of the
* monolithic AST matcher.
*
* @param source the source to match against
* @param matchCond the match conditions found so far
* @param activeStatements a matcher for the active statements, or {@code null} to use the
* built-in {@link #matchChildren} for phase (3)
* @return the resulting match conditions, or {@code null} if matching fails
*/
public MatchConditions match(SourceData source, MatchConditions matchCond,
@Nullable ProgramChildrenMatcher activeStatements) {
assert getPrefixLength() > 0;
SourceData newSource = source;

Expand Down Expand Up @@ -192,8 +216,13 @@ public MatchConditions match(SourceData source, MatchConditions matchCond) {
return null;
}

// matching children
matchCond = matchChildren(newSource, matchCond, executionContext == null ? 0 : 1);
// matching children (the active statements) -- phase (3)
final int offset = executionContext == null ? 0 : 1;
if (activeStatements != null && newSource.getChildPos() >= 0) {
matchCond = matchActiveStatements(newSource, matchCond, activeStatements, offset);
} else {
matchCond = matchChildren(newSource, matchCond, offset);
}

if (matchCond == null) {
return null;
Expand All @@ -205,6 +234,37 @@ public MatchConditions match(SourceData source, MatchConditions matchCond) {
return matchCond;
}

/**
* Phase (3) via a supplied matcher (VM sub-program or compiled): matches the active statements
* of this context block (its children from index {@code offset}) against the children of
* {@code newSource.getElement()} starting at {@code newSource.getChildPos()}. This mirrors
* {@link #matchChildren(SourceData, MatchConditions, int)} for the case where every active
* statement consumes exactly one source child (the only case the generator converts -- list
* schema variables and other variable-arity constructs keep the interpreter). On success the
* source position is advanced exactly as {@code matchChildren} would, so the subsequent
* {@link #makeContextInfoComplete} computes the same suffix start.
*/
private @Nullable MatchConditions matchActiveStatements(SourceData newSource,
MatchConditions matchCond, ProgramChildrenMatcher activeStatements, int offset) {
final int startPos = newSource.getChildPos();
// number of active statements to match (each consumes exactly one source child)
final int n = getChildCount() - offset;
final ProgramElement parent = newSource.getElement();
if (!(parent instanceof NonTerminalProgramElement ntParent)
|| ntParent.getChildCount() < startPos + n) {
// not enough source children -> matchChildren would also fail (null source child)
return null;
}
final MatchConditions result = (MatchConditions) activeStatements.matchChildrenFrom(
parent, startPos, matchCond, newSource.getServices());
if (result == null) {
return null;
}
// advance the source position past the matched children, as matchChildren would
newSource.setChildPos(startPos + n);
return result;
}

/**
* completes match of context block by adding the prefix end position and the suffix start
* position
Expand Down Expand Up @@ -295,10 +355,10 @@ private PosInProgram matchPrefixEnd(final ProgramPrefix prefix, int pos, PosInPr
ProgramPrefix currentPrefix = prefix;
int i = 0;
while (i <= pos) {
final IntIterator it = currentPrefix.getFirstActiveChildPos().iterator();
while (it.hasNext()) {
prefixEnd = prefixEnd.down(it.next());
}
// concatenate this prefix element's active-child position in one step instead of
// iterating + a fresh PosInProgram per position (matchPrefixEnd is the dominant
// cost of a context match; this avoids an IntIterator and intermediate copies)
prefixEnd = prefixEnd.append(currentPrefix.getFirstActiveChildPos());
i++;
if (i <= pos) {
// as fail-fast measure I do not test here using
Expand Down
10 changes: 10 additions & 0 deletions key.core/src/main/java/de/uka/ilkd/key/logic/JTerm.java
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,16 @@ public interface JTerm
*/
boolean containsJavaBlockRecursive();

/**
* Checks if this {@link JTerm} or one of its direct or indirect children has a
* {@link de.uka.ilkd.key.logic.op.Transformer} operator. Cached; used by
* {@link de.uka.ilkd.key.rule.RewriteTaclet#checkPrefix} to skip the prefix walk in the common
* transformer-free case.
*
* @return {@code true} iff a transformer occurs anywhere in the term tree
*/
boolean containsTransformerRecursive();

/**
* Returns a human-readable source of this term. For example the filename with line and offset.
*/
Expand Down
22 changes: 22 additions & 0 deletions key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ private enum ThreeValuedTruth {
*/
private ThreeValuedTruth containsJavaBlockRecursive = ThreeValuedTruth.UNKNOWN;

/** caches whether this term or a (direct/indirect) child has a {@link Transformer} operator. */
private ThreeValuedTruth containsTransformerRecursive = ThreeValuedTruth.UNKNOWN;

// -------------------------------------------------------------------------
// constructors
// -------------------------------------------------------------------------
Expand Down Expand Up @@ -441,5 +444,24 @@ public boolean containsJavaBlockRecursive() {
return containsJavaBlockRecursive == ThreeValuedTruth.TRUE;
}

@Override
public boolean containsTransformerRecursive() {
if (containsTransformerRecursive == ThreeValuedTruth.UNKNOWN) {
ThreeValuedTruth result = ThreeValuedTruth.FALSE;
if (op instanceof Transformer) {
result = ThreeValuedTruth.TRUE;
} else {
for (int i = 0, arity = subs.size(); i < arity; i++) {
if (subs.get(i).containsTransformerRecursive()) {
result = ThreeValuedTruth.TRUE;
break;
}
}
}
this.containsTransformerRecursive = result;
}
return containsTransformerRecursive == ThreeValuedTruth.TRUE;
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@

import java.util.*;
import java.util.Map.Entry;
import java.util.stream.Collectors;

import de.uka.ilkd.key.java.Services;
import de.uka.ilkd.key.logic.*;
Expand Down Expand Up @@ -2156,10 +2155,55 @@ public static JTerm removeIrrelevantLabels(JTerm term, Services services) {
* @see TermLabel#isProofRelevant()
*/
public static JTerm removeIrrelevantLabels(JTerm term, TermFactory tf) {
// Identity-preserving rebuild: only allocate along paths that actually carry an irrelevant
// label, and return the original term unchanged otherwise. The previous implementation
// rebuilt the whole term tree on every call (stream().map()/filter().collect() per node),
// which was the single biggest allocator during proof search (~20%) even though the vast
// majority of subterms have no irrelevant labels and need not change.
final ImmutableArray<JTerm> subs = term.subs();
final int n = subs.size();
JTerm[] newSubs = null; // allocated lazily, only once a sub actually changes
for (int i = 0; i < n; i++) {
final JTerm oldSub = subs.get(i);
final JTerm newSub = removeIrrelevantLabels(oldSub, tf);
if (newSub != oldSub && newSubs == null) {
newSubs = new JTerm[n];
for (int j = 0; j < i; j++) {
newSubs[j] = subs.get(j);
}
}
if (newSubs != null) {
newSubs[i] = newSub;
}
}

final ImmutableArray<TermLabel> labels = term.getLabels();
ImmutableArray<TermLabel> newLabels = labels;
if (!labels.isEmpty()) {
int relevant = 0;
for (int i = 0, sz = labels.size(); i < sz; i++) {
if (labels.get(i).isProofRelevant()) {
relevant++;
}
}
if (relevant != labels.size()) {
final TermLabel[] kept = new TermLabel[relevant];
int k = 0;
for (int i = 0, sz = labels.size(); i < sz; i++) {
final TermLabel l = labels.get(i);
if (l.isProofRelevant()) {
kept[k++] = l;
}
}
newLabels = new ImmutableArray<>(kept);
}
}

if (newSubs == null && newLabels == labels) {
return term; // no irrelevant label anywhere in this subtree -> no allocation
}
return tf.createTerm(term.op(),
new ImmutableArray<>(term.subs().stream().map(t -> removeIrrelevantLabels(t, tf))
.collect(Collectors.toList())),
term.boundVars(), new ImmutableArray<>(term.getLabels().stream()
.filter(TermLabel::isProofRelevant).collect(Collectors.toList())));
newSubs == null ? subs : new ImmutableArray<>(newSubs),
term.boundVars(), newLabels);
}
}
18 changes: 18 additions & 0 deletions key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,24 @@ private static JavaKeYParser createParser(CharStream stream) {
return createParser(createLexer(stream));
}

/**
* Releases the ANTLR prediction (DFA) cache of the KeY parser.
* <p>
* This cache is built lazily while parsing and held on the generated parser's {@code static}
* fields, so it stays resident for the whole JVM -- including during proof search, where it is
* not needed (on a large proof it retains ~15-20 MB). It is a pure cache: ANTLR rebuilds it
* transparently on the next parse, so dropping it is correctness-safe (one-time re-warm on a
* subsequent parse). Intended to be called once a problem/proof has finished loading.
*/
public static void clearParserCaches() {
try {
new JavaKeYParser(new CommonTokenStream(createLexer(CharStreams.fromString(""))))
.getInterpreter().clearDFA();
} catch (RuntimeException e) {
LOGGER.warn("Could not clear parser DFA caches", e);
}
}

public static JavaKeYLexer createLexer(Path file) throws IOException {
return createLexer(CharStreams.fromPath(file));
}
Expand Down
10 changes: 10 additions & 0 deletions key.core/src/main/java/de/uka/ilkd/key/proof/Goal.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
import org.key_project.prover.sequent.Sequent;
import org.key_project.prover.sequent.SequentChangeInfo;
import org.key_project.prover.sequent.SequentFormula;
import org.key_project.prover.strategy.DelegationBasedRuleApplicationManager;
import org.key_project.prover.strategy.RuleApplicationManager;
import org.key_project.util.collection.ImmutableList;
import org.key_project.util.collection.ImmutableSLList;
Expand Down Expand Up @@ -243,6 +244,15 @@ private void fireSequentChanged(SequentChangeInfo sci) {
var time1 = System.nanoTime();
PERF_UPDATE_TAG_MANAGER.getAndAdd(time1 - time);
ruleAppIndex.sequentChanged(sci);
// Feed the change to the (possibly delegation-wrapped) queue manager so it can wake parked
// assumes-bases on their matching round (see QueueRuleApplicationManager#parkedByOp).
RuleApplicationManager<Goal> m = ruleAppManager;
while (m instanceof DelegationBasedRuleApplicationManager<Goal> d) {
m = d.getDelegate();
}
if (m instanceof QueueRuleApplicationManager qm) {
qm.sequentChanged(sci);
}
var time2 = System.nanoTime();
PERF_UPDATE_RULE_APP_INDEX.getAndAdd(time2 - time1);
for (GoalListener listener : listeners) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import de.uka.ilkd.key.java.Services;
import de.uka.ilkd.key.nparser.JavaKeYLexer;
import de.uka.ilkd.key.nparser.KeyAst.ProofScript;
import de.uka.ilkd.key.nparser.ParsingFacade;
import de.uka.ilkd.key.nparser.ProofScriptEntry;
import de.uka.ilkd.key.proof.Node;
import de.uka.ilkd.key.proof.Proof;
Expand All @@ -30,6 +31,7 @@
import de.uka.ilkd.key.settings.ProofIndependentSettings;
import de.uka.ilkd.key.speclang.Contract;
import de.uka.ilkd.key.speclang.SLEnvInput;
import de.uka.ilkd.key.speclang.njml.JmlFacade;
import de.uka.ilkd.key.strategy.Strategy;
import de.uka.ilkd.key.strategy.StrategyProperties;

Expand Down Expand Up @@ -339,6 +341,11 @@ public final void load(Consumer<Proof> callbackProofLoaded) throws Exception {
}
} finally {
control.loadingFinished(this, poContainer, proofList, result);
// parsing is done; release the ANTLR DFA caches so they are not retained during the
// (long) proof search. They are a pure cache and rebuild transparently on the next
// parse.
ParsingFacade.clearParserCaches();
JmlFacade.clearCaches();
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
* structure described by the term of the find-part.
*/
public class RewriteTaclet extends FindTaclet {

/**
* creates a Schematic Theory Specific Rule (Taclet) with the given parameters that represents a
* rewrite rule.
Expand Down Expand Up @@ -108,6 +109,13 @@ private boolean veto(JTerm t) {
public MatchConditions checkPrefix(
PosInOccurrence p_pos,
MatchConditions p_mc) {
// Fast path: for an unrestricted taclet the loop below only vetoes on a Transformer on the
// path; if the formula has none at all (cached), neither can the prefix, so the O(depth)
// walk is skipped.
if (applicationRestriction().equals(ApplicationRestriction.NONE)
&& !((JTerm) p_pos.sequentFormula().formula()).containsTransformerRecursive()) {
return p_mc;
}
int polarity = p_pos.isInAntec() ? -1 : 1; // init polarity
SVInstantiations svi = p_mc.getInstantiations();
// this is assumed to hold
Expand Down
Loading
Loading