From 0112a2750575401c61ef460722ff372f7ffb2bad Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 00:01:40 +0200 Subject: [PATCH 01/26] Express generic program matching as VM match instructions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Java program inside a modality was matched by a single monolithic MatchProgramInstruction delegating to ProgramElement.match. Make the generic program part (ordinary statements/expressions: class + exact arity + child recursion, and non-list program schema variables) matchable by the same instruction VM the rest of the find pattern uses: - MatchProgramElementInstruction (class + exact arity, generic over SyntaxElement) and MatchSubProgramInstruction (runs a sub-program over the modality's program via its own cursor). - The generator converts such programs into a VMInstruction sub-program; anything it does not handle falls back to MatchProgramInstruction. - Seams introduced here: MatchProgram (the match-program abstraction implemented by VMProgramInterpreter, and later by the compiled matcher) and ProgramChildrenMatcher (for matching a run of program children). Gated behind -Dkey.matcher.programInstructions (read at matcher construction so it can be toggled by reloading; default off → unchanged monolithic path); behaviour-preserving when on. Co-Authored-By: Claude Opus 4.8 --- .../SyntaxElementMatchProgramGenerator.java | 135 +++++++++++++++++- .../MatchProgramElementInstruction.java | 39 +++++ .../MatchSubProgramInstruction.java | 39 +++++ .../prover/rules/matcher/vm/MatchProgram.java | 35 +++++ .../matcher/vm/ProgramChildrenMatcher.java | 37 +++++ .../matcher/vm/VMProgramInterpreter.java | 44 +++++- 6 files changed, 324 insertions(+), 5 deletions(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchProgramElementInstruction.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchSubProgramInstruction.java create mode 100644 key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/MatchProgram.java create mode 100644 key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/ProgramChildrenMatcher.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java index b62cff84d0b..fd39aeecd9d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java @@ -4,21 +4,35 @@ package de.uka.ilkd.key.rule.match.vm; import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import de.uka.ilkd.key.java.ast.JavaNonTerminalProgramElement; +import de.uka.ilkd.key.java.ast.JavaProgramElement; +import de.uka.ilkd.key.java.ast.ProgramElement; +import de.uka.ilkd.key.java.ast.SourceData; import de.uka.ilkd.key.logic.GenericArgument; import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.logic.op.*; import de.uka.ilkd.key.logic.sort.GenericSort; import de.uka.ilkd.key.logic.sort.ParametricSortInstance; +import de.uka.ilkd.key.rule.MatchConditions; +import de.uka.ilkd.key.rule.match.vm.instructions.MatchProgramElementInstruction; +import de.uka.ilkd.key.rule.match.vm.instructions.MatchSubProgramInstruction; +import org.key_project.logic.SyntaxElement; import org.key_project.logic.op.Modality; import org.key_project.logic.op.Operator; import org.key_project.logic.op.QuantifiableVariable; import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; import org.key_project.util.collection.ImmutableArray; +import org.jspecify.annotations.Nullable; + import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.*; /** @@ -29,6 +43,24 @@ */ public class SyntaxElementMatchProgramGenerator { + /** + * System property ({@code -Dkey.matcher.programInstructions=true}) selecting whether the Java + * program of a modality is matched by a sub-program of {@link VMInstruction}s (a {@link + * MatchSubProgramInstruction}) instead of the monolithic {@code MatchProgramInstruction}. Only + * patterns built from generic-match element kinds + (non-list) program schema variables are + * converted; anything else (context blocks, loops, value literals, list SVs) falls back to the + * interpreter. Default {@code false} keeps the existing behaviour. + *

+ * Read at matcher-construction time (i.e. when the taclet base is loaded) rather than once at + * class load, so toggling it and reloading the proof switches the behaviour. + */ + public static final String PROGRAM_INSTRUCTIONS_PROPERTY = "key.matcher.programInstructions"; + + /** + * caches, per program-element class, whether it uses the generic {@code match} (no override). + */ + private static final Map, Boolean> GENERIC_MATCH = new ConcurrentHashMap<>(); + /** * creates a matcher for the given pattern * @@ -36,8 +68,23 @@ public class SyntaxElementMatchProgramGenerator { * @return the specialized matcher for the given pattern */ public static VMInstruction[] createProgram(JTerm pattern) { + return createProgram(pattern, Boolean.getBoolean(PROGRAM_INSTRUCTIONS_PROPERTY)); + } + + /** + * creates a matcher for the given pattern, choosing explicitly whether the Java program of a + * modality is matched by converted {@link VMInstruction} sub-programs ({@code true}) or by the + * monolithic {@code MatchProgramInstruction} ({@code false}). The production path uses + * {@link #createProgram(JTerm)} which reads the {@code key.matcher.programInstructions} flag; + * this overload exists mainly to build both variants in one JVM for differential testing. + * + * @param pattern the {@link JTerm} specifying the pattern + * @param programInstructions whether to convert program matching to VM sub-programs + * @return the specialized matcher for the given pattern + */ + public static VMInstruction[] createProgram(JTerm pattern, boolean programInstructions) { ArrayList program = new ArrayList<>(); - createProgram(pattern, program); + createProgram(pattern, program, programInstructions); return program.toArray(new VMInstruction[0]); } @@ -48,8 +95,10 @@ public static VMInstruction[] createProgram(JTerm pattern) { * @param pattern the {@link JTerm} used as pattern for which to create a matcher * @param program the list of {@link MatchInstruction} to which the instructions for matching * {@code pattern} are added. + * @param programInstructions whether to convert program matching to VM sub-programs */ - private static void createProgram(JTerm pattern, ArrayList program) { + private static void createProgram(JTerm pattern, ArrayList program, + boolean programInstructions) { final Operator op = pattern.op(); final ImmutableArray boundVars = pattern.boundVars(); @@ -106,7 +155,10 @@ private static void createProgram(JTerm pattern, ArrayList progra program.add(getMatchIdentityInstruction(mod.kind())); } program.add(gotoNextInstruction()); - program.add(matchProgram(pattern.javaBlock().program())); + final JavaProgramElement prog = pattern.javaBlock().program(); + final VMInstruction progInstr = + programInstructions ? buildProgramInstruction(prog) : null; + program.add(progInstr != null ? progInstr : matchProgram(prog)); program.add(gotoNextSiblingInstruction()); } default -> { @@ -123,11 +175,86 @@ private static void createProgram(JTerm pattern, ArrayList progra } for (int i = 0; i < pattern.arity(); i++) { - createProgram(pattern.sub(i), program); + createProgram(pattern.sub(i), program, programInstructions); } if (!boundVars.isEmpty()) { program.add(unbindVariables(boundVars)); } } + + /** + * Builds the instruction matching the Java program {@code prog} of a modality by direct tree + * navigation, or returns {@code null} if {@code prog} uses a construct the converter does not + * handle (the caller then falls back to the monolithic {@code MatchProgramInstruction}). The + * program is matched generically by a {@link MatchSubProgramInstruction}. + */ + private static @Nullable VMInstruction buildProgramInstruction(JavaProgramElement prog) { + final VMInstruction[] sub = buildProgramSubProgram(prog); + return sub == null ? null : new MatchSubProgramInstruction(new VMProgramInterpreter(sub)); + } + + /** + * Builds a sub-program of {@link VMInstruction}s matching the given Java program by direct tree + * navigation, or returns {@code null} if the program uses a construct the converter does not + * handle (the caller then falls back to the monolithic {@code MatchProgramInstruction}). + */ + private static VMInstruction @Nullable [] buildProgramSubProgram(JavaProgramElement prog) { + final List out = new ArrayList<>(); + return appendProgram(prog, out) ? out.toArray(new VMInstruction[0]) : null; + } + + /** + * Appends instructions matching {@code pe} (and its subtree) to {@code out}, mirroring the + * generic program match (class equality + exact-size child recursion) and reusing the existing + * program-SV instruction. Returns {@code false} (leaving {@code out} unusable) for any + * construct + * that is not safe to convert: list schema variables, other schema variables, and element types + * that override {@code match} (context blocks, loops, value-checking literals, ...). + */ + private static boolean appendProgram(SyntaxElement pe, List out) { + if (pe instanceof ProgramSV psv) { + if (psv.isListSV()) { + return false; // list SV -> variable block size, leave it to the interpreter + } + out.add(getMatchInstructionForSV(psv)); + out.add(gotoNextSiblingInstruction()); + return true; + } + if (pe instanceof SchemaVariable) { + return false; // other schema variables in programs: be safe, fall back + } + if (!(pe instanceof ProgramElement progEl) || !isGenericMatch(progEl)) { + return false; // overrides match (context block, loop, value literal, ...) -> fall back + } + final int childCount = pe.getChildCount(); + out.add(new MatchProgramElementInstruction(pe.getClass(), childCount)); + out.add(gotoNextInstruction()); + for (int i = 0; i < childCount; i++) { + if (!appendProgram(pe.getChild(i), out)) { + return false; + } + } + return true; + } + + /** + * @return whether the element's class uses the generic + * {@code match(SourceData, MatchConditions)} (declared in {@code JavaProgramElement} or + * {@code JavaNonTerminalProgramElement}: class equality + exact-size child recursion) + * rather than its own override. + */ + static boolean isGenericMatch(ProgramElement pe) { + return GENERIC_MATCH.computeIfAbsent(pe.getClass(), c -> { + try { + final Class declaring = + c.getMethod("match", SourceData.class, MatchConditions.class) + .getDeclaringClass(); + return declaring == JavaProgramElement.class + || declaring == JavaNonTerminalProgramElement.class; + } catch (NoSuchMethodException e) { + return false; + } + }); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchProgramElementInstruction.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchProgramElementInstruction.java new file mode 100644 index 00000000000..fe02c7538c9 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchProgramElementInstruction.java @@ -0,0 +1,39 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm.instructions; + +import org.key_project.logic.LogicServices; +import org.key_project.logic.SyntaxElement; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; + +import org.jspecify.annotations.Nullable; + +/** + * Matches a single program element structurally: the current element must have exactly the expected + * concrete class and number of children. This mirrors the generic program matching + * ({@code JavaProgramElement.match} / {@code JavaNonTerminalProgramElement.match}: class equality + * plus an exact block size), and is only emitted for element types that use that generic match (the + * compiler falls back to the interpreter's {@code MatchProgramInstruction} for any type that + * overrides {@code match}, e.g. context blocks, loops or value-checking literals). + */ +public final class MatchProgramElementInstruction implements MatchInstruction { + + private final Class kind; + private final int childCount; + + public MatchProgramElementInstruction(Class kind, int childCount) { + this.kind = kind; + this.childCount = childCount; + } + + @Override + public @Nullable MatchResultInfo match(SyntaxElement actualElement, + MatchResultInfo matchConditions, LogicServices services) { + if (actualElement.getClass() == kind && actualElement.getChildCount() == childCount) { + return matchConditions; + } + return null; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchSubProgramInstruction.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchSubProgramInstruction.java new file mode 100644 index 00000000000..7578fd0dc59 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchSubProgramInstruction.java @@ -0,0 +1,39 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm.instructions; + +import de.uka.ilkd.key.logic.JavaBlock; + +import org.key_project.logic.LogicServices; +import org.key_project.logic.SyntaxElement; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; + +import org.jspecify.annotations.Nullable; + +/** + * Matches the Java program of a modality by running a sub-program of {@code VMInstruction}s over + * the + * program tree (with its own cursor), instead of the monolithic {@code MatchProgramInstruction} + * which delegates to the separate {@code ProgramElement.match} AST matcher. The current element is + * the modality's {@link JavaBlock} (as for {@code MatchProgramInstruction}); the sub-program runs + * on + * its {@code program()}, leaving the outer cursor at the {@code JavaBlock} so the surrounding + * navigation is unchanged. + */ +public final class MatchSubProgramInstruction implements MatchInstruction { + + private final VMProgramInterpreter program; + + public MatchSubProgramInstruction(VMProgramInterpreter program) { + this.program = program; + } + + @Override + public @Nullable MatchResultInfo match(SyntaxElement actualElement, + MatchResultInfo matchConditions, LogicServices services) { + return program.match(((JavaBlock) actualElement).program(), matchConditions, services); + } +} diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/MatchProgram.java b/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/MatchProgram.java new file mode 100644 index 00000000000..adce1a3ea8d --- /dev/null +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/MatchProgram.java @@ -0,0 +1,35 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.vm; + +import org.key_project.logic.LogicServices; +import org.key_project.logic.SyntaxElement; +import org.key_project.prover.rules.instantiation.MatchResultInfo; + +import org.jspecify.annotations.Nullable; + +/** + * A program that matches a syntax element against a fixed pattern (the find expression of a + * taclet). + *

+ * There are two implementations: the {@link VMProgramInterpreter}, which interprets a sequence of + * {@code VMInstruction}s over a generic cursor, and a compiled variant that navigates the term + * structure directly (no cursor) for the patterns it supports. Both are interchangeable; which one + * a + * {@code VMTacletMatcher} uses is selected at construction time, so the system can always fall back + * to the pure interpreter. + */ +public interface MatchProgram { + + /** + * Attempts to match the given syntax element against this program's pattern. + * + * @param toMatch the {@link SyntaxElement} to be matched + * @param mc the initial match conditions; may be extended on success + * @param services the {@link LogicServices} + * @return the resulting {@link MatchResultInfo} on success, or {@code null} if no match + */ + @Nullable + MatchResultInfo match(SyntaxElement toMatch, MatchResultInfo mc, LogicServices services); +} diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/ProgramChildrenMatcher.java b/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/ProgramChildrenMatcher.java new file mode 100644 index 00000000000..0ac8082ff90 --- /dev/null +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/ProgramChildrenMatcher.java @@ -0,0 +1,37 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.vm; + +import org.key_project.logic.LogicServices; +import org.key_project.logic.SyntaxElement; +import org.key_project.prover.rules.instantiation.MatchResultInfo; + +import org.jspecify.annotations.Nullable; + +/** + * Matches a contiguous run of children of a parent syntax element, starting at a given child index. + * This is the abstraction used for the active statements of a context block (phase (3) of the + * context match): the located source element and the offset of the first active statement are + * provided, and the run of active-statement matchers consumes one child each. + *

+ * It is implemented both by the interpreter ({@link VMProgramInterpreter#matchChildrenFrom}, which + * navigates the children with a cursor) and by the compiled matcher (which navigates them directly + * via {@code getChild}), so the same context-block bookkeeping can drive either matcher. + */ +@FunctionalInterface +public interface ProgramChildrenMatcher { + + /** + * Matches the children of {@code parent} starting at index {@code startChild}. + * + * @param parent the element whose children are to be matched + * @param startChild the index of the first child to match against + * @param mc the initial match conditions + * @param services the logic services + * @return the resulting match conditions, or {@code null} if the match fails + */ + @Nullable + MatchResultInfo matchChildrenFrom(SyntaxElement parent, int startChild, MatchResultInfo mc, + LogicServices services); +} diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/VMProgramInterpreter.java b/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/VMProgramInterpreter.java index 711c6138a05..6bb4ed14385 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/VMProgramInterpreter.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/rules/matcher/vm/VMProgramInterpreter.java @@ -26,7 +26,7 @@ * constraints such as variable instantiations if successful, or {@code null} * if the match fails. */ -public class VMProgramInterpreter { +public class VMProgramInterpreter implements MatchProgram, ProgramChildrenMatcher { /** * The sequence of instructions to be executed. @@ -55,6 +55,7 @@ public VMProgramInterpreter(VMInstruction[] instruction) { * @return a {@link MatchResultInfo} containing the result of the match, * or {@code null} if no match was possible */ + @Override public @Nullable MatchResultInfo match(SyntaxElement toMatch, MatchResultInfo mc, LogicServices services) { MatchResultInfo result = mc; @@ -67,4 +68,45 @@ public VMProgramInterpreter(VMInstruction[] instruction) { navi.release(); return result; } + + /** + * Executes the program against the children of {@code parent} starting at child index + * {@code startChild}, i.e. the program is interpreted as a sequence of per-child matchers each + * consuming exactly one child of {@code parent} (advancing via {@code gotoNextSibling}). This + * is + * used to match the active statements of a context block, where matching does not start at the + * root but at a child offset of the located source element (the equivalent of + * {@code matchChildren(source, mc, offset)} on the interpreter side). + *

+ * The caller must guarantee that {@code parent} has at least {@code startChild + k} children, + * where {@code k} is the number of children this program consumes; otherwise the cursor would + * run past the available children. (The context-block matcher ensures this before calling.) + * + * @param parent the element whose children are to be matched + * @param startChild the index of the first child to match against + * @param mc the initial match conditions + * @param services the logic services + * @return the resulting match conditions, or {@code null} if the match fails + */ + @Override + public @Nullable MatchResultInfo matchChildrenFrom(SyntaxElement parent, int startChild, + MatchResultInfo mc, LogicServices services) { + if (instruction.length == 0) { + // nothing to match (empty active-statement block) -> succeed unchanged + return mc; + } + MatchResultInfo result = mc; + final PoolSyntaxElementCursor navi = PoolSyntaxElementCursor.get(parent); + navi.gotoNext(); // descend to the first child of parent + for (int i = 0; i < startChild; i++) { + navi.gotoNextSibling(); // advance to child number startChild + } + int instrPtr = 0; + while (result != null && instrPtr < instruction.length) { + result = instruction[instrPtr].match(navi, result, services); + instrPtr++; + } + navi.release(); + return result; + } } From ba86013f50443c6abc86fe35ef7d7b66d47775ed Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 00:02:04 +0200 Subject: [PATCH 02/26] Express context-block program matching as VM match instructions The .. ... pattern of symbolic-execution taclets (ContextStatementBlock) is matched specially: variable-length prefix descent to the active statement, inner execution context, active-statement matching, and prefix/suffix position bookkeeping. Convert phase (3) -- the active statements -- to VM instructions while keeping the intricate phases (1)(2)(4) in place: - ContextStatementBlock.match gains a phase-(3) seam taking a ProgramChildrenMatcher; the default still uses matchChildren, but a supplied matcher (a VM sub-program here) can drive the active-statement matching instead. - MatchContextStatementBlockInstruction wires a context block to that seam. - VMProgramInterpreter.matchChildrenFrom runs a sub-program over a run of source children from a child offset (the active statements). - The generator emits the context-block instruction for a top-level context block, falling back when an active statement is not convertible. Same -Dkey.matcher.programInstructions gate; behaviour-preserving when on. --- .../key/java/ast/ContextStatementBlock.java | 74 +++++++++++++++++-- .../SyntaxElementMatchProgramGenerator.java | 36 ++++++++- ...MatchContextStatementBlockInstruction.java | 51 +++++++++++++ 3 files changed, 152 insertions(+), 9 deletions(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchContextStatementBlockInstruction.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/ast/ContextStatementBlock.java b/key.core/src/main/java/de/uka/ilkd/key/java/ast/ContextStatementBlock.java index 304e085d9f5..2b3b5c8030d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/ast/ContextStatementBlock.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/ast/ContextStatementBlock.java @@ -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 @@ -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; @@ -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; @@ -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 @@ -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 diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java index fd39aeecd9d..bcb62f832ea 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java @@ -8,6 +8,7 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import de.uka.ilkd.key.java.ast.ContextStatementBlock; import de.uka.ilkd.key.java.ast.JavaNonTerminalProgramElement; import de.uka.ilkd.key.java.ast.JavaProgramElement; import de.uka.ilkd.key.java.ast.ProgramElement; @@ -18,6 +19,7 @@ import de.uka.ilkd.key.logic.sort.GenericSort; import de.uka.ilkd.key.logic.sort.ParametricSortInstance; import de.uka.ilkd.key.rule.MatchConditions; +import de.uka.ilkd.key.rule.match.vm.instructions.MatchContextStatementBlockInstruction; import de.uka.ilkd.key.rule.match.vm.instructions.MatchProgramElementInstruction; import de.uka.ilkd.key.rule.match.vm.instructions.MatchSubProgramInstruction; @@ -186,14 +188,44 @@ private static void createProgram(JTerm pattern, ArrayList progra /** * Builds the instruction matching the Java program {@code prog} of a modality by direct tree * navigation, or returns {@code null} if {@code prog} uses a construct the converter does not - * handle (the caller then falls back to the monolithic {@code MatchProgramInstruction}). The - * program is matched generically by a {@link MatchSubProgramInstruction}. + * handle (the caller then falls back to the monolithic {@code MatchProgramInstruction}). A + * top-level {@link ContextStatementBlock} (the {@code .. ...} pattern of symbolic-execution + * taclets) is matched by a {@link MatchContextStatementBlockInstruction} that converts only the + * active-statement matching; any other program is matched generically by a + * {@link MatchSubProgramInstruction}. */ private static @Nullable VMInstruction buildProgramInstruction(JavaProgramElement prog) { + if (prog instanceof ContextStatementBlock csb) { + final VMInstruction[] active = buildContextActiveStatementsProgram(csb); + return active == null ? null + : new MatchContextStatementBlockInstruction(csb, + new VMProgramInterpreter(active)); + } final VMInstruction[] sub = buildProgramSubProgram(prog); return sub == null ? null : new MatchSubProgramInstruction(new VMProgramInterpreter(sub)); } + /** + * Builds a sub-program matching the active statements of the context block {@code csb} (its + * children from the active offset, i.e. skipping the execution context if present), or returns + * {@code null} if any active statement uses a construct the converter does not handle. The + * resulting program is meant to be run via + * {@link VMProgramInterpreter#matchChildrenFrom(org.key_project.logic.SyntaxElement, int, org.key_project.prover.rules.instantiation.MatchResultInfo, org.key_project.logic.LogicServices)} + * starting at the located source child, so that each per-statement matcher consumes exactly one + * source child -- mirroring {@code matchChildren} on the interpreter side. + */ + private static VMInstruction @Nullable [] buildContextActiveStatementsProgram( + ContextStatementBlock csb) { + final int offset = csb.getExecutionContext() == null ? 0 : 1; + final List out = new ArrayList<>(); + for (int i = offset, n = csb.getChildCount(); i < n; i++) { + if (!appendProgram(csb.getChildAt(i), out)) { + return null; + } + } + return out.toArray(new VMInstruction[0]); + } + /** * Builds a sub-program of {@link VMInstruction}s matching the given Java program by direct tree * navigation, or returns {@code null} if the program uses a construct the converter does not diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchContextStatementBlockInstruction.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchContextStatementBlockInstruction.java new file mode 100644 index 00000000000..1c5955bc2ea --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/instructions/MatchContextStatementBlockInstruction.java @@ -0,0 +1,51 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm.instructions; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.java.ast.ContextStatementBlock; +import de.uka.ilkd.key.java.ast.SourceData; +import de.uka.ilkd.key.logic.JavaBlock; +import de.uka.ilkd.key.rule.MatchConditions; + +import org.key_project.logic.LogicServices; +import org.key_project.logic.SyntaxElement; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; + +import org.jspecify.annotations.Nullable; + +/** + * Matches the Java program of a modality whose program is a {@link ContextStatementBlock} (the + * {@code .. ... } pattern that is ubiquitous in symbolic-execution taclets). The intricate context + * bookkeeping (variable-length prefix descent, inner execution context, prefix/suffix positions) is + * still performed by {@link ContextStatementBlock#match}; only the matching of the active + * statements + * is delegated to the supplied VM sub-program, replacing the monolithic + * {@code MatchProgramInstruction} + * for that subtree. The current element is the modality's {@link JavaBlock} (as for + * {@code MatchProgramInstruction}). + * + * @see ContextStatementBlock#match(SourceData, MatchConditions, VMProgramInterpreter) + */ +public final class MatchContextStatementBlockInstruction implements MatchInstruction { + + private final ContextStatementBlock contextBlock; + private final VMProgramInterpreter activeStatements; + + public MatchContextStatementBlockInstruction(ContextStatementBlock contextBlock, + VMProgramInterpreter activeStatements) { + this.contextBlock = contextBlock; + this.activeStatements = activeStatements; + } + + @Override + public @Nullable MatchResultInfo match(SyntaxElement actualElement, + MatchResultInfo matchConditions, LogicServices services) { + return contextBlock.match( + new SourceData(((JavaBlock) actualElement).program(), -1, (Services) services), + (MatchConditions) matchConditions, activeStatements); + } +} From 9a84e1121f598dc343a38507cc61ce1c075f35da Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 00:02:29 +0200 Subject: [PATCH 03/26] Add a cursor-free compiled taclet find-matcher MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CompiledMatchProgram is a second MatchProgram backend that navigates the term and Java-program structure directly (term.op()/sub(i) and SyntaxElement.getChild), avoiding the PoolSyntaxElementCursor entirely. It compiles essentially the whole find-taclet base: ordinary operators and schema variables, bound variables (quantifiers/substitutions), modalities with their program (generic programs and context blocks), parametric function instances and elementary updates; program elements with their own match (value literals, type refs, loops) and variable-arity children (list SVs #slist) are reused cursor-free by delegating to their own match. VMTacletMatcher selects the compiled find-matcher when -Dkey.matcher.compiled is set (read at construction, so toggling it and reloading switches matchers; default off → the interpreter, which stays the source of truth). Behaviour-preserving; ~1.2-1.7x on matcher-bound proving. CompiledMatchProgramTest checks the compiled matcher against explicit expectations (propositional, function and bound-variable patterns, success and failure). --- .../rule/match/vm/CompiledMatchProgram.java | 475 ++++++++++++++++++ .../key/rule/match/vm/VMTacletMatcher.java | 21 +- .../match/vm/CompiledMatchProgramTest.java | 122 +++++ 3 files changed, 616 insertions(+), 2 deletions(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java new file mode 100644 index 00000000000..e87fe55b9bd --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java @@ -0,0 +1,475 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.util.concurrent.atomic.AtomicLong; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.java.ast.ContextStatementBlock; +import de.uka.ilkd.key.java.ast.JavaProgramElement; +import de.uka.ilkd.key.java.ast.ProgramElement; +import de.uka.ilkd.key.java.ast.SourceData; +import de.uka.ilkd.key.logic.GenericArgument; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.ElementaryUpdate; +import de.uka.ilkd.key.logic.op.LocationVariable; +import de.uka.ilkd.key.logic.op.ModalOperatorSV; +import de.uka.ilkd.key.logic.op.ParametricFunctionInstance; +import de.uka.ilkd.key.logic.op.ProgramSV; +import de.uka.ilkd.key.logic.sort.GenericSort; +import de.uka.ilkd.key.logic.sort.ParametricSortInstance; +import de.uka.ilkd.key.rule.MatchConditions; + +import org.key_project.logic.LogicServices; +import org.key_project.logic.SyntaxElement; +import org.key_project.logic.op.Modality; +import org.key_project.logic.op.Operator; +import org.key_project.logic.op.QuantifiableVariable; +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.ProgramChildrenMatcher; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.util.collection.ImmutableArray; + +import org.jspecify.annotations.Nullable; + +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchGenericSortInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchIdentityInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchInstructionForSV; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getSimilarParametricFunctionInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchAndBindVariables; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchModalOperatorSV; + +/** + * A compiled {@link MatchProgram} for a taclet's find expression. Instead of interpreting a list of + * {@code VMInstruction}s over a generic {@code PoolSyntaxElementCursor}, it navigates the term + * structure directly via {@code term.op()} / {@code term.sub(i)} (and the Java program via + * {@code getChild(i)}), which avoids the cursor entirely. + *

+ * It compiles essentially the whole taclet base: ordinary operators and schema variables, bound + * variables (quantifiers / substitutions), modalities with their Java program (generic programs and + * context blocks, see {@link #compileModality}), parametric function instances and elementary + * updates. Program elements that define their own {@code match} (value literals, type references, + * loops, ...) and generic elements with variable-arity children (a list schema variable + * {@code #slist}) are reused cursor-free by {@linkplain #delegateToMatch delegating to their own + * match}, so the surrounding term stays compiled. {@link #compile(JTerm)} returns {@code null} only + * for the rare patterns it still cannot handle -- term labels, parametric-sort generic arguments, + * unusual schema-variable shapes -- and the caller then falls back to the + * {@code VMProgramInterpreter}, which stays the source of truth so the compiled path can always be + * switched off. + * + * @see org.key_project.prover.rules.matcher.vm.VMProgramInterpreter + */ +public final class CompiledMatchProgram implements MatchProgram { + + /** number of find patterns that were successfully compiled (for measurement). */ + private static final AtomicLong COMPILED = new AtomicLong(); + /** number of find patterns that fell back to the interpreter (for measurement). */ + private static final AtomicLong FALLBACK = new AtomicLong(); + + /** + * A single compiled matching step over a (sub)term. Replaces the cursor-driven instruction + * sequence by direct navigation. + */ + @FunctionalInterface + private interface Step { + @Nullable + MatchResultInfo match(JTerm term, MatchResultInfo mc, LogicServices services); + } + + private final Step root; + + private CompiledMatchProgram(Step root) { + this.root = root; + } + + @Override + public @Nullable MatchResultInfo match(SyntaxElement toMatch, MatchResultInfo mc, + LogicServices services) { + return root.match((JTerm) toMatch, mc, services); + } + + /** + * Compiles the given find pattern, or returns {@code null} if it uses a feature not yet + * supported by the compiler (the caller then uses the interpreter). + * + * @param pattern the find expression of the taclet + * @return a compiled program, or {@code null} to fall back to the interpreter + */ + public static @Nullable CompiledMatchProgram compile(JTerm pattern) { + final Step root = compileStep(pattern); + if (root == null) { + FALLBACK.incrementAndGet(); + return null; + } + COMPILED.incrementAndGet(); + return new CompiledMatchProgram(root); + } + + private static @Nullable Step compileStep(JTerm pattern) { + // term labels are matched by a dedicated instruction; not yet compiled + if (pattern.hasLabels()) { + return null; + } + + final Step core = compileCore(pattern); + if (core == null) { + return null; + } + + final ImmutableArray boundVars = pattern.boundVars(); + if (boundVars.isEmpty()) { + return core; + } + + // bound variables (quantifiers, substitutions, ...): bind the pattern's bound variables to + // the source term's bound variables (renaming-aware), match the operator and subterms in + // that scope, then unbind -- exactly as the interpreter does with + // BindVariablesInstruction / UnbindVariablesInstruction, but cursor-free. The bind + // instruction reads the source term's own bound variables from the element it is given. + final MatchInstruction bind = matchAndBindVariables(boundVars); + return (term, mc, services) -> { + MatchResultInfo r = bind.match(term, mc, services); + if (r == null) { + return null; + } + r = core.match(term, r, services); + if (r == null) { + return null; + } + return ((MatchConditions) r).shrinkRenameTable(); + }; + } + + /** + * Compiles the operator and subterms of {@code pattern} (without the bound-variable / label + * handling, which {@link #compileStep} wraps around this). Returns {@code null} if a construct + * is not yet supported. + */ + private static @Nullable Step compileCore(JTerm pattern) { + final Operator op = pattern.op(); + + if (op instanceof SchemaVariable sv) { + if (pattern.arity() != 0) { + return null; // unusual schema-variable shape; let the interpreter handle it + } + // a schema variable matches the whole (sub)term; reuse the existing SV match logic, + // which already accepts the element directly (no cursor needed) + final MatchInstruction svInstr = getMatchInstructionForSV(sv); + return (term, mc, services) -> svInstr.match(term, mc, services); + } + + // a modality: compile the modal-operator kind, the Java program and the sub-formula(s) + if (op instanceof Modality) { + return compileModality(pattern); + } + + // a parametric function instance: similar-base check + generic-argument matching + subterms + if (op instanceof ParametricFunctionInstance) { + return compileParametricFunction(pattern); + } + + // an elementary update lhs := value: match the left-hand side then the value + if (op instanceof ElementaryUpdate) { + return compileElementaryUpdate(pattern); + } + + final int arity = pattern.arity(); + if (arity == 0) { + // a constant/leaf operator: faithful to MatchIdentityInstruction (reference equality) + return (term, mc, services) -> term.op() == op ? mc : null; + } + + final Step[] subs = new Step[arity]; + for (int i = 0; i < arity; i++) { + final Step s = compileStep(pattern.sub(i)); + if (s == null) { + return null; + } + subs[i] = s; + } + + return (term, mc, services) -> { + if (term.op() != op) { + return null; + } + MatchResultInfo r = mc; + for (int i = 0; i < subs.length; i++) { + r = subs[i].match(term.sub(i), r, services); + if (r == null) { + return null; + } + } + return r; + }; + } + + /** + * Compiles an elementary update {@code lhs := value}: matches the left-hand side (a schema + * variable, or a concrete location variable by identity) then the value subterm, mirroring the + * generator's elementary-update case. + */ + private static @Nullable Step compileElementaryUpdate(JTerm pattern) { + final ElementaryUpdate elUp = (ElementaryUpdate) pattern.op(); + final MatchInstruction lhsMatcher; + if (elUp.lhs() instanceof SchemaVariable sv) { + lhsMatcher = getMatchInstructionForSV(sv); + } else if (elUp.lhs() instanceof LocationVariable locVar) { + lhsMatcher = getMatchIdentityInstruction(locVar); + } else { + return null; // unexpected left-hand side kind -> fall back + } + final Step valueStep = compileStep(pattern.sub(0)); + if (valueStep == null) { + return null; + } + return (term, mc, services) -> { + if (!(term.op() instanceof ElementaryUpdate actualElUp)) { + return null; + } + final MatchResultInfo r = lhsMatcher.match(actualElUp.lhs(), mc, services); + return r == null ? null : valueStep.match(term.sub(0), r, services); + }; + } + + /** + * Compiles a parametric function instance: a similar-base check on the operator, then the + * generic arguments (generic sorts via {@link MatchGenericSortInstruction}, concrete arguments + * by identity), then the subterms. Mirrors the generator's parametric-function case. Returns + * {@code null} if a generic argument uses a parametric sort instance (which the generator does + * not handle either). + */ + private static @Nullable Step compileParametricFunction(JTerm pattern) { + final ParametricFunctionInstance pfi = (ParametricFunctionInstance) pattern.op(); + final MatchInstruction similar = getSimilarParametricFunctionInstruction(pfi); + + final int argCount = pfi.getChildCount(); + final MatchInstruction[] argMatchers = new MatchInstruction[argCount]; + for (int i = 0; i < argCount; i++) { + final GenericArgument arg = (GenericArgument) pfi.getChild(i); + if (arg.sort() instanceof GenericSort gs) { + argMatchers[i] = getMatchGenericSortInstruction(gs); + } else if (arg.sort() instanceof ParametricSortInstance) { + return null; // parametric sort in generic args: generator does not handle it either + } else { + argMatchers[i] = getMatchIdentityInstruction(arg); + } + } + + final int arity = pattern.arity(); + final Step[] subs = new Step[arity]; + for (int i = 0; i < arity; i++) { + final Step s = compileStep(pattern.sub(i)); + if (s == null) { + return null; + } + subs[i] = s; + } + + return (term, mc, services) -> { + if (!(term.op() instanceof ParametricFunctionInstance actualPfi)) { + return null; + } + MatchResultInfo r = similar.match(actualPfi, mc, services); + for (int i = 0; r != null && i < argCount; i++) { + r = argMatchers[i].match(actualPfi.getChild(i), r, services); + } + for (int i = 0; r != null && i < subs.length; i++) { + r = subs[i].match(term.sub(i), r, services); + } + return r; + }; + } + + /** + * A single compiled matching step over a program (sub)element. Navigates the Java AST directly + * via {@code getChild(i)} instead of a cursor, mirroring the converted program VM instructions. + */ + @FunctionalInterface + private interface ProgStep { + @Nullable + MatchResultInfo match(SyntaxElement actual, MatchResultInfo mc, LogicServices services); + } + + /** + * Compiles a modality pattern {@code \ phi}: the modal-operator kind (reusing the + * existing element-based instructions), the Java program (generic program or context block, + * cursor-free) and the sub-formula(s). Returns {@code null} if the program or a sub-formula + * uses + * a construct the compiler does not handle (the caller then falls back to the interpreter). + */ + private static @Nullable Step compileModality(JTerm pattern) { + final Modality mod = (Modality) pattern.op(); + final MatchInstruction kindInstr = + mod.kind() instanceof ModalOperatorSV sv ? matchModalOperatorSV(sv) + : getMatchIdentityInstruction(mod.kind()); + + final JavaProgramElement prog = pattern.javaBlock().program(); + final Step progMatch; + if (prog instanceof ContextStatementBlock csb) { + final ProgStep[] active = compileActiveStatements(csb); + if (active != null) { + // phase (3) of the context match, cursor-free: each active statement consumes one + // child + final ProgramChildrenMatcher phase3 = (parent, startChild, mc, services) -> { + MatchResultInfo r = mc; + for (int k = 0; k < active.length; k++) { + r = active[k].match(parent.getChild(startChild + k), r, services); + if (r == null) { + return null; + } + } + return r; + }; + // phases (1)(2)(4) stay in ContextStatementBlock.match; only phase (3) is compiled + progMatch = (term, mc, services) -> csb.match( + new SourceData(term.javaBlock().program(), -1, (Services) services), + (MatchConditions) mc, phase3); + } else { + // an active statement is variable-arity (a list SV) or otherwise uncompilable: + // delegate the whole context match to the interpreter (its matchChildren handles + // list SVs); the surrounding term skeleton stays compiled + progMatch = (term, mc, services) -> csb.match( + new SourceData(term.javaBlock().program(), -1, (Services) services), + (MatchConditions) mc); + } + } else { + final ProgStep ps = compileProgram(prog); + if (ps == null) { + return null; + } + progMatch = (term, mc, services) -> ps.match(term.javaBlock().program(), mc, services); + } + + final int arity = pattern.arity(); + final Step[] subs = new Step[arity]; + for (int i = 0; i < arity; i++) { + final Step s = compileStep(pattern.sub(i)); + if (s == null) { + return null; + } + subs[i] = s; + } + + return (term, mc, services) -> { + if (!(term.op() instanceof Modality m)) { + return null; + } + MatchResultInfo r = kindInstr.match(m.kind(), mc, services); + if (r == null) { + return null; + } + r = progMatch.match(term, r, services); + if (r == null) { + return null; + } + for (int i = 0; i < subs.length; i++) { + r = subs[i].match(term.sub(i), r, services); + if (r == null) { + return null; + } + } + return r; + }; + } + + /** + * Compiles a Java program (sub)element: a generic-match element with a fixed, one-source-child + * structure is matched by direct {@code getChild} navigation (class equality + exact-size child + * recursion); a non-list program schema variable reuses its program-SV instruction. Anything + * else that is still a {@link ProgramElement} -- an element with its own {@code match} (value + * literals, type references, loops, ...) or a generic element whose children are not a + * fixed one-to-one structure (e.g. they contain a list schema variable {@code #slist}) -- is + * matched cursor-free by {@linkplain #delegateToMatch delegating to its own match}. Returns + * {@code null} only for a list schema variable on its own (variable arity: its enclosing + * element + * delegates) and for non-program schema variables. + */ + private static @Nullable ProgStep compileProgram(SyntaxElement pe) { + if (pe instanceof ProgramSV psv) { + if (psv.isListSV()) { + // a list SV by itself is variable-arity; the enclosing element delegates instead + return null; + } + final MatchInstruction svInstr = getMatchInstructionForSV(psv); + return svInstr::match; + } + if (pe instanceof SchemaVariable) { + return null; // other schema variables in programs: be safe, fall back + } + if (!(pe instanceof ProgramElement progEl)) { + return null; + } + if (SyntaxElementMatchProgramGenerator.isGenericMatch(progEl)) { + final int childCount = pe.getChildCount(); + final ProgStep[] subs = new ProgStep[childCount]; + boolean fixedStructure = true; + for (int i = 0; i < childCount; i++) { + final ProgStep s = compileProgram(pe.getChild(i)); + if (s == null) { + fixedStructure = false; // e.g. a list SV child -> not one-to-one + break; + } + subs[i] = s; + } + if (fixedStructure) { + final Class kind = pe.getClass(); + return (actual, mc, services) -> { + if (actual.getClass() != kind || actual.getChildCount() != childCount) { + return null; + } + MatchResultInfo r = mc; + for (int i = 0; i < childCount; i++) { + r = subs[i].match(actual.getChild(i), r, services); + if (r == null) { + return null; + } + } + return r; + }; + } + } + // an element with its own match (value literals, TypeRef, SchematicFieldReference, + // VariableSpecification, loops, ...) or a generic element with variable-arity children + // (a list SV): reuse its own match cursor-free (see delegateToMatch) + return delegateToMatch(progEl); + } + + /** + * Matches {@code progEl} by reusing its own {@code match(SourceData, MatchConditions)} + * cursor-free, exactly as {@code MatchProgramInstruction} does at the program root. This keeps + * the surrounding program compiled (only this sub-element delegates) and is + * behaviour-preserving + * by construction: it is the very match the interpreter would call, including the + * {@code matchChildren} handling of list schema variables. + */ + private static ProgStep delegateToMatch(ProgramElement progEl) { + return (actual, mc, services) -> progEl.match( + new SourceData((ProgramElement) actual, -1, (Services) services), (MatchConditions) mc); + } + + /** + * Compiles the active statements of a context block (its children from the active offset, i.e. + * skipping the execution context if present), or returns {@code null} if any active statement + * uses a construct the compiler does not handle. + */ + private static ProgStep @Nullable [] compileActiveStatements(ContextStatementBlock csb) { + final int offset = csb.getExecutionContext() == null ? 0 : 1; + final ProgStep[] active = new ProgStep[csb.getChildCount() - offset]; + for (int i = offset, n = csb.getChildCount(); i < n; i++) { + final ProgStep s = compileProgram(csb.getChildAt(i)); + if (s == null) { + return null; + } + active[i - offset] = s; + } + return active; + } + + /** @return {@code [compiled, fallback]} pattern counts since startup (for measurement). */ + public static long[] statistics() { + return new long[] { COMPILED.get(), FALLBACK.get() }; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java index 2a877e4fabc..977bccca39f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java @@ -29,6 +29,7 @@ import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; import org.key_project.prover.rules.instantiation.AssumesMatchResult; import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; import org.key_project.prover.sequent.Sequent; import org.key_project.prover.sequent.SequentFormula; @@ -56,8 +57,18 @@ */ public class VMTacletMatcher implements TacletMatcher { + /** + * System property ({@code -Dkey.matcher.compiled=true}) selecting the cursor-free compiled find + * matcher (direct term navigation where the pattern allows, interpreter otherwise). Default + * {@code false} keeps the pure interpreter. + *

+ * Read in the constructor (i.e. per taclet, when the taclet base is loaded) rather than once at + * class load, so toggling it and reloading the proof switches matchers. + */ + public static final String COMPILE_MATCHERS_PROPERTY = "key.matcher.compiled"; + /** the matcher for the find expression of the taclet */ - private final VMProgramInterpreter findMatchProgram; + private final MatchProgram findMatchProgram; /** the matcher for the taclet's assumes formulas */ private final HashMap assumesMatchPrograms = new HashMap<>(); @@ -99,8 +110,14 @@ public VMTacletMatcher(Taclet taclet) { findExp = findTaclet.find(); ignoreTopLevelUpdates = taclet.ignoreTopLevelUpdates() && !(findExp.op() instanceof UpdateApplication); - findMatchProgram = + final VMProgramInterpreter interpreter = new VMProgramInterpreter(SyntaxElementMatchProgramGenerator.createProgram(findExp)); + if (Boolean.getBoolean(COMPILE_MATCHERS_PROPERTY)) { + final CompiledMatchProgram compiled = CompiledMatchProgram.compile(findExp); + findMatchProgram = compiled != null ? compiled : interpreter; + } else { + findMatchProgram = interpreter; + } } else { ignoreTopLevelUpdates = false; diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java new file mode 100644 index 00000000000..808001d2ae2 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java @@ -0,0 +1,122 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.parser.ParserException; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.rule.FindTaclet; +import de.uka.ilkd.key.rule.MatchConditions; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.util.HelperClassForTests; + +import org.key_project.logic.Name; +import org.key_project.prover.rules.instantiation.MatchResultInfo; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Unit tests for the cursor-free {@link CompiledMatchProgram} find-matcher, the compiled + * counterpart of {@link VMTacletMatcherTest} (which covers the interpreter). For the same taclets + * and the same matching / non-matching terms it asserts that the compiled matcher produces the + * expected result -- success with the expected schema-variable instantiations, and {@code null} on + * the failure cases -- so the compiled path is checked independently of the differential test, and + * in particular against explicit expectations rather than only against the interpreter. + * + *

+ * Coverage focuses on term-level matching (propositional / function patterns) and, importantly, + * {@link #compiledBoundVariableMatching() bound variables} (quantifiers / renaming), which the + * compiler handles cursor-free. + */ +public class CompiledMatchProgramTest { + + private static final MatchResultInfo EMPTY = MatchConditions.EMPTY_MATCHCONDITIONS; + + private static Services services; + private static FindTaclet propositional; // taclet_match_rule_1: phi & psi + private static FindTaclet function; // taclet_match_rule_2: f(...) + private static FindTaclet binder; // taclet_match_rule_3: \forall x; ... + + @BeforeAll + public static void init() { + final ProofAggregate pa = HelperClassForTests.parse( + HelperClassForTests.TESTCASE_DIRECTORY.resolve("tacletmatch") + .resolve("tacletMatch1.key")); + services = pa.getFirstProof().getServices(); + propositional = findTaclet(pa, "taclet_match_rule_1"); + function = findTaclet(pa, "taclet_match_rule_2"); + binder = findTaclet(pa, "taclet_match_rule_3"); + } + + private static FindTaclet findTaclet(ProofAggregate pa, String name) { + final Taclet t = + pa.getFirstProof().getInitConfig().lookupActiveTaclet(new Name(name)); + assertTrue(t instanceof FindTaclet, name + " must be a find taclet"); + return (FindTaclet) t; + } + + /** compiles the find expression; the taclets here are all within the compiler's coverage. */ + private static CompiledMatchProgram compile(FindTaclet t) { + final CompiledMatchProgram p = CompiledMatchProgram.compile((JTerm) t.find()); + assertNotNull(p, "find pattern of " + t.name() + " was expected to compile"); + return p; + } + + private MatchResultInfo match(CompiledMatchProgram p, String term) throws ParserException { + return p.match(services.getTermBuilder().parseTerm(term), EMPTY, services); + } + + @Test + public void compiledPropositionalMatching() throws ParserException { + final CompiledMatchProgram p = compile(propositional); + + final JTerm toMatch = services.getTermBuilder().parseTerm("A & B"); + final MatchResultInfo mc = p.match(toMatch, EMPTY, services); + assertNotNull(mc, "compiled matcher should match A & B"); + assertSame(toMatch.sub(0), mc.getInstantiations().lookupValue(new Name("phi"))); + assertSame(toMatch.sub(1), mc.getInstantiations().lookupValue(new Name("psi"))); + + for (String matching : new String[] { "(!A | (A<->B)) & B", "A & (B & A)", + "(\\forall int x; x>=0) & A" }) { + assertNotNull(match(p, matching), "compiled matcher should match " + matching); + } + // failure cases + for (String nonMatching : new String[] { "A | (B & A)", "A", + "\\forall int x;(x>=0 & A)" }) { + assertNull(match(p, nonMatching), "compiled matcher should not match " + nonMatching); + } + } + + @Test + public void compiledFunctionMatching() throws ParserException { + final CompiledMatchProgram p = compile(function); + + for (String matching : new String[] { "f(1, 1, 2)", "f(c, c, d)" }) { + assertNotNull(match(p, matching), "compiled matcher should match " + matching); + } + // failure cases: wrong shape / different head symbol / repeated-SV mismatch + for (String nonMatching : new String[] { "f(1,2,1)", "g(1,1,2)", "h(1,1)", "c", + "z(1,1,1,1)", "f(c,d,c)" }) { + assertNull(match(p, nonMatching), "compiled matcher should not match " + nonMatching); + } + } + + @Test + public void compiledBoundVariableMatching() throws ParserException { + final CompiledMatchProgram p = compile(binder); + + assertNotNull(match(p, "\\forall int x; x + 1 > 0"), + "compiled matcher should match the bound-variable pattern"); + // failure case: the body shape differs (1 + x rather than x + 1) + assertNull(match(p, "\\forall int x; 1 + x > 0"), + "compiled matcher should not match a differing bound-variable body"); + } +} From 889f66471b5fdda7f28503937d66e4735d9f134b Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 00:02:50 +0200 Subject: [PATCH 04/26] Parallelize the runAllProofs testRAP task testRAP (generated runAllProofs regression suite, in-process ProveTest per fork) now runs on up to 10 parallel JVMs (-PrapForks=N), with a configurable per-fork heap (-PrapHeap), and forwards the compiled-matcher switch (-Pmatcher.compiled=true / -Dkey.matcher.compiled) to the proof runs so the regression suite can exercise the compiled matcher. --- key.core/build.gradle | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/key.core/build.gradle b/key.core/build.gradle index 128931a290c..73146343a6f 100644 --- a/key.core/build.gradle +++ b/key.core/build.gradle @@ -235,14 +235,20 @@ 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) + 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') From 3d3ba8ac29cfcfbdc940205d2a90fd8f87e286c8 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 00:02:50 +0200 Subject: [PATCH 05/26] test(dev): matcher differential test + isolated benchmarks [DROP BEFORE MERGE] Development-only verification/measurement, not intended for the PR: - ProgramMatchDifferentialTest: every find-taclet matched by the interpreter oracle vs the converted/compiled matchers over a real-proof term corpus, asserting identical results (success/failure + instantiations). - CompiledMatchProgramBenchmark / ContextMatchBenchmark: isolated interpreter-vs-compiled matching-time measurements. --- .../vm/CompiledMatchProgramBenchmark.java | 184 ++++++++++++ .../rule/match/vm/ContextMatchBenchmark.java | 261 ++++++++++++++++++ .../vm/ProgramMatchDifferentialTest.java | 239 ++++++++++++++++ 3 files changed, 684 insertions(+) create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java new file mode 100644 index 00000000000..5e5f384541d --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java @@ -0,0 +1,184 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.rule.FindTaclet; +import de.uka.ilkd.key.rule.MatchConditions; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.util.HelperClassForTests; + +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; +import org.key_project.prover.sequent.SequentFormula; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Isolated micro-benchmark for the find matcher: it compares {@link VMProgramInterpreter} against + * {@link CompiledMatchProgram} directly (no taclet index, strategy or proof pipeline), over the + * subset of the real taclet base that the compiler handles (FOL / integer / propositional patterns; + * program symbolic-execution rules are excluded by the compiler and thus not part of the + * comparison). + * + *

+ * By default it runs on the self-contained {@code tacletMatch1.key}. Point it at a wider set (e.g. + * the bundled TPTP PUZ/SYN problems, which load the full FOL taclet base) with + * {@code -Dbench.problems=/abs/a.key,/abs/b.key}. Run with + * {@code ./gradlew :key.core:test --tests *CompiledMatchProgramBenchmark}. + */ +public class CompiledMatchProgramBenchmark { + + private static final MatchResultInfo EMPTY = MatchConditions.EMPTY_MATCHCONDITIONS; + + /** supplementary closed formulas (used when a problem's own sequent yields few terms). */ + private static final String[] CORPUS_FORMULAS = { + "A & B", "(!A | (A <-> B)) & B", "A -> (B -> A)", "\\forall int x; x >= 0", + "\\forall int x; x + 1 > x", "\\forall int x; \\exists int y; x + y = 0", + "1 + 2 * 3 = 7", "\\forall int x; \\forall int y; (x + y = y + x)" + }; + + private record Task(List interps, List comps, + List corpus, Services services) { + } + + @Test + public void benchmarkInterpreterVsCompiled() { + final List tasks = new ArrayList<>(); + for (String p : problemPaths()) { + final Task t = buildTask(p); + if (t != null) { + tasks.add(t); + } + } + if (tasks.isEmpty()) { + return; + } + + // warmup + for (int pass = 0; pass < 5; pass++) { + for (Task t : tasks) { + runInterp(t); + runComp(t); + } + } + + // timed: alternate phases per pass to average out JIT / cache effects + final int passes = 30; + long interpMatches = 0, compMatches = 0, interpNanos = 0, compNanos = 0; + for (int pass = 0; pass < passes; pass++) { + for (Task t : tasks) { + long t0 = System.nanoTime(); + interpMatches += runInterp(t); + interpNanos += System.nanoTime() - t0; + + t0 = System.nanoTime(); + compMatches += runComp(t); + compNanos += System.nanoTime() - t0; + } + } + + System.out.printf("[isolated matcher, %d problem(s)] interpreter=%.1f ms compiled=%.1f ms" + + " speedup=%.2fx (matches interp=%d comp=%d)%n", + tasks.size(), interpNanos / 1e6, compNanos / 1e6, + (double) interpNanos / compNanos, interpMatches / passes, compMatches / passes); + assertEquals(interpMatches, compMatches, + "compiled and interpreter must agree on the number of matches"); + } + + private static List problemPaths() { + final String prop = System.getProperty("bench.problems"); + if (prop != null && !prop.isBlank()) { + return List.of(prop.split(",")); + } + return List.of(HelperClassForTests.TESTCASE_DIRECTORY.resolve("tacletmatch") + .resolve("tacletMatch1.key").toString()); + } + + private static Task buildTask(String pathStr) { + final Path path = Path.of(pathStr.trim()); + if (!Files.exists(path)) { + System.out.println(" (skip, not found) " + path); + return null; + } + final ProofAggregate pa = HelperClassForTests.parse(path); + final Services services = pa.getFirstProof().getServices(); + + final List corpus = new ArrayList<>(); + for (SequentFormula sf : pa.getFirstProof().root().sequent()) { + collectSubterms((JTerm) sf.formula(), corpus); + } + for (String f : CORPUS_FORMULAS) { + try { + collectSubterms(services.getTermBuilder().parseTerm(f), corpus); + } catch (Exception ignored) { + // formula not parseable in this problem's signature + } + } + + final List interps = new ArrayList<>(); + final List comps = new ArrayList<>(); + int findTaclets = 0; + for (Taclet t : pa.getFirstProof().getInitConfig().activatedTaclets()) { + if (!(t instanceof FindTaclet ft)) { + continue; + } + findTaclets++; + final JTerm find = (JTerm) ft.find(); + final CompiledMatchProgram comp = CompiledMatchProgram.compile(find); + if (comp == null) { + continue; + } + comps.add(comp); + interps.add( + new VMProgramInterpreter(SyntaxElementMatchProgramGenerator.createProgram(find))); + } + System.out.printf(" %-22s findTaclets=%4d compilable=%4d (%2.0f%%) corpus=%d%n", + path.getFileName(), findTaclets, comps.size(), + findTaclets == 0 ? 0 : 100.0 * comps.size() / findTaclets, corpus.size()); + return new Task(interps, comps, corpus, services); + } + + private static long runInterp(Task t) { + long matches = 0; + for (int p = 0, np = t.interps.size(); p < np; p++) { + final VMProgramInterpreter prog = t.interps.get(p); + for (int i = 0, n = t.corpus.size(); i < n; i++) { + if (prog.match(t.corpus.get(i), EMPTY, t.services) != null) { + matches++; + } + } + } + return matches; + } + + private static long runComp(Task t) { + long matches = 0; + for (int p = 0, np = t.comps.size(); p < np; p++) { + final CompiledMatchProgram prog = t.comps.get(p); + for (int i = 0, n = t.corpus.size(); i < n; i++) { + if (prog.match(t.corpus.get(i), EMPTY, t.services) != null) { + matches++; + } + } + } + return matches; + } + + private static void collectSubterms(JTerm t, List out) { + out.add(t); + for (int i = 0, n = t.arity(); i < n; i++) { + collectSubterms(t.sub(i), out); + } + } +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java new file mode 100644 index 00000000000..17a0c14e470 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java @@ -0,0 +1,261 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.FindTaclet; +import de.uka.ilkd.key.rule.MatchConditions; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.util.HelperClassForTests; +import de.uka.ilkd.key.util.ProofStarter; + +import org.key_project.logic.op.Modality; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; +import org.key_project.prover.sequent.SequentFormula; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Isolated micro-benchmark for the program (symbolic-execution) find matcher: it compares the + * cursor-based interpreter ({@link VMProgramInterpreter}) against the cursor-free compiled matcher + * ({@link CompiledMatchProgram}), over the subset of program-bearing taclets that the compiler + * handles (modality / context-block patterns; step 3). Both are built from the same find term and + * run directly (no taclet index, strategy or proof pipeline), so this measures only the matcher. + * + *

+ * The corpus is harvested by running a bounded amount of symbolic execution on a real proof and + * collecting the modality sub-terms (the redex candidates that drive program matching). By + * default it runs on {@code proofStarter/CC/project.key}; point it at any problem with + * {@code -Dbench.problems=/abs/a.key,/abs/b.key} (e.g. a straight-line problem), bound the harvest + * with {@code -Dbench.steps=N} and the timed passes with {@code -Dbench.passes=N}. Run with + * {@code ./gradlew :key.core:test --tests *ContextMatchBenchmark}. + */ +public class ContextMatchBenchmark { + + private static final MatchResultInfo EMPTY = MatchConditions.EMPTY_MATCHCONDITIONS; + + private static final int STEPS = Integer.getInteger("bench.steps", 6000); + private static final int PASSES = Integer.getInteger("bench.passes", 30); + + private record Task(List interp, List compiled, + List corpus, Services services, String label, int programTaclets, + int[] deepProg, int[] deepTerm) { + } + + @Test + public void benchmarkInterpreterVsCompiled() throws Exception { + final List> envs = new ArrayList<>(); + final List tasks = new ArrayList<>(); + try { + for (String p : problemPaths()) { + final Path path = Path.of(p.trim()); + if (!Files.exists(path)) { + System.out.println(" (skip, not found) " + path); + continue; + } + final KeYEnvironment env = + KeYEnvironment.load(path, null, null, null); + envs.add(env); + tasks.add(buildTask(env, path.getFileName().toString())); + } + if (tasks.isEmpty()) { + return; + } + + // warmup + for (int pass = 0; pass < 5; pass++) { + for (Task t : tasks) { + run(t.interp, t); + run(t.compiled, t); + runDeep(t.interp, t); + runDeep(t.compiled, t); + } + } + + // (A) mixed sweep: every compilable taclet x every modality term (mostly fail-fast, + // the common case in real proving); (B) focused on the deep/matching pairs. + long interpMatches = 0, compMatches = 0, interpNanos = 0, compNanos = 0; + long interpDeepNanos = 0, compDeepNanos = 0; + for (int pass = 0; pass < PASSES; pass++) { + for (Task t : tasks) { + long t0 = System.nanoTime(); + interpMatches += run(t.interp, t); + interpNanos += System.nanoTime() - t0; + + t0 = System.nanoTime(); + compMatches += run(t.compiled, t); + compNanos += System.nanoTime() - t0; + + t0 = System.nanoTime(); + runDeep(t.interp, t); + interpDeepNanos += System.nanoTime() - t0; + + t0 = System.nanoTime(); + runDeep(t.compiled, t); + compDeepNanos += System.nanoTime() - t0; + } + } + + int deepPairs = 0; + for (Task t : tasks) { + deepPairs += t.deepProg.length; + System.out.printf( + " %-26s programTaclets=%d compilable=%d modalityCorpus=%d deepPairs=%d%n", + t.label, t.programTaclets, t.interp.size(), t.corpus.size(), t.deepProg.length); + } + System.out.printf( + "[program matcher, %d task(s), %d passes]%n" + + " (A) mixed sweep interpreter=%.1f ms compiled=%.1f ms speedup=%.2fx%n" + + " (B) deep matches interpreter=%.1f ms compiled=%.1f ms speedup=%.2fx" + + " (%d pairs/pass)%n", + tasks.size(), PASSES, + interpNanos / 1e6, compNanos / 1e6, (double) interpNanos / compNanos, + interpDeepNanos / 1e6, compDeepNanos / 1e6, + (double) interpDeepNanos / compDeepNanos, deepPairs); + assertEquals(interpMatches, compMatches, + "interpreter and compiled matcher must agree on the number of matches"); + } finally { + for (KeYEnvironment env : envs) { + env.dispose(); + } + } + } + + private static Task buildTask(KeYEnvironment env, String label) { + final Proof proof = env.getLoadedProof(); + final Services services = proof.getServices(); + + final ProofStarter ps = new ProofStarter(false); + ps.init(proof); + ps.setMaxRuleApplications(STEPS); + ps.start(); + + final List corpus = harvestModalityCorpus(proof); + + final List interp = new ArrayList<>(); + final List compiled = new ArrayList<>(); + int programTaclets = 0; + for (Taclet t : proof.getInitConfig().activatedTaclets()) { + if (!(t instanceof FindTaclet ft) || !(ft.find() instanceof JTerm find) + || !containsModality(find)) { + continue; + } + programTaclets++; + final CompiledMatchProgram comp = CompiledMatchProgram.compile(find); + if (comp == null) { + continue; // not compilable -> would use the interpreter in production + } + // oracle interpreter for the same find (programInstructions=false: monolithic + // MatchProgramInstruction, the current production interpreter path) + interp.add( + new VMProgramInterpreter( + SyntaxElementMatchProgramGenerator.createProgram(find, false))); + compiled.add(comp); + } + + // collect the (program, term) pairs that actually match -- the deep matches that exercise + // the program/context walk (the mixed sweep is >99% fail-fast and hides them) + final List deep = new ArrayList<>(); + for (int p = 0, np = interp.size(); p < np; p++) { + for (int i = 0, n = corpus.size(); i < n; i++) { + if (interp.get(p).match(corpus.get(i), EMPTY, services) != null) { + deep.add(new int[] { p, i }); + } + } + } + final int[] deepProg = new int[deep.size()]; + final int[] deepTerm = new int[deep.size()]; + for (int k = 0; k < deep.size(); k++) { + deepProg[k] = deep.get(k)[0]; + deepTerm[k] = deep.get(k)[1]; + } + return new Task(interp, compiled, corpus, services, label, programTaclets, deepProg, + deepTerm); + } + + private static long run(List progs, Task t) { + long matches = 0; + for (int p = 0, np = progs.size(); p < np; p++) { + final MatchProgram prog = progs.get(p); + for (int i = 0, n = t.corpus.size(); i < n; i++) { + if (prog.match(t.corpus.get(i), EMPTY, t.services) != null) { + matches++; + } + } + } + return matches; + } + + /** runs only the (program, term) pairs that match -- isolates the deep program/context walk. */ + private static long runDeep(List progs, Task t) { + long matches = 0; + for (int k = 0, n = t.deepProg.length; k < n; k++) { + if (progs.get(t.deepProg[k]).match(t.corpus.get(t.deepTerm[k]), EMPTY, + t.services) != null) { + matches++; + } + } + return matches; + } + + private static List problemPaths() { + final String prop = System.getProperty("bench.problems"); + if (prop != null && !prop.isBlank()) { + return List.of(prop.split(",")); + } + return List.of(HelperClassForTests.TESTCASE_DIRECTORY + .resolve("proofStarter/CC/project.key").toString()); + } + + /** harvests the deduplicated modality sub-terms (redex candidates) from every proof node. */ + private static List harvestModalityCorpus(Proof proof) { + final Set seen = new LinkedHashSet<>(); + final Iterator nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node n = nodes.next(); + for (SequentFormula sf : n.sequent()) { + collectModalities((JTerm) sf.formula(), seen); + } + } + return new ArrayList<>(seen); + } + + private static void collectModalities(JTerm t, Set out) { + if (t.op() instanceof Modality) { + out.add(t); + } + for (int i = 0, n = t.arity(); i < n; i++) { + collectModalities(t.sub(i), out); + } + } + + private static boolean containsModality(JTerm t) { + if (t.op() instanceof Modality) { + return true; + } + for (int i = 0, n = t.arity(); i < n; i++) { + if (containsModality(t.sub(i))) { + return true; + } + } + return false; + } +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java new file mode 100644 index 00000000000..f6004f692b6 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java @@ -0,0 +1,239 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.FindTaclet; +import de.uka.ilkd.key.rule.MatchConditions; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.match.vm.instructions.MatchContextStatementBlockInstruction; +import de.uka.ilkd.key.rule.match.vm.instructions.MatchSubProgramInstruction; +import de.uka.ilkd.key.util.HelperClassForTests; +import de.uka.ilkd.key.util.ProofStarter; + +import org.key_project.logic.op.Modality; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; +import org.key_project.prover.sequent.SequentFormula; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * Differential test / oracle for the matcher work. For every find-taclet of the full Java taclet + * base it builds, in the same JVM, the interpreter oracle + * ({@code key.matcher.programInstructions=false}, modality programs matched by the monolithic + * {@code MatchProgramInstruction} delegating to {@code ProgramElement.match}) and, where + * applicable, + * the converted interpreter ({@code =true}: generic programs via + * {@link MatchSubProgramInstruction}, context blocks via + * {@link MatchContextStatementBlockInstruction}) and the cursor-free compiled matcher + * ({@link CompiledMatchProgram}, incl. modality / context-block / bound-variable patterns). All are + * run over a corpus of terms harvested from a real proof and asserted to produce identical results + * (match success/failure and the resulting instantiations, including the context-block + * prefix/suffix + * instantiation). + * + *

+ * This guards the converted and compiled matchers against the interpreter at the unit-test level. + * The complementary end-to-end check is identical proof statistics (nodes / branches / rule + * applications) for a full {@code --auto} proof with the flag on vs off (the CLI + * {@code .auto.proof} + * stores only the problem, not the proof tree, so a file diff is not a valid replay check). + */ +public class ProgramMatchDifferentialTest { + + private static final MatchResultInfo EMPTY = MatchConditions.EMPTY_MATCHCONDITIONS; + + /** the symbolic-execution proof whose terms form the matching corpus. */ + private static final Path CORPUS_PROOF = + HelperClassForTests.TESTCASE_DIRECTORY.resolve("proofStarter/CC/project.key"); + + /** cap on symbolic-execution steps run to harvest the corpus (keeps the test fast). */ + private static final int CORPUS_STEPS = 6000; + + @Test + public void convertedMatchesInterpreter() throws Exception { + final KeYEnvironment env = + KeYEnvironment.load(CORPUS_PROOF, null, null, null); + try { + final Proof proof = env.getLoadedProof(); + final Services services = proof.getServices(); + + // run a bounded amount of symbolic execution to populate the proof tree with terms at + // many execution stages (method frames, peeled blocks, loops, ...) + final ProofStarter ps = new ProofStarter(false); + ps.init(proof); + ps.setMaxRuleApplications(CORPUS_STEPS); + ps.start(); + + final List corpus = harvestCorpus(proof); + + int findTaclets = 0; + int programTaclets = 0; + int convertedContext = 0; + int convertedGeneric = 0; + int compiledTaclets = 0; + int compiledBoundVar = 0; + long matches = 0; + int comparisons = 0; + for (Taclet t : proof.getInitConfig().activatedTaclets()) { + if (!(t instanceof FindTaclet ft) || !(ft.find() instanceof JTerm find)) { + continue; + } + findTaclets++; + final boolean program = containsModality(find); + final VMProgramInterpreter oracle = new VMProgramInterpreter( + SyntaxElementMatchProgramGenerator.createProgram(find, false)); + // the cursor-free compiled matcher; null if not (yet) compilable + final CompiledMatchProgram compiled = CompiledMatchProgram.compile(find); + if (compiled != null) { + compiledTaclets++; + if (containsBoundVars(find)) { + compiledBoundVar++; + } + } + // the converted interpreter (programInstructions=true) only differs for programs + VMProgramInterpreter converted = null; + if (program) { + programTaclets++; + final VMInstruction[] convertedProg = + SyntaxElementMatchProgramGenerator.createProgram(find, true); + if (contains(convertedProg, MatchContextStatementBlockInstruction.class)) { + convertedContext++; + } + if (contains(convertedProg, MatchSubProgramInstruction.class)) { + convertedGeneric++; + } + converted = new VMProgramInterpreter(convertedProg); + } + + for (JTerm term : corpus) { + final MatchResultInfo oracleRes = oracle.match(term, EMPTY, services); + comparisons++; + if (converted != null) { + assertSameResult(t, term, oracleRes, + converted.match(term, EMPTY, services)); + } + if (compiled != null) { + assertSameResult(t, term, oracleRes, compiled.match(term, EMPTY, services)); + } + if (oracleRes != null) { + matches++; + } + } + } + + System.out.printf( + "[program-match differential] findTaclets=%d programTaclets=%d convertedContext=%d " + + "convertedGeneric=%d compiled=%d (boundVar=%d) corpus=%d comparisons=%d " + + "matches=%d%n", + findTaclets, programTaclets, convertedContext, convertedGeneric, compiledTaclets, + compiledBoundVar, corpus.size(), comparisons, matches); + // sanity floor: the run must actually exercise the step-2 context-block conversion + assertEquals(true, convertedContext > 0, + "expected at least some taclets to use the converted context-block matcher"); + // sanity floor: the run must actually exercise the compiled program matcher (step 3) + assertEquals(true, compiledTaclets > 0, + "expected at least some program taclets to compile"); + // sanity floor: the run must actually exercise compiled bound-variable matching + assertEquals(true, compiledBoundVar > 0, + "expected at least some bound-variable taclets to compile"); + } finally { + env.dispose(); + } + } + + /** asserts that oracle and converted matcher agree (success/failure and instantiations). */ + private static void assertSameResult(Taclet t, JTerm term, MatchResultInfo oracle, + MatchResultInfo converted) { + final boolean oracleOk = oracle != null; + final boolean convertedOk = converted != null; + assertEquals(oracleOk, convertedOk, + () -> "match success differs for taclet " + t.name() + " on " + term); + if (oracleOk) { + final var oracleInst = ((MatchConditions) oracle).getInstantiations(); + final var convertedInst = ((MatchConditions) converted).getInstantiations(); + assertEquals(oracleInst, convertedInst, + () -> "instantiations differ for taclet " + t.name() + " on " + term + + "\n oracle: " + oracleInst + + "\n converted: " + convertedInst); + // the context instantiation (prefix/suffix positions) is the critical step-2 output + assertEquals( + String.valueOf(oracleInst.getContextInstantiation()), + String.valueOf(convertedInst.getContextInstantiation()), + () -> "context instantiation differs for taclet " + t.name() + " on " + term); + } + } + + /** collects a deduplicated corpus of subterms from every node of the proof tree. */ + private static List harvestCorpus(Proof proof) { + final Set seen = new LinkedHashSet<>(); + final Iterator nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node n = nodes.next(); + for (SequentFormula sf : n.sequent()) { + collectSubterms((JTerm) sf.formula(), seen); + } + } + return new ArrayList<>(seen); + } + + private static void collectSubterms(JTerm t, Set out) { + out.add(t); + for (int i = 0, n = t.arity(); i < n; i++) { + collectSubterms(t.sub(i), out); + } + } + + /** whether the term tree binds any variable (quantifier, substitution, ...). */ + private static boolean containsBoundVars(JTerm t) { + if (!t.boundVars().isEmpty()) { + return true; + } + for (int i = 0, n = t.arity(); i < n; i++) { + if (containsBoundVars(t.sub(i))) { + return true; + } + } + return false; + } + + /** whether the term tree contains a modality (i.e. carries a Java program). */ + private static boolean containsModality(JTerm t) { + if (t.op() instanceof Modality) { + return true; + } + for (int i = 0, n = t.arity(); i < n; i++) { + if (containsModality(t.sub(i))) { + return true; + } + } + return false; + } + + /** whether the generated (top-level) program contains an instruction of the given kind. */ + private static boolean contains(VMInstruction[] program, Class kind) { + for (VMInstruction instr : program) { + if (kind.isInstance(instr)) { + return true; + } + } + return false; + } +} From a9c9339756de57fea5362d39b7c29bc9fadee346 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 01:27:57 +0200 Subject: [PATCH 06/26] matcher: introduce key.ncore.compiler match-plan framework A new language-agnostic module holding the match-plan IR from which both find-matcher back-ends (the VMProgramInterpreter and the cursor-free compiled matcher) are derived from a single description, so a construct described once drives both. - MatchPlan: the IR node (emitInstructions for the interpreter, compile for the compiled matcher); OperatorPlan / SchemaVarPlan cover the term skeleton. - MatchHead: the operator-specific check (no subterm recursion); GenericOperatorHead handles ordinary operators. - BinderMatcher / ProgramMatchHook: the two cross-language SPIs (bound variables and the modality program AST), kept abstract here so other ncore-based provers (Rust, Solidity) can reuse the framework. The module depends only on key.ncore / key.ncore.calculus / key.util (no Java-DL types); key.core gains a dependency on it. --- key.core/build.gradle | 1 + key.ncore.compiler/build.gradle | 25 ++++ .../rules/matcher/compiler/BinderMatcher.java | 51 ++++++++ .../matcher/compiler/GenericOperatorHead.java | 40 +++++++ .../rules/matcher/compiler/MatchHead.java | 43 +++++++ .../rules/matcher/compiler/MatchPlan.java | 56 +++++++++ .../rules/matcher/compiler/OperatorPlan.java | 110 ++++++++++++++++++ .../matcher/compiler/ProgramMatchHook.java | 52 +++++++++ .../rules/matcher/compiler/SchemaVarPlan.java | 75 ++++++++++++ .../rules/matcher/compiler/package-info.java | 25 ++++ settings.gradle | 1 + 11 files changed, 479 insertions(+) create mode 100644 key.ncore.compiler/build.gradle create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/BinderMatcher.java create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/GenericOperatorHead.java create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchHead.java create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchPlan.java create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/OperatorPlan.java create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/ProgramMatchHook.java create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/SchemaVarPlan.java create mode 100644 key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/package-info.java diff --git a/key.core/build.gradle b/key.core/build.gradle index 73146343a6f..cdc1edadef3 100644 --- a/key.core/build.gradle +++ b/key.core/build.gradle @@ -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" diff --git a/key.ncore.compiler/build.gradle b/key.ncore.compiler/build.gradle new file mode 100644 index 00000000000..a1bf3d11a48 --- /dev/null +++ b/key.ncore.compiler/build.gradle @@ -0,0 +1,25 @@ +repositories { + mavenCentral() +} + +dependencies { + api project(':key.util') + api project(':key.ncore') + api project(':key.ncore.calculus') + implementation('org.jspecify:jspecify:1.0.0') +} + +checkerFramework { + if (System.getProperty("ENABLE_NULLNESS")) { + checkers = [ + "org.checkerframework.checker.nullness.NullnessChecker", + ] + extraJavacArgs = [ + "-Xmaxerrs", "10000", + "-Astubs=$rootDir/key.util/src/main/checkerframework:permit-nullness-assertion-exception.astub", + "-AstubNoWarnIfNotFound", + "-Werror", + "-Aversion", + ] + } +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/BinderMatcher.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/BinderMatcher.java new file mode 100644 index 00000000000..08341f22c53 --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/BinderMatcher.java @@ -0,0 +1,51 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.compiler; + +import org.key_project.logic.op.QuantifiableVariable; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; +import org.key_project.util.collection.ImmutableArray; + +/** + * Language SPI for matching bound variables (the variables introduced by a binder such as + * a quantifier, a substitution or a {@code let}). This is one of the two axes on which the + * Java/Rust/Solidity front-ends differ: each binds its own kind of logic / schema variables (e.g. + * {@code LogicVariable} vs. {@code BoundVariable}) and keeps its own renaming/instantiation state. + * + *

+ * The match-plan framework owns the scaffolding (bind the pattern's bound variables before + * matching the operator and subterms, then unbind afterwards, in both back-ends); a language plugs + * in the actual binding behaviour here. The {@linkplain #binder(ImmutableArray) binder} matches the + * pattern's bound variables against the source element's own bound variables and is shared by both + * back-ends (it is element-based); only the un-binding is back-end specific (an instruction for the + * interpreter, a direct call for the compiler). + */ +public interface BinderMatcher { + + /** + * The element-based instruction that binds the given pattern bound variables (it reads the + * source element's own bound variables). Used by both back-ends. + * + * @param boundVars the pattern's bound variables + * @return the binding instruction + */ + MatchInstruction binder(ImmutableArray boundVars); + + /** + * The interpreter instruction that pops the binding scope opened by {@link #binder}. + * + * @return the un-binding instruction + */ + VMInstruction unbinderInstruction(); + + /** + * Pops the binding scope opened by {@link #binder} for the compiled back-end. + * + * @param mc the match result after matching the binder body + * @return the match result with the binding scope removed + */ + MatchResultInfo unbind(MatchResultInfo mc); +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/GenericOperatorHead.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/GenericOperatorHead.java new file mode 100644 index 00000000000..a074927fb89 --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/GenericOperatorHead.java @@ -0,0 +1,40 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.compiler; + +import java.util.List; + +import org.key_project.logic.Term; +import org.key_project.logic.op.Operator; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.GotoNextInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.MatchIdentityInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +/** + * The head for an ordinary operator: the operator must be (reference-)identical to the pattern's. + * This is the language-agnostic default for any operator that has no special matching (i.e. is + * matched by {@code MatchIdentityInstruction} in the interpreter and by an {@code op() == op} check + * in the compiler). + */ +public final class GenericOperatorHead implements MatchHead { + + private final Operator op; + + public GenericOperatorHead(Operator op) { + this.op = op; + } + + @Override + public void emit(List out) { + out.add(new MatchIdentityInstruction(op)); + out.add(GotoNextInstruction.INSTANCE); + } + + @Override + public MatchProgram compileHeadCheck() { + final Operator expected = op; + return (element, mc, services) -> ((Term) element).op() == expected ? mc : null; + } +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchHead.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchHead.java new file mode 100644 index 00000000000..8423dfc4cea --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchHead.java @@ -0,0 +1,43 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.compiler; + +import java.util.List; + +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +/** + * The operator-specific "head" of an {@link OperatorPlan}: it checks the operator of a term and any + * operator-specific data (e.g. a modal-operator kind, a parametric function's generic arguments, + * an elementary update's left-hand side), but not the subterms -- those are recursed by + * the + * enclosing {@link OperatorPlan}. + * + *

+ * Generic heads (ordinary operators) live in this module; language-specific heads (modalities, + * parametric functions, ...) are supplied by the front-end. A head carries both back-ends, lifted + * from the corresponding hand-written matcher fragments. + */ +public interface MatchHead { + + /** + * Appends the interpreter instructions matching this head. On entry the cursor points at the + * operator; on completion it must point at the first subterm so the enclosing + * {@link OperatorPlan} can match the subterms. + * + * @param out the instruction list being built + */ + void emit(List out); + + /** + * Builds the compiled head check: applied to the term element, it verifies the operator (and + * head-specific data) and returns the extended match result, or {@code null} on failure. It + * does + * not recurse into subterms. + * + * @return the compiled head matcher + */ + MatchProgram compileHeadCheck(); +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchPlan.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchPlan.java new file mode 100644 index 00000000000..6daac292c75 --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/MatchPlan.java @@ -0,0 +1,56 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.compiler; + +import java.util.List; + +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +/** + * A node of a match plan: a single, language-agnostic description of how to match one + * (sub)pattern, from which both back-ends are derived. + * + *

+ * A match plan is built once per find pattern (when the taclet base is loaded) by a per-language + * dispatch that composes plan nodes for each syntax construct. The point is that each construct is + * described in exactly one place: a node carries both + *

+ * Adding a construct (or fixing its matching) is therefore done once, in the node, and both the + * interpreter and the compiler stay in sync by construction. + * + *

+ * Both emissions are produced at plan-construction time, so neither adds runtime overhead over the + * hand-written matchers they replace: the interpreter still runs a {@code VMInstruction[]} and the + * compiler still runs the resulting {@link MatchProgram}. + */ +public interface MatchPlan { + + /** + * Appends, to {@code out}, the {@link VMInstruction}s matching this (sub)pattern for the + * cursor-based interpreter. The cursor is expected to point at the element to be matched and, + * on + * completion of the appended instructions, to have advanced past it (to its next sibling), so + * that sibling plans can be appended directly after. + * + * @param out the instruction list being built + */ + void emitInstructions(List out); + + /** + * Builds the cursor-free compiled matcher for this (sub)pattern. The returned + * {@link MatchProgram} is applied to the syntax element to be matched (the same element the + * interpreter's cursor would point at) and returns the extended match result, or {@code null} + * on + * failure. + * + * @return the compiled matcher for this plan node + */ + MatchProgram compile(); +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/OperatorPlan.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/OperatorPlan.java new file mode 100644 index 00000000000..b2236a78420 --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/OperatorPlan.java @@ -0,0 +1,110 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.compiler; + +import java.util.List; + +import org.key_project.logic.Term; +import org.key_project.logic.op.QuantifiableVariable; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.CheckNodeKindInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.GotoNextInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.GotoNextSiblingInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; +import org.key_project.util.collection.ImmutableArray; + +import org.jspecify.annotations.Nullable; + +/** + * Plan for a term whose top operator is matched by a {@link MatchHead} (the operator + any + * operator-specific data) and whose subterms are matched by child plans. Bound variables, if any, + * are bound around the whole node via the {@link BinderMatcher}. + * + *

+ * This is the language-agnostic counterpart of the non-schema-variable branch of the hand-written + * matchers: the interpreter emission reproduces {@code checkNodeKind(Term) + gotoNext + head + + * (skip bound variables) + subterms} (wrapped in bind/unbind), and the compiled emission checks the + * head then recurses the subterms (wrapped in bind/unbind). + */ +public final class OperatorPlan implements MatchPlan { + + private final MatchHead head; + private final List children; + private final ImmutableArray boundVars; + private final BinderMatcher binder; + + /** + * @param head the operator head (operator + operator-specific checks) + * @param children one plan per subterm, in order + * @param boundVars the term's bound variables (possibly empty) + * @param binder the binder SPI (used only if {@code boundVars} is non-empty) + */ + public OperatorPlan(MatchHead head, List children, + ImmutableArray boundVars, BinderMatcher binder) { + this.head = head; + this.children = children; + this.boundVars = boundVars; + this.binder = binder; + } + + @Override + public void emitInstructions(List out) { + final boolean bound = !boundVars.isEmpty(); + if (bound) { + out.add(binder.binder(boundVars)); + } + out.add(new CheckNodeKindInstruction(Term.class)); + out.add(GotoNextInstruction.INSTANCE); + head.emit(out); + if (bound) { + for (int i = 0, n = boundVars.size(); i < n; i++) { + out.add(GotoNextSiblingInstruction.INSTANCE); + } + } + for (MatchPlan child : children) { + child.emitInstructions(out); + } + if (bound) { + out.add(binder.unbinderInstruction()); + } + } + + @Override + public MatchProgram compile() { + final MatchProgram headCheck = head.compileHeadCheck(); + final int n = children.size(); + final MatchProgram[] childMatchers = new MatchProgram[n]; + for (int i = 0; i < n; i++) { + childMatchers[i] = children.get(i).compile(); + } + final MatchProgram core = (element, mc, services) -> { + MatchResultInfo r = headCheck.match(element, mc, services); + if (r == null) { + return null; + } + final Term term = (Term) element; + for (int i = 0; i < n; i++) { + r = childMatchers[i].match(term.sub(i), r, services); + if (r == null) { + return null; + } + } + return r; + }; + if (boundVars.isEmpty()) { + return core; + } + final MatchInstruction bind = binder.binder(boundVars); + return (element, mc, services) -> { + final @Nullable MatchResultInfo bound = bind.match(element, mc, services); + if (bound == null) { + return null; + } + final @Nullable MatchResultInfo body = core.match(element, bound, services); + return body == null ? null : binder.unbind(body); + }; + } +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/ProgramMatchHook.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/ProgramMatchHook.java new file mode 100644 index 00000000000..cc47ce845f4 --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/ProgramMatchHook.java @@ -0,0 +1,52 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.compiler; + +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +/** + * Language SPI for matching the program carried by a modality (the {@code \<{ ... }\>} of + * a + * symbolic-execution taclet). This is the second of the two axes on which the Java/Rust/Solidity + * front-ends differ (the first is {@link BinderMatcher}): each has its own program AST -- Java's + * {@code JavaBlock}/{@code ContextStatementBlock}, Rust's {@code RustyBlock}, Solidity's block -- + * but all are {@link org.key_project.logic.SyntaxElement}s navigated through {@code getChild}. + * + *

+ * A hook is built per modality pattern (it captures that pattern's program) and exposes the program + * matcher for both back-ends. On the interpreter side it is a single {@link VMInstruction} run with + * the cursor positioned at the program block; on the compiled side it is a {@link MatchProgram} + * applied directly to the source program block. Both consume exactly one element (the block), so + * the + * enclosing modality head can advance to the post-condition subterm afterwards. + * + *

+ * The framework owns the surrounding modality skeleton (check the modality, match its kind, then + * the + * program, then recurse the subterms); a language plugs in only the divergent program matching + * here. + * Java additionally has the rich {@code ContextStatementBlock} prefix/suffix machinery, which is + * entirely encapsulated behind this hook; Rust and Solidity supply their own simpler block + * matchers. + */ +public interface ProgramMatchHook { + + /** + * The interpreter instruction matching the modality's program. On entry the cursor points at + * the + * source program block; it consumes that block. + * + * @return the program-matching instruction + */ + VMInstruction programInstruction(); + + /** + * The compiled matcher for the modality's program, applied directly to the source program + * block. + * + * @return the compiled program matcher + */ + MatchProgram compileProgram(); +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/SchemaVarPlan.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/SchemaVarPlan.java new file mode 100644 index 00000000000..ce61c1c12dc --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/SchemaVarPlan.java @@ -0,0 +1,75 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.rules.matcher.compiler; + +import java.util.List; + +import org.key_project.logic.op.QuantifiableVariable; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.GotoNextSiblingInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; +import org.key_project.util.collection.ImmutableArray; + +import org.jspecify.annotations.Nullable; + +/** + * Plan for a (sub)pattern that is a schema variable: it matches the whole element via the provided + * schema-variable {@link MatchInstruction} (which the front-end supplies, since schema-variable + * kinds are language-specific). Bound variables, if any, are bound around it via the + * {@link BinderMatcher}. + * + *

+ * Language-agnostic counterpart of the schema-variable branch of the hand-written matchers: the + * interpreter emission is {@code matchSV + gotoNextSibling} (wrapped in bind/unbind), the compiled + * emission applies the schema-variable instruction directly (wrapped in bind/unbind). + */ +public final class SchemaVarPlan implements MatchPlan { + + private final MatchInstruction schemaVarInstruction; + private final ImmutableArray boundVars; + private final BinderMatcher binder; + + public SchemaVarPlan(MatchInstruction schemaVarInstruction, + ImmutableArray boundVars, BinderMatcher binder) { + this.schemaVarInstruction = schemaVarInstruction; + this.boundVars = boundVars; + this.binder = binder; + } + + @Override + public void emitInstructions(List out) { + final boolean bound = !boundVars.isEmpty(); + if (bound) { + out.add(binder.binder(boundVars)); + } + out.add(schemaVarInstruction); + out.add(GotoNextSiblingInstruction.INSTANCE); + if (bound) { + for (int i = 0, n = boundVars.size(); i < n; i++) { + out.add(GotoNextSiblingInstruction.INSTANCE); + } + out.add(binder.unbinderInstruction()); + } + } + + @Override + public MatchProgram compile() { + final MatchInstruction sv = schemaVarInstruction; + final MatchProgram core = sv::match; + if (boundVars.isEmpty()) { + return core; + } + final MatchInstruction bind = binder.binder(boundVars); + return (element, mc, services) -> { + final @Nullable MatchResultInfo bound = bind.match(element, mc, services); + if (bound == null) { + return null; + } + final @Nullable MatchResultInfo body = core.match(element, bound, services); + return body == null ? null : binder.unbind(body); + }; + } +} diff --git a/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/package-info.java b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/package-info.java new file mode 100644 index 00000000000..7c0238be3aa --- /dev/null +++ b/key.ncore.compiler/src/main/java/org/key_project/prover/rules/matcher/compiler/package-info.java @@ -0,0 +1,25 @@ +/* + * This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only + */ + +/** + * A language-agnostic framework for taclet find-matching that produces, from a single description + * of + * a pattern, both an interpreted matcher (a sequence of + * {@link org.key_project.prover.rules.matcher.vm.instruction.VMInstruction}s run by + * {@link org.key_project.prover.rules.matcher.vm.VMProgramInterpreter}) and a cursor-free compiled + * matcher. + * + *

+ * It works purely over {@link org.key_project.logic.SyntaxElement} / + * {@link org.key_project.logic.Term} + * and the calculus matcher abstractions ({@code MatchResultInfo}, {@code VMInstruction}, + * {@code MatchProgram}); it depends only on {@code key.ncore}, {@code key.ncore.calculus} and + * {@code key.util}, never on the Java-DL specific {@code key.core}. Language-specific behaviour + * (the concrete syntax constructs, the program/AST sub-matching and the binding of bound variables) + * is supplied through small SPIs so that the Java, Rust and Solidity front-ends can share this core + * and only add their own constructs. + */ +package org.key_project.prover.rules.matcher.compiler; diff --git a/settings.gradle b/settings.gradle index 00226f6fa82..e217520e491 100644 --- a/settings.gradle +++ b/settings.gradle @@ -7,6 +7,7 @@ plugins { include "key.util" include "key.ncore" include 'key.ncore.calculus' +include 'key.ncore.compiler' include "key.core" //include "key.core.rifl" From 094039721e666c24ae7c465eea7dc963cb99ec6c Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 01:28:14 +0200 Subject: [PATCH 07/26] matcher: build Java-DL find-matchers on the match-plan framework The single Java-DL dispatch (JavaMatchPlanBuilder.buildPlan) builds a MatchPlan for a find pattern; both back-ends are then derived from it. It covers the FOL term skeleton, elementary updates, parametric function instances and modalities, falling back to the legacy matchers only for term labels. - Heads ElementaryUpdateHead / ParametricFunctionHead / ModalityHead carry the operator-specific interpreter + compiled fragments, lifted verbatim from the hand-written generator and compiled matcher (so behaviour is preserved). - JavaBinderMatcher / JavaProgramMatchHook implement the two SPIs (bound-variable binding/renaming; the JavaBlock / ContextStatementBlock program matching). - CompiledMatchProgram.compiledProgramMatcher is extracted from compileModality (now keyed on the JavaBlock) so the compiled matcher and the program hook share one program-matching implementation; buildProgramInstruction is made package-visible for the hook's interpreter side. - JavaMatchPlanBuilder also exposes the production facades interpreterProgram / compiledProgram (framework-built, with legacy fallback for term labels). --- .../rule/match/vm/CompiledMatchProgram.java | 91 +++++++---- .../rule/match/vm/ElementaryUpdateHead.java | 73 +++++++++ .../key/rule/match/vm/JavaBinderMatcher.java | 46 ++++++ .../rule/match/vm/JavaMatchPlanBuilder.java | 152 ++++++++++++++++++ .../rule/match/vm/JavaProgramMatchHook.java | 67 ++++++++ .../ilkd/key/rule/match/vm/ModalityHead.java | 92 +++++++++++ .../rule/match/vm/ParametricFunctionHead.java | 94 +++++++++++ .../SyntaxElementMatchProgramGenerator.java | 2 +- 8 files changed, 581 insertions(+), 36 deletions(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ElementaryUpdateHead.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaBinderMatcher.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ModalityHead.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ParametricFunctionHead.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java index e87fe55b9bd..6b00f89e9bd 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java @@ -12,6 +12,7 @@ import de.uka.ilkd.key.java.ast.SourceData; import de.uka.ilkd.key.logic.GenericArgument; import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.JavaBlock; import de.uka.ilkd.key.logic.op.ElementaryUpdate; import de.uka.ilkd.key.logic.op.LocationVariable; import de.uka.ilkd.key.logic.op.ModalOperatorSV; @@ -307,40 +308,9 @@ private interface ProgStep { : getMatchIdentityInstruction(mod.kind()); final JavaProgramElement prog = pattern.javaBlock().program(); - final Step progMatch; - if (prog instanceof ContextStatementBlock csb) { - final ProgStep[] active = compileActiveStatements(csb); - if (active != null) { - // phase (3) of the context match, cursor-free: each active statement consumes one - // child - final ProgramChildrenMatcher phase3 = (parent, startChild, mc, services) -> { - MatchResultInfo r = mc; - for (int k = 0; k < active.length; k++) { - r = active[k].match(parent.getChild(startChild + k), r, services); - if (r == null) { - return null; - } - } - return r; - }; - // phases (1)(2)(4) stay in ContextStatementBlock.match; only phase (3) is compiled - progMatch = (term, mc, services) -> csb.match( - new SourceData(term.javaBlock().program(), -1, (Services) services), - (MatchConditions) mc, phase3); - } else { - // an active statement is variable-arity (a list SV) or otherwise uncompilable: - // delegate the whole context match to the interpreter (its matchChildren handles - // list SVs); the surrounding term skeleton stays compiled - progMatch = (term, mc, services) -> csb.match( - new SourceData(term.javaBlock().program(), -1, (Services) services), - (MatchConditions) mc); - } - } else { - final ProgStep ps = compileProgram(prog); - if (ps == null) { - return null; - } - progMatch = (term, mc, services) -> ps.match(term.javaBlock().program(), mc, services); + final MatchProgram progMatch = compiledProgramMatcher(prog); + if (progMatch == null) { + return null; } final int arity = pattern.arity(); @@ -361,7 +331,7 @@ private interface ProgStep { if (r == null) { return null; } - r = progMatch.match(term, r, services); + r = progMatch.match(term.javaBlock(), r, services); if (r == null) { return null; } @@ -375,6 +345,57 @@ private interface ProgStep { }; } + /** + * Compiles the cursor-free matcher for the Java program {@code prog} of a modality, applied + * directly to the source {@link JavaBlock} (it extracts the block's program element). A + * top-level + * {@link ContextStatementBlock} keeps phases (1)(2)(4) of the context match in + * {@code ContextStatementBlock.match} and compiles only phase (3) (each active statement + * consumes + * one source child), unless an active statement is variable-arity (a list SV) or otherwise + * uncompilable -- then the whole context match is delegated to + * {@code ContextStatementBlock.match} + * (its {@code matchChildren} handles list SVs) while the surrounding term skeleton stays + * compiled. + * Any other program is compiled by {@link #compileProgram}. Returns {@code null} only if that + * generic compilation cannot handle the program. Shared by {@link #compileModality} and the + * Java {@code ProgramMatchHook} so both reuse one program-matching implementation. + */ + static @Nullable MatchProgram compiledProgramMatcher(JavaProgramElement prog) { + if (prog instanceof ContextStatementBlock csb) { + final ProgStep[] active = compileActiveStatements(csb); + if (active != null) { + // phase (3) of the context match, cursor-free: each active statement consumes one + // child + final ProgramChildrenMatcher phase3 = (parent, startChild, mc, services) -> { + MatchResultInfo r = mc; + for (int k = 0; k < active.length; k++) { + r = active[k].match(parent.getChild(startChild + k), r, services); + if (r == null) { + return null; + } + } + return r; + }; + // phases (1)(2)(4) stay in ContextStatementBlock.match; only phase (3) is compiled + return (block, mc, services) -> csb.match( + new SourceData(((JavaBlock) block).program(), -1, (Services) services), + (MatchConditions) mc, phase3); + } + // an active statement is variable-arity (a list SV) or otherwise uncompilable: + // delegate the whole context match to the interpreter (its matchChildren handles + // list SVs); the surrounding term skeleton stays compiled + return (block, mc, services) -> csb.match( + new SourceData(((JavaBlock) block).program(), -1, (Services) services), + (MatchConditions) mc); + } + final ProgStep ps = compileProgram(prog); + if (ps == null) { + return null; + } + return (block, mc, services) -> ps.match(((JavaBlock) block).program(), mc, services); + } + /** * Compiles a Java program (sub)element: a generic-match element with a fixed, one-source-child * structure is matched by direct {@code getChild} navigation (class equality + exact-size child diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ElementaryUpdateHead.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ElementaryUpdateHead.java new file mode 100644 index 00000000000..cbf6531f410 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ElementaryUpdateHead.java @@ -0,0 +1,73 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.util.List; + +import de.uka.ilkd.key.logic.op.ElementaryUpdate; +import de.uka.ilkd.key.logic.op.LocationVariable; + +import org.key_project.logic.Term; +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.rules.matcher.compiler.MatchHead; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +import org.jspecify.annotations.Nullable; + +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getCheckNodeKindInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchIdentityInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchInstructionForSV; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.gotoNextInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.gotoNextSiblingInstruction; + +/** + * Match head for an {@link ElementaryUpdate} {@code lhs := value}: it matches the operator and the + * left-hand side; the value subterm is matched by the enclosing + * {@link org.key_project.prover.rules.matcher.compiler.OperatorPlan}. Mirrors the elementary-update + * fragments of the hand-written interpreter generator and compiled matcher. + */ +public final class ElementaryUpdateHead implements MatchHead { + + private final MatchInstruction lhsMatcher; + /** whether the left-hand side is a schema variable (it advances by sibling, not by descent). */ + private final boolean lhsIsSchemaVariable; + + private ElementaryUpdateHead(MatchInstruction lhsMatcher, boolean lhsIsSchemaVariable) { + this.lhsMatcher = lhsMatcher; + this.lhsIsSchemaVariable = lhsIsSchemaVariable; + } + + /** + * @param elUp the elementary update pattern + * @return a head for {@code elUp}, or {@code null} if its left-hand side is neither a schema + * variable nor a concrete location variable (then the caller falls back) + */ + public static @Nullable ElementaryUpdateHead of(ElementaryUpdate elUp) { + if (elUp.lhs() instanceof SchemaVariable sv) { + return new ElementaryUpdateHead(getMatchInstructionForSV(sv), true); + } else if (elUp.lhs() instanceof LocationVariable locVar) { + return new ElementaryUpdateHead(getMatchIdentityInstruction(locVar), false); + } + return null; + } + + @Override + public void emit(List out) { + out.add(getCheckNodeKindInstruction(ElementaryUpdate.class)); + out.add(gotoNextInstruction()); + out.add(lhsMatcher); + out.add(lhsIsSchemaVariable ? gotoNextSiblingInstruction() : gotoNextInstruction()); + } + + @Override + public MatchProgram compileHeadCheck() { + final MatchInstruction lhs = lhsMatcher; + return (element, mc, + services) -> ((Term) element).op() instanceof ElementaryUpdate actualElUp + ? lhs.match(actualElUp.lhs(), mc, services) + : null; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaBinderMatcher.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaBinderMatcher.java new file mode 100644 index 00000000000..15d74b455c4 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaBinderMatcher.java @@ -0,0 +1,46 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import de.uka.ilkd.key.rule.MatchConditions; +import de.uka.ilkd.key.rule.match.vm.instructions.UnbindVariablesInstruction; + +import org.key_project.logic.op.QuantifiableVariable; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.compiler.BinderMatcher; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; +import org.key_project.util.collection.ImmutableArray; + +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchAndBindVariables; + +/** + * Java-DL implementation of the {@link BinderMatcher} SPI: bound variables are bound via the + * {@code matchAndBindVariables} instruction and the renaming scope is popped via + * {@link UnbindVariablesInstruction} (interpreter) / {@link MatchConditions#shrinkRenameTable()} + * (compiled). + */ +public final class JavaBinderMatcher implements BinderMatcher { + + /** stateless; a single shared instance suffices. */ + public static final JavaBinderMatcher INSTANCE = new JavaBinderMatcher(); + + private JavaBinderMatcher() {} + + @SuppressWarnings("unchecked") + @Override + public MatchInstruction binder(ImmutableArray boundVars) { + return matchAndBindVariables((ImmutableArray) boundVars); + } + + @Override + public VMInstruction unbinderInstruction() { + return new UnbindVariablesInstruction(); + } + + @Override + public MatchResultInfo unbind(MatchResultInfo mc) { + return ((MatchConditions) mc).shrinkRenameTable(); + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java new file mode 100644 index 00000000000..135537a52d8 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java @@ -0,0 +1,152 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.util.ArrayList; +import java.util.List; + +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.ElementaryUpdate; +import de.uka.ilkd.key.logic.op.ParametricFunctionInstance; + +import org.key_project.logic.op.Modality; +import org.key_project.logic.op.Operator; +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.rules.matcher.compiler.GenericOperatorHead; +import org.key_project.prover.rules.matcher.compiler.MatchHead; +import org.key_project.prover.rules.matcher.compiler.MatchPlan; +import org.key_project.prover.rules.matcher.compiler.OperatorPlan; +import org.key_project.prover.rules.matcher.compiler.SchemaVarPlan; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +import org.jspecify.annotations.Nullable; + +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchInstructionForSV; + +/** + * The single Java-DL dispatch that builds a {@link MatchPlan} for a find pattern, from which both + * the interpreter and the compiled find-matcher are derived. Describing a construct here gives both + * back-ends at once (the goal of the match-plan framework). + * + *

+ * It covers the FOL term skeleton (schema variables, ordinary operators with their subterms, bound + * variables), elementary updates, parametric function instances and modalities (the Java program is + * matched through a {@link org.key_project.prover.rules.matcher.compiler.ProgramMatchHook}). It + * returns {@code null} only for constructs outside this set (currently term labels) or when a + * modality's program cannot be matched by the framework, so callers fall back to the legacy + * hand-written matchers for those. + */ +public final class JavaMatchPlanBuilder { + + private JavaMatchPlanBuilder() {} + + /** + * Builds the interpreter program for {@code pattern} through the match-plan framework, reading + * the {@code key.matcher.programInstructions} flag (as the legacy generator does). Falls back + * to + * the legacy generator for constructs the framework does not build (term labels). + * + * @param pattern the find / assumes pattern + * @return the VM instruction program + */ + public static VMInstruction[] interpreterProgram(JTerm pattern) { + return interpreterProgram(pattern, + Boolean.getBoolean(SyntaxElementMatchProgramGenerator.PROGRAM_INSTRUCTIONS_PROPERTY)); + } + + /** + * Builds the interpreter program for {@code pattern} through the match-plan framework, falling + * back to the legacy generator for constructs the framework does not build (term labels). + * + * @param pattern the find / assumes pattern + * @param programInstructions whether modality programs are converted to VM instructions + * @return the VM instruction program + */ + public static VMInstruction[] interpreterProgram(JTerm pattern, boolean programInstructions) { + final MatchPlan plan = buildPlan(pattern, programInstructions); + if (plan == null) { + return SyntaxElementMatchProgramGenerator.createProgram(pattern, programInstructions); + } + final List out = new ArrayList<>(); + plan.emitInstructions(out); + return out.toArray(new VMInstruction[0]); + } + + /** + * Builds the cursor-free compiled matcher for {@code pattern} through the match-plan framework, + * falling back to the legacy compiled matcher for constructs the framework does not build. + * + * @param pattern the find pattern + * @return the compiled matcher, or {@code null} if neither the framework nor the legacy + * compiler + * can build it (the caller then uses the interpreter) + */ + public static @Nullable MatchProgram compiledProgram(JTerm pattern) { + final MatchPlan plan = buildPlan(pattern, false); + if (plan != null) { + return plan.compile(); + } + return CompiledMatchProgram.compile(pattern); + } + + /** + * Builds a match plan for {@code pattern}, or returns {@code null} if it uses a construct not + * yet handled by the dispatch (the caller then uses the legacy matcher). + * + * @param pattern the find (sub)pattern + * @param programInstructions whether modality programs are converted to VM instructions on the + * interpreter side (irrelevant for the FOL skeleton and the compiled back-end) + * @return a match plan, or {@code null} to fall back + */ + public static @Nullable MatchPlan buildPlan(JTerm pattern, boolean programInstructions) { + if (pattern.hasLabels()) { + return null; // term labels: not handled by the framework yet + } + final Operator op = pattern.op(); + + if (op instanceof SchemaVariable sv) { + if (pattern.arity() != 0) { + return null; // unusual schema-variable shape + } + return new SchemaVarPlan(getMatchInstructionForSV(sv), pattern.boundVars(), + JavaBinderMatcher.INSTANCE); + } + + final MatchHead head = buildHead(pattern, programInstructions); + if (head == null) { + return null; // unsupported construct or uncompilable program -> fall back + } + + // the operator head plus a plan per subterm + final int arity = pattern.arity(); + final List children = new ArrayList<>(arity); + for (int i = 0; i < arity; i++) { + final MatchPlan child = buildPlan(pattern.sub(i), programInstructions); + if (child == null) { + return null; // a subterm is not handled -> the whole pattern falls back + } + children.add(child); + } + return new OperatorPlan(head, children, pattern.boundVars(), JavaBinderMatcher.INSTANCE); + } + + /** + * The operator-specific head for {@code pattern}'s operator, or {@code null} if the operator is + * not supported (or, for a modality, its program cannot be matched by the framework). + */ + private static @Nullable MatchHead buildHead(JTerm pattern, boolean programInstructions) { + final Operator op = pattern.op(); + if (op instanceof ElementaryUpdate elUp) { + return ElementaryUpdateHead.of(elUp); + } + if (op instanceof ParametricFunctionInstance pfi) { + return ParametricFunctionHead.of(pfi); + } + if (op instanceof Modality mod) { + return ModalityHead.of(mod, pattern.javaBlock().program(), programInstructions); + } + return new GenericOperatorHead(op); + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java new file mode 100644 index 00000000000..68980ac244a --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java @@ -0,0 +1,67 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import de.uka.ilkd.key.java.ast.JavaProgramElement; + +import org.key_project.prover.rules.matcher.compiler.ProgramMatchHook; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +import org.jspecify.annotations.Nullable; + +import static de.uka.ilkd.key.rule.match.vm.SyntaxElementMatchProgramGenerator.buildProgramInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchProgram; + +/** + * Java-DL implementation of the {@link ProgramMatchHook} program-AST axis: it matches the + * {@code JavaBlock} program of a modality. The interpreter side reuses the generator's converted + * program instruction ({@link SyntaxElementMatchProgramGenerator#buildProgramInstruction}, falling + * back to the monolithic {@code MatchProgramInstruction} when conversion is off or unavailable); + * the + * compiled side reuses {@link CompiledMatchProgram#compiledProgramMatcher} (context-block phases + + * generic {@code getChild} navigation + value-leaf / list-SV delegation). Both are lifted verbatim + * from the hand-written matchers, so the framework reproduces them exactly. + */ +public final class JavaProgramMatchHook implements ProgramMatchHook { + + private final JavaProgramElement prog; + private final boolean programInstructions; + private final MatchProgram compiled; + + private JavaProgramMatchHook(JavaProgramElement prog, boolean programInstructions, + MatchProgram compiled) { + this.prog = prog; + this.programInstructions = programInstructions; + this.compiled = compiled; + } + + /** + * @param prog the modality's program pattern + * @param programInstructions whether the interpreter side converts the program to VM + * instructions (otherwise the monolithic {@code MatchProgramInstruction} is used) + * @return a hook for {@code prog}, or {@code null} if the compiled side cannot handle the + * program + * (then the enclosing modality falls back to the legacy matcher) + */ + public static @Nullable JavaProgramMatchHook of(JavaProgramElement prog, + boolean programInstructions) { + final MatchProgram compiled = CompiledMatchProgram.compiledProgramMatcher(prog); + if (compiled == null) { + return null; + } + return new JavaProgramMatchHook(prog, programInstructions, compiled); + } + + @Override + public VMInstruction programInstruction() { + final VMInstruction converted = programInstructions ? buildProgramInstruction(prog) : null; + return converted != null ? converted : matchProgram(prog); + } + + @Override + public MatchProgram compileProgram() { + return compiled; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ModalityHead.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ModalityHead.java new file mode 100644 index 00000000000..d1accbea58b --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ModalityHead.java @@ -0,0 +1,92 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.util.List; + +import de.uka.ilkd.key.java.ast.JavaProgramElement; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.ModalOperatorSV; + +import org.key_project.logic.Term; +import org.key_project.logic.op.Modality; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.compiler.MatchHead; +import org.key_project.prover.rules.matcher.compiler.ProgramMatchHook; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +import org.jspecify.annotations.Nullable; + +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getCheckNodeKindInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchIdentityInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.gotoNextInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.gotoNextSiblingInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchModalOperatorSV; + +/** + * Match head for a {@link Modality} {@code \<{ prog }\> post}: it matches the operator, the modal + * kind (a {@link ModalOperatorSV} or a concrete kind by identity) and the Java program (through a + * {@link ProgramMatchHook}); the post-condition subterm is matched by the enclosing + * {@link org.key_project.prover.rules.matcher.compiler.OperatorPlan}. The program AST is the second + * cross-language axis (see {@link ProgramMatchHook}); the kind and skeleton are lifted from the + * hand-written interpreter generator and compiled matcher. + */ +public final class ModalityHead implements MatchHead { + + private final MatchInstruction kindInstr; + private final ProgramMatchHook programHook; + + private ModalityHead(MatchInstruction kindInstr, ProgramMatchHook programHook) { + this.kindInstr = kindInstr; + this.programHook = programHook; + } + + /** + * @param mod the modality pattern + * @param prog the modality's Java program ({@code pattern.javaBlock().program()}) + * @param programInstructions whether the interpreter side converts the program to VM + * instructions + * @return a head for {@code mod}, or {@code null} if the program cannot be matched by the + * framework (then the caller falls back) + */ + public static @Nullable ModalityHead of(Modality mod, JavaProgramElement prog, + boolean programInstructions) { + final JavaProgramMatchHook hook = JavaProgramMatchHook.of(prog, programInstructions); + if (hook == null) { + return null; + } + final MatchInstruction kindInstr = mod.kind() instanceof ModalOperatorSV sv + ? matchModalOperatorSV(sv) + : getMatchIdentityInstruction(mod.kind()); + return new ModalityHead(kindInstr, hook); + } + + @Override + public void emit(List out) { + out.add(getCheckNodeKindInstruction(Modality.class)); + out.add(gotoNextInstruction()); + out.add(kindInstr); + out.add(gotoNextInstruction()); + out.add(programHook.programInstruction()); + out.add(gotoNextSiblingInstruction()); + } + + @Override + public MatchProgram compileHeadCheck() { + final MatchInstruction kind = kindInstr; + final MatchProgram programMatch = programHook.compileProgram(); + return (element, mc, services) -> { + if (!(((Term) element).op() instanceof Modality m)) { + return null; + } + final MatchResultInfo r = kind.match(m.kind(), mc, services); + if (r == null) { + return null; + } + return programMatch.match(((JTerm) element).javaBlock(), r, services); + }; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ParametricFunctionHead.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ParametricFunctionHead.java new file mode 100644 index 00000000000..c67cf949670 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/ParametricFunctionHead.java @@ -0,0 +1,94 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import java.util.List; + +import de.uka.ilkd.key.logic.GenericArgument; +import de.uka.ilkd.key.logic.op.ParametricFunctionInstance; +import de.uka.ilkd.key.logic.sort.GenericSort; +import de.uka.ilkd.key.logic.sort.ParametricSortInstance; + +import org.key_project.logic.Term; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.compiler.MatchHead; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; +import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; + +import org.jspecify.annotations.Nullable; + +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getCheckNodeKindInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchGenericSortInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchIdentityInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getSimilarParametricFunctionInstruction; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.gotoNextInstruction; + +/** + * Match head for a {@link ParametricFunctionInstance}: it checks that the operator has the same + * base + * and matches the generic arguments (a generic sort, or a concrete argument by identity); the + * function's subterms are matched by the enclosing + * {@link org.key_project.prover.rules.matcher.compiler.OperatorPlan}. Mirrors the + * parametric-function fragments of the hand-written matchers. + */ +public final class ParametricFunctionHead implements MatchHead { + + private final MatchInstruction similar; + private final MatchInstruction[] argMatchers; + + private ParametricFunctionHead(MatchInstruction similar, MatchInstruction[] argMatchers) { + this.similar = similar; + this.argMatchers = argMatchers; + } + + /** + * @param pfi the parametric function instance pattern + * @return a head for {@code pfi}, or {@code null} if a generic argument uses a parametric sort + * instance (which the matchers do not handle; then the caller falls back) + */ + public static @Nullable ParametricFunctionHead of(ParametricFunctionInstance pfi) { + final int argCount = pfi.getChildCount(); + final MatchInstruction[] argMatchers = new MatchInstruction[argCount]; + for (int i = 0; i < argCount; i++) { + final GenericArgument arg = (GenericArgument) pfi.getChild(i); + if (arg.sort() instanceof GenericSort gs) { + argMatchers[i] = getMatchGenericSortInstruction(gs); + } else if (arg.sort() instanceof ParametricSortInstance) { + return null; + } else { + argMatchers[i] = getMatchIdentityInstruction(arg); + } + } + return new ParametricFunctionHead(getSimilarParametricFunctionInstruction(pfi), + argMatchers); + } + + @Override + public void emit(List out) { + out.add(getCheckNodeKindInstruction(ParametricFunctionInstance.class)); + out.add(similar); + out.add(gotoNextInstruction()); + for (MatchInstruction argMatcher : argMatchers) { + out.add(argMatcher); + out.add(gotoNextInstruction()); + } + } + + @Override + public MatchProgram compileHeadCheck() { + final MatchInstruction sim = similar; + final MatchInstruction[] args = argMatchers; + return (element, mc, services) -> { + if (!(((Term) element).op() instanceof ParametricFunctionInstance actualPfi)) { + return null; + } + MatchResultInfo r = sim.match(actualPfi, mc, services); + for (int i = 0; r != null && i < args.length; i++) { + r = args[i].match(actualPfi.getChild(i), r, services); + } + return r; + }; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java index bcb62f832ea..3cc12b13449 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java @@ -194,7 +194,7 @@ private static void createProgram(JTerm pattern, ArrayList progra * active-statement matching; any other program is matched generically by a * {@link MatchSubProgramInstruction}. */ - private static @Nullable VMInstruction buildProgramInstruction(JavaProgramElement prog) { + static @Nullable VMInstruction buildProgramInstruction(JavaProgramElement prog) { if (prog instanceof ContextStatementBlock csb) { final VMInstruction[] active = buildContextActiveStatementsProgram(csb); return active == null ? null From 49b33a0166dd74c467d17d47b84bd91352e7a943 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 01:28:30 +0200 Subject: [PATCH 08/26] matcher: route VMTacletMatcher through the match-plan framework The find and assumes matchers are now built via JavaMatchPlanBuilder (interpreterProgram / compiledProgram) instead of calling the two hand-written dispatches directly, making the match-plan IR the single source of truth in production. The facades fall back to the legacy generator / compiled matcher for the constructs the framework does not build yet (term labels), so behaviour is unchanged. The key.matcher.compiled / key.matcher.programInstructions flags keep their meaning. --- .../de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java index 977bccca39f..3373d4ebe7d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java @@ -110,10 +110,13 @@ public VMTacletMatcher(Taclet taclet) { findExp = findTaclet.find(); ignoreTopLevelUpdates = taclet.ignoreTopLevelUpdates() && !(findExp.op() instanceof UpdateApplication); + // both back-ends are derived from the unified match-plan framework (one dispatch per + // construct, see JavaMatchPlanBuilder), which falls back to the legacy hand-written + // matchers for the few constructs it does not build yet (term labels) final VMProgramInterpreter interpreter = - new VMProgramInterpreter(SyntaxElementMatchProgramGenerator.createProgram(findExp)); + new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(findExp)); if (Boolean.getBoolean(COMPILE_MATCHERS_PROPERTY)) { - final CompiledMatchProgram compiled = CompiledMatchProgram.compile(findExp); + final MatchProgram compiled = JavaMatchPlanBuilder.compiledProgram(findExp); findMatchProgram = compiled != null ? compiled : interpreter; } else { findMatchProgram = interpreter; @@ -128,7 +131,7 @@ public VMTacletMatcher(Taclet taclet) { for (final SequentFormula sf : assumesSequent) { assumesMatchPrograms.put(sf.formula(), new VMProgramInterpreter( - SyntaxElementMatchProgramGenerator.createProgram((JTerm) sf.formula()))); + JavaMatchPlanBuilder.interpreterProgram((JTerm) sf.formula()))); } } From 7d16e6293de66c94d75193bd1e6c02ba2f36d8fe Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 01:28:30 +0200 Subject: [PATCH 09/26] test(dev): differential + benchmark cover the match-plan framework [DROP BEFORE MERGE] Extends the dev-only differential test and micro-benchmark (added in the "matcher differential test + isolated benchmarks" drop commit) to also exercise the match-plan framework alongside the hand-written matchers: - ProgramMatchDifferentialTest builds the plan and verifies its interpreter (with program-instruction conversion both off and on, since production reads that flag) and its compiled matcher against the legacy oracle (24.8M comparisons). - CompiledMatchProgramBenchmark times the framework-built matchers next to the hand-written ones for both back-ends (the no-overhead check). Like the commit it extends, this is dropped before merge. --- .../vm/CompiledMatchProgramBenchmark.java | 117 +++++++++++------- .../vm/ProgramMatchDifferentialTest.java | 44 ++++++- 2 files changed, 113 insertions(+), 48 deletions(-) diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java index 5e5f384541d..43693238211 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java @@ -17,6 +17,7 @@ import de.uka.ilkd.key.util.HelperClassForTests; import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; import org.key_project.prover.sequent.SequentFormula; @@ -25,11 +26,19 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Isolated micro-benchmark for the find matcher: it compares {@link VMProgramInterpreter} against - * {@link CompiledMatchProgram} directly (no taclet index, strategy or proof pipeline), over the - * subset of the real taclet base that the compiler handles (FOL / integer / propositional patterns; - * program symbolic-execution rules are excluded by the compiler and thus not part of the - * comparison). + * Isolated micro-benchmark for the find matcher (no taclet index, strategy or proof pipeline), over + * the subset of the real taclet base that the compiler handles. It serves two purposes: + * + *

    + *
  1. the headline comparison {@link VMProgramInterpreter} vs {@link CompiledMatchProgram} (the + * cursor-free win), and
  2. + *
  3. the no-overhead check for the match-plan framework: the matchers built through + * {@link JavaMatchPlanBuilder} (one description, two back-ends) are timed alongside the + * hand-written + * ones. Since the plan is lowered once at construction to the same {@code VMInstruction[]} / + * cursor-free closures, the framework-built matchers must run at parity with the hand-written ones + * (the IR adds no per-match cost).
  4. + *
* *

* By default it runs on the self-contained {@code tacletMatch1.key}. Point it at a wider set (e.g. @@ -48,7 +57,12 @@ public class CompiledMatchProgramBenchmark { "1 + 2 * 3 = 7", "\\forall int x; \\forall int y; (x + y = y + x)" }; - private record Task(List interps, List comps, + /** + * the four matchers built per compilable find-taclet: hand-written and framework, each + * back-end. + */ + private record Task(List handInterps, List handComps, + List planInterps, List planComps, List corpus, Services services) { } @@ -68,32 +82,52 @@ public void benchmarkInterpreterVsCompiled() { // warmup for (int pass = 0; pass < 5; pass++) { for (Task t : tasks) { - runInterp(t); - runComp(t); + run(t.handInterps, t); + run(t.handComps, t); + run(t.planInterps, t); + run(t.planComps, t); } } // timed: alternate phases per pass to average out JIT / cache effects final int passes = 30; - long interpMatches = 0, compMatches = 0, interpNanos = 0, compNanos = 0; + long handInterpN = 0, handCompN = 0, planInterpN = 0, planCompN = 0; + long handInterpM = 0, handCompM = 0, planInterpM = 0, planCompM = 0; for (int pass = 0; pass < passes; pass++) { for (Task t : tasks) { long t0 = System.nanoTime(); - interpMatches += runInterp(t); - interpNanos += System.nanoTime() - t0; + handInterpM += run(t.handInterps, t); + handInterpN += System.nanoTime() - t0; + + t0 = System.nanoTime(); + planInterpM += run(t.planInterps, t); + planInterpN += System.nanoTime() - t0; + + t0 = System.nanoTime(); + handCompM += run(t.handComps, t); + handCompN += System.nanoTime() - t0; t0 = System.nanoTime(); - compMatches += runComp(t); - compNanos += System.nanoTime() - t0; + planCompM += run(t.planComps, t); + planCompN += System.nanoTime() - t0; } } - System.out.printf("[isolated matcher, %d problem(s)] interpreter=%.1f ms compiled=%.1f ms" - + " speedup=%.2fx (matches interp=%d comp=%d)%n", - tasks.size(), interpNanos / 1e6, compNanos / 1e6, - (double) interpNanos / compNanos, interpMatches / passes, compMatches / passes); - assertEquals(interpMatches, compMatches, - "compiled and interpreter must agree on the number of matches"); + System.out.printf("[isolated matcher, %d problem(s)]%n", tasks.size()); + System.out.printf( + " interpreter : hand-written=%.1f ms framework=%.1f ms (overhead %+.1f%%)%n", + handInterpN / 1e6, planInterpN / 1e6, + 100.0 * (planInterpN - handInterpN) / handInterpN); + System.out.printf( + " compiled : hand-written=%.1f ms framework=%.1f ms (overhead %+.1f%%)%n", + handCompN / 1e6, planCompN / 1e6, 100.0 * (planCompN - handCompN) / handCompN); + System.out.printf(" speedup (framework compiled vs framework interpreter) = %.2fx%n", + (double) planInterpN / planCompN); + // all four matchers must see exactly the same matches + assertEquals(handInterpM, handCompM, "hand-written back-ends must agree on #matches"); + assertEquals(handInterpM, planInterpM, + "framework interpreter must agree with hand-written"); + assertEquals(handInterpM, planCompM, "framework compiled must agree with hand-written"); } private static List problemPaths() { @@ -126,8 +160,10 @@ private static Task buildTask(String pathStr) { } } - final List interps = new ArrayList<>(); - final List comps = new ArrayList<>(); + final List handInterps = new ArrayList<>(); + final List handComps = new ArrayList<>(); + final List planInterps = new ArrayList<>(); + final List planComps = new ArrayList<>(); int findTaclets = 0; for (Taclet t : pa.getFirstProof().getInitConfig().activatedTaclets()) { if (!(t instanceof FindTaclet ft)) { @@ -135,37 +171,28 @@ private static Task buildTask(String pathStr) { } findTaclets++; final JTerm find = (JTerm) ft.find(); - final CompiledMatchProgram comp = CompiledMatchProgram.compile(find); - if (comp == null) { - continue; + final MatchProgram handComp = CompiledMatchProgram.compile(find); + final MatchProgram planComp = JavaMatchPlanBuilder.compiledProgram(find); + if (handComp == null || planComp == null) { + continue; // restrict to the compilable subset, identical for both } - comps.add(comp); - interps.add( + handComps.add(handComp); + planComps.add(planComp); + handInterps.add( new VMProgramInterpreter(SyntaxElementMatchProgramGenerator.createProgram(find))); + planInterps.add( + new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(find))); } System.out.printf(" %-22s findTaclets=%4d compilable=%4d (%2.0f%%) corpus=%d%n", - path.getFileName(), findTaclets, comps.size(), - findTaclets == 0 ? 0 : 100.0 * comps.size() / findTaclets, corpus.size()); - return new Task(interps, comps, corpus, services); - } - - private static long runInterp(Task t) { - long matches = 0; - for (int p = 0, np = t.interps.size(); p < np; p++) { - final VMProgramInterpreter prog = t.interps.get(p); - for (int i = 0, n = t.corpus.size(); i < n; i++) { - if (prog.match(t.corpus.get(i), EMPTY, t.services) != null) { - matches++; - } - } - } - return matches; + path.getFileName(), findTaclets, handComps.size(), + findTaclets == 0 ? 0 : 100.0 * handComps.size() / findTaclets, corpus.size()); + return new Task(handInterps, handComps, planInterps, planComps, corpus, services); } - private static long runComp(Task t) { + private static long run(List programs, Task t) { long matches = 0; - for (int p = 0, np = t.comps.size(); p < np; p++) { - final CompiledMatchProgram prog = t.comps.get(p); + for (int p = 0, np = programs.size(); p < np; p++) { + final MatchProgram prog = programs.get(p); for (int i = 0, n = t.corpus.size(); i < n; i++) { if (prog.match(t.corpus.get(i), EMPTY, t.services) != null) { matches++; diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java index f6004f692b6..f292028fc20 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java @@ -26,6 +26,8 @@ import org.key_project.logic.op.Modality; import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.compiler.MatchPlan; +import org.key_project.prover.rules.matcher.vm.MatchProgram; import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; import org.key_project.prover.sequent.SequentFormula; @@ -90,6 +92,7 @@ public void convertedMatchesInterpreter() throws Exception { int convertedGeneric = 0; int compiledTaclets = 0; int compiledBoundVar = 0; + int planTaclets = 0; long matches = 0; int comparisons = 0; for (Taclet t : proof.getInitConfig().activatedTaclets()) { @@ -108,6 +111,28 @@ public void convertedMatchesInterpreter() throws Exception { compiledBoundVar++; } } + // the unified match-plan framework (both back-ends from one description); null for + // constructs not yet migrated to the dispatch + final MatchPlan plan = JavaMatchPlanBuilder.buildPlan(find, false); + VMProgramInterpreter planInterp = null; + MatchProgram planCompiled = null; + if (plan != null) { + planTaclets++; + final List planInstr = new ArrayList<>(); + plan.emitInstructions(planInstr); + planInterp = new VMProgramInterpreter(planInstr.toArray(new VMInstruction[0])); + planCompiled = plan.compile(); + } + // also verify the plan's interpreter with program-instruction conversion ON + // (production reads key.matcher.programInstructions; the plan must agree for both) + final MatchPlan planConv = JavaMatchPlanBuilder.buildPlan(find, true); + VMProgramInterpreter planConvInterp = null; + if (planConv != null) { + final List planConvInstr = new ArrayList<>(); + planConv.emitInstructions(planConvInstr); + planConvInterp = + new VMProgramInterpreter(planConvInstr.toArray(new VMInstruction[0])); + } // the converted interpreter (programInstructions=true) only differs for programs VMProgramInterpreter converted = null; if (program) { @@ -133,6 +158,16 @@ public void convertedMatchesInterpreter() throws Exception { if (compiled != null) { assertSameResult(t, term, oracleRes, compiled.match(term, EMPTY, services)); } + if (plan != null) { + assertSameResult(t, term, oracleRes, + planInterp.match(term, EMPTY, services)); + assertSameResult(t, term, oracleRes, + planCompiled.match(term, EMPTY, services)); + } + if (planConv != null) { + assertSameResult(t, term, oracleRes, + planConvInterp.match(term, EMPTY, services)); + } if (oracleRes != null) { matches++; } @@ -141,10 +176,13 @@ public void convertedMatchesInterpreter() throws Exception { System.out.printf( "[program-match differential] findTaclets=%d programTaclets=%d convertedContext=%d " - + "convertedGeneric=%d compiled=%d (boundVar=%d) corpus=%d comparisons=%d " - + "matches=%d%n", + + "convertedGeneric=%d compiled=%d (boundVar=%d) plan=%d corpus=%d " + + "comparisons=%d matches=%d%n", findTaclets, programTaclets, convertedContext, convertedGeneric, compiledTaclets, - compiledBoundVar, corpus.size(), comparisons, matches); + compiledBoundVar, planTaclets, corpus.size(), comparisons, matches); + // sanity floor: the run must actually exercise the unified match-plan framework + assertEquals(true, planTaclets > 0, + "expected at least some taclets to be built by the match-plan framework"); // sanity floor: the run must actually exercise the step-2 context-block conversion assertEquals(true, convertedContext > 0, "expected at least some taclets to use the converted context-block matcher"); From 4916ca4b95c3f337bd6289f3447d641437e5088a Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 02:11:38 +0200 Subject: [PATCH 10/26] matcher: collapse the hand-written matchers into the framework (single source) With both back-ends now derived from the match-plan framework, the two hand-written per-construct dispatches are redundant and are removed, leaving the framework as the single source of truth for find-matching: - delete CompiledMatchProgram's term-level dispatch (compile / compileStep / compileCore / compile{ElementaryUpdate,ParametricFunction,Modality}); the heads already carry that logic. Its reused Java-program helpers (compiledProgramMatcher, compileProgram, delegateToMatch, compileActiveStatements) move to a small JavaProgramCompiler used by the program hook. - delete SyntaxElementMatchProgramGenerator's createProgram dispatch; only the program-instruction conversion helpers (buildProgramInstruction & co), which the hook reuses, remain. - migrate term labels into the framework (JavaMatchPlanBuilder.LabelPlan, reusing the matchTermLabelSV instruction) so buildPlan is total; the facades no longer fall back -- an unsupported pattern raises a clear error naming the missing head (no current taclet hits this; the whole standard base is covered). - retarget CompiledMatchProgramTest to the framework facade. Net: ~390 fewer lines of production matcher code, no duplicated dispatch. The interpreter/compiled engines and the program helpers are unchanged. --- .../rule/match/vm/CompiledMatchProgram.java | 496 ------------------ .../rule/match/vm/JavaMatchPlanBuilder.java | 98 ++-- .../rule/match/vm/JavaProgramCompiler.java | 201 +++++++ .../rule/match/vm/JavaProgramMatchHook.java | 4 +- .../SyntaxElementMatchProgramGenerator.java | 139 +---- .../match/vm/CompiledMatchProgramTest.java | 27 +- 6 files changed, 287 insertions(+), 678 deletions(-) delete mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramCompiler.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java deleted file mode 100644 index 6b00f89e9bd..00000000000 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgram.java +++ /dev/null @@ -1,496 +0,0 @@ -/* This file is part of KeY - https://key-project.org - * KeY is licensed under the GNU General Public License Version 2 - * SPDX-License-Identifier: GPL-2.0-only */ -package de.uka.ilkd.key.rule.match.vm; - -import java.util.concurrent.atomic.AtomicLong; - -import de.uka.ilkd.key.java.Services; -import de.uka.ilkd.key.java.ast.ContextStatementBlock; -import de.uka.ilkd.key.java.ast.JavaProgramElement; -import de.uka.ilkd.key.java.ast.ProgramElement; -import de.uka.ilkd.key.java.ast.SourceData; -import de.uka.ilkd.key.logic.GenericArgument; -import de.uka.ilkd.key.logic.JTerm; -import de.uka.ilkd.key.logic.JavaBlock; -import de.uka.ilkd.key.logic.op.ElementaryUpdate; -import de.uka.ilkd.key.logic.op.LocationVariable; -import de.uka.ilkd.key.logic.op.ModalOperatorSV; -import de.uka.ilkd.key.logic.op.ParametricFunctionInstance; -import de.uka.ilkd.key.logic.op.ProgramSV; -import de.uka.ilkd.key.logic.sort.GenericSort; -import de.uka.ilkd.key.logic.sort.ParametricSortInstance; -import de.uka.ilkd.key.rule.MatchConditions; - -import org.key_project.logic.LogicServices; -import org.key_project.logic.SyntaxElement; -import org.key_project.logic.op.Modality; -import org.key_project.logic.op.Operator; -import org.key_project.logic.op.QuantifiableVariable; -import org.key_project.logic.op.sv.SchemaVariable; -import org.key_project.prover.rules.instantiation.MatchResultInfo; -import org.key_project.prover.rules.matcher.vm.MatchProgram; -import org.key_project.prover.rules.matcher.vm.ProgramChildrenMatcher; -import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; -import org.key_project.util.collection.ImmutableArray; - -import org.jspecify.annotations.Nullable; - -import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchGenericSortInstruction; -import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchIdentityInstruction; -import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchInstructionForSV; -import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getSimilarParametricFunctionInstruction; -import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchAndBindVariables; -import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchModalOperatorSV; - -/** - * A compiled {@link MatchProgram} for a taclet's find expression. Instead of interpreting a list of - * {@code VMInstruction}s over a generic {@code PoolSyntaxElementCursor}, it navigates the term - * structure directly via {@code term.op()} / {@code term.sub(i)} (and the Java program via - * {@code getChild(i)}), which avoids the cursor entirely. - *

- * It compiles essentially the whole taclet base: ordinary operators and schema variables, bound - * variables (quantifiers / substitutions), modalities with their Java program (generic programs and - * context blocks, see {@link #compileModality}), parametric function instances and elementary - * updates. Program elements that define their own {@code match} (value literals, type references, - * loops, ...) and generic elements with variable-arity children (a list schema variable - * {@code #slist}) are reused cursor-free by {@linkplain #delegateToMatch delegating to their own - * match}, so the surrounding term stays compiled. {@link #compile(JTerm)} returns {@code null} only - * for the rare patterns it still cannot handle -- term labels, parametric-sort generic arguments, - * unusual schema-variable shapes -- and the caller then falls back to the - * {@code VMProgramInterpreter}, which stays the source of truth so the compiled path can always be - * switched off. - * - * @see org.key_project.prover.rules.matcher.vm.VMProgramInterpreter - */ -public final class CompiledMatchProgram implements MatchProgram { - - /** number of find patterns that were successfully compiled (for measurement). */ - private static final AtomicLong COMPILED = new AtomicLong(); - /** number of find patterns that fell back to the interpreter (for measurement). */ - private static final AtomicLong FALLBACK = new AtomicLong(); - - /** - * A single compiled matching step over a (sub)term. Replaces the cursor-driven instruction - * sequence by direct navigation. - */ - @FunctionalInterface - private interface Step { - @Nullable - MatchResultInfo match(JTerm term, MatchResultInfo mc, LogicServices services); - } - - private final Step root; - - private CompiledMatchProgram(Step root) { - this.root = root; - } - - @Override - public @Nullable MatchResultInfo match(SyntaxElement toMatch, MatchResultInfo mc, - LogicServices services) { - return root.match((JTerm) toMatch, mc, services); - } - - /** - * Compiles the given find pattern, or returns {@code null} if it uses a feature not yet - * supported by the compiler (the caller then uses the interpreter). - * - * @param pattern the find expression of the taclet - * @return a compiled program, or {@code null} to fall back to the interpreter - */ - public static @Nullable CompiledMatchProgram compile(JTerm pattern) { - final Step root = compileStep(pattern); - if (root == null) { - FALLBACK.incrementAndGet(); - return null; - } - COMPILED.incrementAndGet(); - return new CompiledMatchProgram(root); - } - - private static @Nullable Step compileStep(JTerm pattern) { - // term labels are matched by a dedicated instruction; not yet compiled - if (pattern.hasLabels()) { - return null; - } - - final Step core = compileCore(pattern); - if (core == null) { - return null; - } - - final ImmutableArray boundVars = pattern.boundVars(); - if (boundVars.isEmpty()) { - return core; - } - - // bound variables (quantifiers, substitutions, ...): bind the pattern's bound variables to - // the source term's bound variables (renaming-aware), match the operator and subterms in - // that scope, then unbind -- exactly as the interpreter does with - // BindVariablesInstruction / UnbindVariablesInstruction, but cursor-free. The bind - // instruction reads the source term's own bound variables from the element it is given. - final MatchInstruction bind = matchAndBindVariables(boundVars); - return (term, mc, services) -> { - MatchResultInfo r = bind.match(term, mc, services); - if (r == null) { - return null; - } - r = core.match(term, r, services); - if (r == null) { - return null; - } - return ((MatchConditions) r).shrinkRenameTable(); - }; - } - - /** - * Compiles the operator and subterms of {@code pattern} (without the bound-variable / label - * handling, which {@link #compileStep} wraps around this). Returns {@code null} if a construct - * is not yet supported. - */ - private static @Nullable Step compileCore(JTerm pattern) { - final Operator op = pattern.op(); - - if (op instanceof SchemaVariable sv) { - if (pattern.arity() != 0) { - return null; // unusual schema-variable shape; let the interpreter handle it - } - // a schema variable matches the whole (sub)term; reuse the existing SV match logic, - // which already accepts the element directly (no cursor needed) - final MatchInstruction svInstr = getMatchInstructionForSV(sv); - return (term, mc, services) -> svInstr.match(term, mc, services); - } - - // a modality: compile the modal-operator kind, the Java program and the sub-formula(s) - if (op instanceof Modality) { - return compileModality(pattern); - } - - // a parametric function instance: similar-base check + generic-argument matching + subterms - if (op instanceof ParametricFunctionInstance) { - return compileParametricFunction(pattern); - } - - // an elementary update lhs := value: match the left-hand side then the value - if (op instanceof ElementaryUpdate) { - return compileElementaryUpdate(pattern); - } - - final int arity = pattern.arity(); - if (arity == 0) { - // a constant/leaf operator: faithful to MatchIdentityInstruction (reference equality) - return (term, mc, services) -> term.op() == op ? mc : null; - } - - final Step[] subs = new Step[arity]; - for (int i = 0; i < arity; i++) { - final Step s = compileStep(pattern.sub(i)); - if (s == null) { - return null; - } - subs[i] = s; - } - - return (term, mc, services) -> { - if (term.op() != op) { - return null; - } - MatchResultInfo r = mc; - for (int i = 0; i < subs.length; i++) { - r = subs[i].match(term.sub(i), r, services); - if (r == null) { - return null; - } - } - return r; - }; - } - - /** - * Compiles an elementary update {@code lhs := value}: matches the left-hand side (a schema - * variable, or a concrete location variable by identity) then the value subterm, mirroring the - * generator's elementary-update case. - */ - private static @Nullable Step compileElementaryUpdate(JTerm pattern) { - final ElementaryUpdate elUp = (ElementaryUpdate) pattern.op(); - final MatchInstruction lhsMatcher; - if (elUp.lhs() instanceof SchemaVariable sv) { - lhsMatcher = getMatchInstructionForSV(sv); - } else if (elUp.lhs() instanceof LocationVariable locVar) { - lhsMatcher = getMatchIdentityInstruction(locVar); - } else { - return null; // unexpected left-hand side kind -> fall back - } - final Step valueStep = compileStep(pattern.sub(0)); - if (valueStep == null) { - return null; - } - return (term, mc, services) -> { - if (!(term.op() instanceof ElementaryUpdate actualElUp)) { - return null; - } - final MatchResultInfo r = lhsMatcher.match(actualElUp.lhs(), mc, services); - return r == null ? null : valueStep.match(term.sub(0), r, services); - }; - } - - /** - * Compiles a parametric function instance: a similar-base check on the operator, then the - * generic arguments (generic sorts via {@link MatchGenericSortInstruction}, concrete arguments - * by identity), then the subterms. Mirrors the generator's parametric-function case. Returns - * {@code null} if a generic argument uses a parametric sort instance (which the generator does - * not handle either). - */ - private static @Nullable Step compileParametricFunction(JTerm pattern) { - final ParametricFunctionInstance pfi = (ParametricFunctionInstance) pattern.op(); - final MatchInstruction similar = getSimilarParametricFunctionInstruction(pfi); - - final int argCount = pfi.getChildCount(); - final MatchInstruction[] argMatchers = new MatchInstruction[argCount]; - for (int i = 0; i < argCount; i++) { - final GenericArgument arg = (GenericArgument) pfi.getChild(i); - if (arg.sort() instanceof GenericSort gs) { - argMatchers[i] = getMatchGenericSortInstruction(gs); - } else if (arg.sort() instanceof ParametricSortInstance) { - return null; // parametric sort in generic args: generator does not handle it either - } else { - argMatchers[i] = getMatchIdentityInstruction(arg); - } - } - - final int arity = pattern.arity(); - final Step[] subs = new Step[arity]; - for (int i = 0; i < arity; i++) { - final Step s = compileStep(pattern.sub(i)); - if (s == null) { - return null; - } - subs[i] = s; - } - - return (term, mc, services) -> { - if (!(term.op() instanceof ParametricFunctionInstance actualPfi)) { - return null; - } - MatchResultInfo r = similar.match(actualPfi, mc, services); - for (int i = 0; r != null && i < argCount; i++) { - r = argMatchers[i].match(actualPfi.getChild(i), r, services); - } - for (int i = 0; r != null && i < subs.length; i++) { - r = subs[i].match(term.sub(i), r, services); - } - return r; - }; - } - - /** - * A single compiled matching step over a program (sub)element. Navigates the Java AST directly - * via {@code getChild(i)} instead of a cursor, mirroring the converted program VM instructions. - */ - @FunctionalInterface - private interface ProgStep { - @Nullable - MatchResultInfo match(SyntaxElement actual, MatchResultInfo mc, LogicServices services); - } - - /** - * Compiles a modality pattern {@code \ phi}: the modal-operator kind (reusing the - * existing element-based instructions), the Java program (generic program or context block, - * cursor-free) and the sub-formula(s). Returns {@code null} if the program or a sub-formula - * uses - * a construct the compiler does not handle (the caller then falls back to the interpreter). - */ - private static @Nullable Step compileModality(JTerm pattern) { - final Modality mod = (Modality) pattern.op(); - final MatchInstruction kindInstr = - mod.kind() instanceof ModalOperatorSV sv ? matchModalOperatorSV(sv) - : getMatchIdentityInstruction(mod.kind()); - - final JavaProgramElement prog = pattern.javaBlock().program(); - final MatchProgram progMatch = compiledProgramMatcher(prog); - if (progMatch == null) { - return null; - } - - final int arity = pattern.arity(); - final Step[] subs = new Step[arity]; - for (int i = 0; i < arity; i++) { - final Step s = compileStep(pattern.sub(i)); - if (s == null) { - return null; - } - subs[i] = s; - } - - return (term, mc, services) -> { - if (!(term.op() instanceof Modality m)) { - return null; - } - MatchResultInfo r = kindInstr.match(m.kind(), mc, services); - if (r == null) { - return null; - } - r = progMatch.match(term.javaBlock(), r, services); - if (r == null) { - return null; - } - for (int i = 0; i < subs.length; i++) { - r = subs[i].match(term.sub(i), r, services); - if (r == null) { - return null; - } - } - return r; - }; - } - - /** - * Compiles the cursor-free matcher for the Java program {@code prog} of a modality, applied - * directly to the source {@link JavaBlock} (it extracts the block's program element). A - * top-level - * {@link ContextStatementBlock} keeps phases (1)(2)(4) of the context match in - * {@code ContextStatementBlock.match} and compiles only phase (3) (each active statement - * consumes - * one source child), unless an active statement is variable-arity (a list SV) or otherwise - * uncompilable -- then the whole context match is delegated to - * {@code ContextStatementBlock.match} - * (its {@code matchChildren} handles list SVs) while the surrounding term skeleton stays - * compiled. - * Any other program is compiled by {@link #compileProgram}. Returns {@code null} only if that - * generic compilation cannot handle the program. Shared by {@link #compileModality} and the - * Java {@code ProgramMatchHook} so both reuse one program-matching implementation. - */ - static @Nullable MatchProgram compiledProgramMatcher(JavaProgramElement prog) { - if (prog instanceof ContextStatementBlock csb) { - final ProgStep[] active = compileActiveStatements(csb); - if (active != null) { - // phase (3) of the context match, cursor-free: each active statement consumes one - // child - final ProgramChildrenMatcher phase3 = (parent, startChild, mc, services) -> { - MatchResultInfo r = mc; - for (int k = 0; k < active.length; k++) { - r = active[k].match(parent.getChild(startChild + k), r, services); - if (r == null) { - return null; - } - } - return r; - }; - // phases (1)(2)(4) stay in ContextStatementBlock.match; only phase (3) is compiled - return (block, mc, services) -> csb.match( - new SourceData(((JavaBlock) block).program(), -1, (Services) services), - (MatchConditions) mc, phase3); - } - // an active statement is variable-arity (a list SV) or otherwise uncompilable: - // delegate the whole context match to the interpreter (its matchChildren handles - // list SVs); the surrounding term skeleton stays compiled - return (block, mc, services) -> csb.match( - new SourceData(((JavaBlock) block).program(), -1, (Services) services), - (MatchConditions) mc); - } - final ProgStep ps = compileProgram(prog); - if (ps == null) { - return null; - } - return (block, mc, services) -> ps.match(((JavaBlock) block).program(), mc, services); - } - - /** - * Compiles a Java program (sub)element: a generic-match element with a fixed, one-source-child - * structure is matched by direct {@code getChild} navigation (class equality + exact-size child - * recursion); a non-list program schema variable reuses its program-SV instruction. Anything - * else that is still a {@link ProgramElement} -- an element with its own {@code match} (value - * literals, type references, loops, ...) or a generic element whose children are not a - * fixed one-to-one structure (e.g. they contain a list schema variable {@code #slist}) -- is - * matched cursor-free by {@linkplain #delegateToMatch delegating to its own match}. Returns - * {@code null} only for a list schema variable on its own (variable arity: its enclosing - * element - * delegates) and for non-program schema variables. - */ - private static @Nullable ProgStep compileProgram(SyntaxElement pe) { - if (pe instanceof ProgramSV psv) { - if (psv.isListSV()) { - // a list SV by itself is variable-arity; the enclosing element delegates instead - return null; - } - final MatchInstruction svInstr = getMatchInstructionForSV(psv); - return svInstr::match; - } - if (pe instanceof SchemaVariable) { - return null; // other schema variables in programs: be safe, fall back - } - if (!(pe instanceof ProgramElement progEl)) { - return null; - } - if (SyntaxElementMatchProgramGenerator.isGenericMatch(progEl)) { - final int childCount = pe.getChildCount(); - final ProgStep[] subs = new ProgStep[childCount]; - boolean fixedStructure = true; - for (int i = 0; i < childCount; i++) { - final ProgStep s = compileProgram(pe.getChild(i)); - if (s == null) { - fixedStructure = false; // e.g. a list SV child -> not one-to-one - break; - } - subs[i] = s; - } - if (fixedStructure) { - final Class kind = pe.getClass(); - return (actual, mc, services) -> { - if (actual.getClass() != kind || actual.getChildCount() != childCount) { - return null; - } - MatchResultInfo r = mc; - for (int i = 0; i < childCount; i++) { - r = subs[i].match(actual.getChild(i), r, services); - if (r == null) { - return null; - } - } - return r; - }; - } - } - // an element with its own match (value literals, TypeRef, SchematicFieldReference, - // VariableSpecification, loops, ...) or a generic element with variable-arity children - // (a list SV): reuse its own match cursor-free (see delegateToMatch) - return delegateToMatch(progEl); - } - - /** - * Matches {@code progEl} by reusing its own {@code match(SourceData, MatchConditions)} - * cursor-free, exactly as {@code MatchProgramInstruction} does at the program root. This keeps - * the surrounding program compiled (only this sub-element delegates) and is - * behaviour-preserving - * by construction: it is the very match the interpreter would call, including the - * {@code matchChildren} handling of list schema variables. - */ - private static ProgStep delegateToMatch(ProgramElement progEl) { - return (actual, mc, services) -> progEl.match( - new SourceData((ProgramElement) actual, -1, (Services) services), (MatchConditions) mc); - } - - /** - * Compiles the active statements of a context block (its children from the active offset, i.e. - * skipping the execution context if present), or returns {@code null} if any active statement - * uses a construct the compiler does not handle. - */ - private static ProgStep @Nullable [] compileActiveStatements(ContextStatementBlock csb) { - final int offset = csb.getExecutionContext() == null ? 0 : 1; - final ProgStep[] active = new ProgStep[csb.getChildCount() - offset]; - for (int i = offset, n = csb.getChildCount(); i < n; i++) { - final ProgStep s = compileProgram(csb.getChildAt(i)); - if (s == null) { - return null; - } - active[i - offset] = s; - } - return active; - } - - /** @return {@code [compiled, fallback]} pattern counts since startup (for measurement). */ - public static long[] statistics() { - return new long[] { COMPILED.get(), FALLBACK.get() }; - } -} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java index 135537a52d8..66dbdb8c27b 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java @@ -13,30 +13,35 @@ import org.key_project.logic.op.Modality; import org.key_project.logic.op.Operator; import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.rules.instantiation.MatchResultInfo; import org.key_project.prover.rules.matcher.compiler.GenericOperatorHead; import org.key_project.prover.rules.matcher.compiler.MatchHead; import org.key_project.prover.rules.matcher.compiler.MatchPlan; import org.key_project.prover.rules.matcher.compiler.OperatorPlan; import org.key_project.prover.rules.matcher.compiler.SchemaVarPlan; import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; import org.jspecify.annotations.Nullable; import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchInstructionForSV; +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.matchTermLabelSV; /** * The single Java-DL dispatch that builds a {@link MatchPlan} for a find pattern, from which both * the interpreter and the compiled find-matcher are derived. Describing a construct here gives both - * back-ends at once (the goal of the match-plan framework). + * back-ends at once -- this is the sole source of truth for find-matching; there is no separate + * hand-written interpreter generator or compiled matcher to keep in sync. * *

- * It covers the FOL term skeleton (schema variables, ordinary operators with their subterms, bound - * variables), elementary updates, parametric function instances and modalities (the Java program is - * matched through a {@link org.key_project.prover.rules.matcher.compiler.ProgramMatchHook}). It - * returns {@code null} only for constructs outside this set (currently term labels) or when a - * modality's program cannot be matched by the framework, so callers fall back to the legacy - * hand-written matchers for those. + * It covers the whole Java-DL find-taclet base: the FOL term skeleton (schema variables, ordinary + * operators with their subterms, bound variables), term labels, elementary updates, parametric + * function instances and modalities (the Java program is matched through a + * {@link org.key_project.prover.rules.matcher.compiler.ProgramMatchHook}). A pattern the dispatch + * genuinely cannot model yields {@code null} and the {@linkplain #interpreterProgram facades} raise + * a clear error pointing at the missing head (no current taclet hits this; adding a construct means + * adding one head, see the developer docs). */ public final class JavaMatchPlanBuilder { @@ -44,9 +49,7 @@ private JavaMatchPlanBuilder() {} /** * Builds the interpreter program for {@code pattern} through the match-plan framework, reading - * the {@code key.matcher.programInstructions} flag (as the legacy generator does). Falls back - * to - * the legacy generator for constructs the framework does not build (term labels). + * the {@code key.matcher.programInstructions} flag. * * @param pattern the find / assumes pattern * @return the VM instruction program @@ -57,53 +60,58 @@ public static VMInstruction[] interpreterProgram(JTerm pattern) { } /** - * Builds the interpreter program for {@code pattern} through the match-plan framework, falling - * back to the legacy generator for constructs the framework does not build (term labels). + * Builds the interpreter program for {@code pattern} through the match-plan framework. * * @param pattern the find / assumes pattern * @param programInstructions whether modality programs are converted to VM instructions * @return the VM instruction program */ public static VMInstruction[] interpreterProgram(JTerm pattern, boolean programInstructions) { - final MatchPlan plan = buildPlan(pattern, programInstructions); - if (plan == null) { - return SyntaxElementMatchProgramGenerator.createProgram(pattern, programInstructions); - } final List out = new ArrayList<>(); - plan.emitInstructions(out); + planOrThrow(pattern, programInstructions).emitInstructions(out); return out.toArray(new VMInstruction[0]); } /** - * Builds the cursor-free compiled matcher for {@code pattern} through the match-plan framework, - * falling back to the legacy compiled matcher for constructs the framework does not build. + * Builds the cursor-free compiled matcher for {@code pattern} through the match-plan framework. * * @param pattern the find pattern - * @return the compiled matcher, or {@code null} if neither the framework nor the legacy - * compiler - * can build it (the caller then uses the interpreter) + * @return the compiled matcher */ - public static @Nullable MatchProgram compiledProgram(JTerm pattern) { - final MatchPlan plan = buildPlan(pattern, false); - if (plan != null) { - return plan.compile(); + public static MatchProgram compiledProgram(JTerm pattern) { + return planOrThrow(pattern, false).compile(); + } + + private static MatchPlan planOrThrow(JTerm pattern, boolean programInstructions) { + final MatchPlan plan = buildPlan(pattern, programInstructions); + if (plan == null) { + throw new UnsupportedOperationException( + "the match-plan framework has no head for this find pattern (op " + + pattern.op() + "); add one (see the taclet-matching developer docs): " + + pattern); } - return CompiledMatchProgram.compile(pattern); + return plan; } /** - * Builds a match plan for {@code pattern}, or returns {@code null} if it uses a construct not - * yet handled by the dispatch (the caller then uses the legacy matcher). + * Builds a match plan for {@code pattern}, or returns {@code null} if it uses a construct the + * dispatch cannot model (no current taclet does). * * @param pattern the find (sub)pattern * @param programInstructions whether modality programs are converted to VM instructions on the * interpreter side (irrelevant for the FOL skeleton and the compiled back-end) - * @return a match plan, or {@code null} to fall back + * @return a match plan, or {@code null} */ public static @Nullable MatchPlan buildPlan(JTerm pattern, boolean programInstructions) { - if (pattern.hasLabels()) { - return null; // term labels: not handled by the framework yet + final MatchPlan core = buildCore(pattern, programInstructions); + if (core == null || !pattern.hasLabels()) { + return core; } + // term labels are matched in place (no cursor move), before the operator/subterms + return new LabelPlan(matchTermLabelSV(pattern.getLabels()), core); + } + + private static @Nullable MatchPlan buildCore(JTerm pattern, boolean programInstructions) { final Operator op = pattern.op(); if (op instanceof SchemaVariable sv) { @@ -116,7 +124,7 @@ public static VMInstruction[] interpreterProgram(JTerm pattern, boolean programI final MatchHead head = buildHead(pattern, programInstructions); if (head == null) { - return null; // unsupported construct or uncompilable program -> fall back + return null; // unsupported construct or uncompilable program } // the operator head plus a plan per subterm @@ -125,7 +133,7 @@ public static VMInstruction[] interpreterProgram(JTerm pattern, boolean programI for (int i = 0; i < arity; i++) { final MatchPlan child = buildPlan(pattern.sub(i), programInstructions); if (child == null) { - return null; // a subterm is not handled -> the whole pattern falls back + return null; // a subterm is not handled -> the whole pattern is unsupported } children.add(child); } @@ -149,4 +157,26 @@ public static VMInstruction[] interpreterProgram(JTerm pattern, boolean programI } return new GenericOperatorHead(op); } + + /** + * Wraps a plan with a term-label match: the labels are matched in place (the same + * {@code matchTermLabelSV} instruction the interpreter uses, no cursor move) before the wrapped + * operator/subterm matching. Reused by both back-ends. + */ + private record LabelPlan(MatchInstruction labelInstr, MatchPlan inner) implements MatchPlan { + @Override + public void emitInstructions(List out) { + out.add(labelInstr); + inner.emitInstructions(out); + } + + @Override + public MatchProgram compile() { + final MatchProgram innerCompiled = inner.compile(); + return (element, mc, services) -> { + final MatchResultInfo r = labelInstr.match(element, mc, services); + return r == null ? null : innerCompiled.match(element, r, services); + }; + } + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramCompiler.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramCompiler.java new file mode 100644 index 00000000000..83f009f0bac --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramCompiler.java @@ -0,0 +1,201 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.rule.match.vm; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.java.ast.ContextStatementBlock; +import de.uka.ilkd.key.java.ast.JavaProgramElement; +import de.uka.ilkd.key.java.ast.ProgramElement; +import de.uka.ilkd.key.java.ast.SourceData; +import de.uka.ilkd.key.logic.JavaBlock; +import de.uka.ilkd.key.logic.op.ProgramSV; +import de.uka.ilkd.key.rule.MatchConditions; + +import org.key_project.logic.LogicServices; +import org.key_project.logic.SyntaxElement; +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; +import org.key_project.prover.rules.matcher.vm.ProgramChildrenMatcher; +import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; + +import org.jspecify.annotations.Nullable; + +import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.getMatchInstructionForSV; + +/** + * Compiles the cursor-free matcher for the Java program of a modality, used by the Java + * {@link org.key_project.prover.rules.matcher.compiler.ProgramMatchHook} (the compiled side of the + * match-plan framework). It navigates the Java AST directly via {@code getChild(i)} instead of a + * cursor, mirroring the converted program VM instructions of + * {@link SyntaxElementMatchProgramGenerator}. + * + *

+ * A top-level {@link ContextStatementBlock} keeps phases (1)(2)(4) of the context match in + * {@code ContextStatementBlock.match} and compiles only phase (3); generic structural elements are + * matched by class equality + exact-size child recursion; elements with their own {@code match} + * (value literals, type references, loops, ...) and generic elements with variable-arity children + * (a + * list schema variable {@code #slist}) are reused cursor-free by {@linkplain #delegateToMatch + * delegating to their own match}. + * + * @see org.key_project.prover.rules.matcher.vm.VMProgramInterpreter + */ +final class JavaProgramCompiler { + + private JavaProgramCompiler() {} + + /** + * A single compiled matching step over a program (sub)element. Navigates the Java AST directly + * via {@code getChild(i)} instead of a cursor, mirroring the converted program VM instructions. + */ + @FunctionalInterface + private interface ProgStep { + @Nullable + MatchResultInfo match(SyntaxElement actual, MatchResultInfo mc, LogicServices services); + } + + /** + * Compiles the cursor-free matcher for the Java program {@code prog} of a modality, applied + * directly to the source {@link JavaBlock} (it extracts the block's program element). A + * top-level + * {@link ContextStatementBlock} keeps phases (1)(2)(4) of the context match in + * {@code ContextStatementBlock.match} and compiles only phase (3) (each active statement + * consumes + * one source child), unless an active statement is variable-arity (a list SV) or otherwise + * uncompilable -- then the whole context match is delegated to + * {@code ContextStatementBlock.match} + * (its {@code matchChildren} handles list SVs) while the surrounding term skeleton stays + * compiled. + * Any other program is compiled by {@link #compileProgram}. Returns {@code null} only if that + * generic compilation cannot handle the program. + */ + static @Nullable MatchProgram compiledProgramMatcher(JavaProgramElement prog) { + if (prog instanceof ContextStatementBlock csb) { + final ProgStep[] active = compileActiveStatements(csb); + if (active != null) { + // phase (3) of the context match, cursor-free: each active statement consumes one + // child + final ProgramChildrenMatcher phase3 = (parent, startChild, mc, services) -> { + MatchResultInfo r = mc; + for (int k = 0; k < active.length; k++) { + r = active[k].match(parent.getChild(startChild + k), r, services); + if (r == null) { + return null; + } + } + return r; + }; + // phases (1)(2)(4) stay in ContextStatementBlock.match; only phase (3) is compiled + return (block, mc, services) -> csb.match( + new SourceData(((JavaBlock) block).program(), -1, (Services) services), + (MatchConditions) mc, phase3); + } + // an active statement is variable-arity (a list SV) or otherwise uncompilable: + // delegate the whole context match to the interpreter (its matchChildren handles + // list SVs); the surrounding term skeleton stays compiled + return (block, mc, services) -> csb.match( + new SourceData(((JavaBlock) block).program(), -1, (Services) services), + (MatchConditions) mc); + } + final ProgStep ps = compileProgram(prog); + if (ps == null) { + return null; + } + return (block, mc, services) -> ps.match(((JavaBlock) block).program(), mc, services); + } + + /** + * Compiles a Java program (sub)element: a generic-match element with a fixed, one-source-child + * structure is matched by direct {@code getChild} navigation (class equality + exact-size child + * recursion); a non-list program schema variable reuses its program-SV instruction. Anything + * else that is still a {@link ProgramElement} -- an element with its own {@code match} (value + * literals, type references, loops, ...) or a generic element whose children are not a + * fixed one-to-one structure (e.g. they contain a list schema variable {@code #slist}) -- is + * matched cursor-free by {@linkplain #delegateToMatch delegating to its own match}. Returns + * {@code null} only for a list schema variable on its own (variable arity: its enclosing + * element + * delegates) and for non-program schema variables. + */ + private static @Nullable ProgStep compileProgram(SyntaxElement pe) { + if (pe instanceof ProgramSV psv) { + if (psv.isListSV()) { + // a list SV by itself is variable-arity; the enclosing element delegates instead + return null; + } + final MatchInstruction svInstr = getMatchInstructionForSV(psv); + return svInstr::match; + } + if (pe instanceof SchemaVariable) { + return null; // other schema variables in programs: be safe, fall back + } + if (!(pe instanceof ProgramElement progEl)) { + return null; + } + if (SyntaxElementMatchProgramGenerator.isGenericMatch(progEl)) { + final int childCount = pe.getChildCount(); + final ProgStep[] subs = new ProgStep[childCount]; + boolean fixedStructure = true; + for (int i = 0; i < childCount; i++) { + final ProgStep s = compileProgram(pe.getChild(i)); + if (s == null) { + fixedStructure = false; // e.g. a list SV child -> not one-to-one + break; + } + subs[i] = s; + } + if (fixedStructure) { + final Class kind = pe.getClass(); + return (actual, mc, services) -> { + if (actual.getClass() != kind || actual.getChildCount() != childCount) { + return null; + } + MatchResultInfo r = mc; + for (int i = 0; i < childCount; i++) { + r = subs[i].match(actual.getChild(i), r, services); + if (r == null) { + return null; + } + } + return r; + }; + } + } + // an element with its own match (value literals, TypeRef, SchematicFieldReference, + // VariableSpecification, loops, ...) or a generic element with variable-arity children + // (a list SV): reuse its own match cursor-free (see delegateToMatch) + return delegateToMatch(progEl); + } + + /** + * Matches {@code progEl} by reusing its own {@code match(SourceData, MatchConditions)} + * cursor-free, exactly as {@code MatchProgramInstruction} does at the program root. This keeps + * the surrounding program compiled (only this sub-element delegates) and is + * behaviour-preserving + * by construction: it is the very match the interpreter would call, including the + * {@code matchChildren} handling of list schema variables. + */ + private static ProgStep delegateToMatch(ProgramElement progEl) { + return (actual, mc, services) -> progEl.match( + new SourceData((ProgramElement) actual, -1, (Services) services), (MatchConditions) mc); + } + + /** + * Compiles the active statements of a context block (its children from the active offset, i.e. + * skipping the execution context if present), or returns {@code null} if any active statement + * uses a construct the compiler does not handle. + */ + private static ProgStep @Nullable [] compileActiveStatements(ContextStatementBlock csb) { + final int offset = csb.getExecutionContext() == null ? 0 : 1; + final ProgStep[] active = new ProgStep[csb.getChildCount() - offset]; + for (int i = offset, n = csb.getChildCount(); i < n; i++) { + final ProgStep s = compileProgram(csb.getChildAt(i)); + if (s == null) { + return null; + } + active[i - offset] = s; + } + return active; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java index 68980ac244a..167623ddb8f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaProgramMatchHook.java @@ -20,7 +20,7 @@ * program instruction ({@link SyntaxElementMatchProgramGenerator#buildProgramInstruction}, falling * back to the monolithic {@code MatchProgramInstruction} when conversion is off or unavailable); * the - * compiled side reuses {@link CompiledMatchProgram#compiledProgramMatcher} (context-block phases + + * compiled side reuses {@link JavaProgramCompiler#compiledProgramMatcher} (context-block phases + * generic {@code getChild} navigation + value-leaf / list-SV delegation). Both are lifted verbatim * from the hand-written matchers, so the framework reproduces them exactly. */ @@ -47,7 +47,7 @@ private JavaProgramMatchHook(JavaProgramElement prog, boolean programInstruction */ public static @Nullable JavaProgramMatchHook of(JavaProgramElement prog, boolean programInstructions) { - final MatchProgram compiled = CompiledMatchProgram.compiledProgramMatcher(prog); + final MatchProgram compiled = JavaProgramCompiler.compiledProgramMatcher(prog); if (compiled == null) { return null; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java index 3cc12b13449..0afdb914e3c 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/SyntaxElementMatchProgramGenerator.java @@ -13,33 +13,28 @@ import de.uka.ilkd.key.java.ast.JavaProgramElement; import de.uka.ilkd.key.java.ast.ProgramElement; import de.uka.ilkd.key.java.ast.SourceData; -import de.uka.ilkd.key.logic.GenericArgument; -import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.logic.op.*; -import de.uka.ilkd.key.logic.sort.GenericSort; -import de.uka.ilkd.key.logic.sort.ParametricSortInstance; import de.uka.ilkd.key.rule.MatchConditions; import de.uka.ilkd.key.rule.match.vm.instructions.MatchContextStatementBlockInstruction; import de.uka.ilkd.key.rule.match.vm.instructions.MatchProgramElementInstruction; import de.uka.ilkd.key.rule.match.vm.instructions.MatchSubProgramInstruction; import org.key_project.logic.SyntaxElement; -import org.key_project.logic.op.Modality; -import org.key_project.logic.op.Operator; -import org.key_project.logic.op.QuantifiableVariable; import org.key_project.logic.op.sv.SchemaVariable; import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; -import org.key_project.prover.rules.matcher.vm.instruction.MatchInstruction; import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; -import org.key_project.util.collection.ImmutableArray; import org.jspecify.annotations.Nullable; import static de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet.*; /** - * This class generates a matching program for a given syntax element that can be - * interpreted by the virtual machine's interpreter + * Converts the Java program of a modality into VM match-instructions ({@link VMInstruction}s) by + * direct tree navigation, for the interpreter side of the match-plan framework (used by the Java + * {@link org.key_project.prover.rules.matcher.compiler.ProgramMatchHook}). The term skeleton itself + * is built by {@link JavaMatchPlanBuilder}; this class only handles the program-element conversion + * (generic structural elements and non-list program schema variables; anything else is matched by + * the monolithic {@code MatchProgramInstruction}). * * @see org.key_project.prover.rules.matcher.vm.VMProgramInterpreter */ @@ -63,128 +58,6 @@ public class SyntaxElementMatchProgramGenerator { */ private static final Map, Boolean> GENERIC_MATCH = new ConcurrentHashMap<>(); - /** - * creates a matcher for the given pattern - * - * @param pattern the {@link JTerm} specifying the pattern - * @return the specialized matcher for the given pattern - */ - public static VMInstruction[] createProgram(JTerm pattern) { - return createProgram(pattern, Boolean.getBoolean(PROGRAM_INSTRUCTIONS_PROPERTY)); - } - - /** - * creates a matcher for the given pattern, choosing explicitly whether the Java program of a - * modality is matched by converted {@link VMInstruction} sub-programs ({@code true}) or by the - * monolithic {@code MatchProgramInstruction} ({@code false}). The production path uses - * {@link #createProgram(JTerm)} which reads the {@code key.matcher.programInstructions} flag; - * this overload exists mainly to build both variants in one JVM for differential testing. - * - * @param pattern the {@link JTerm} specifying the pattern - * @param programInstructions whether to convert program matching to VM sub-programs - * @return the specialized matcher for the given pattern - */ - public static VMInstruction[] createProgram(JTerm pattern, boolean programInstructions) { - ArrayList program = new ArrayList<>(); - createProgram(pattern, program, programInstructions); - return program.toArray(new VMInstruction[0]); - } - - /** - * creates a matching program for the given pattern. It appends the necessary match instruction - * to the given list of instructions - * - * @param pattern the {@link JTerm} used as pattern for which to create a matcher - * @param program the list of {@link MatchInstruction} to which the instructions for matching - * {@code pattern} are added. - * @param programInstructions whether to convert program matching to VM sub-programs - */ - private static void createProgram(JTerm pattern, ArrayList program, - boolean programInstructions) { - final Operator op = pattern.op(); - - final ImmutableArray boundVars = pattern.boundVars(); - - if (!boundVars.isEmpty()) { - program.add(matchAndBindVariables(boundVars)); - } - - if (pattern.hasLabels()) { - program.add(matchTermLabelSV(pattern.getLabels())); - } - - if (op instanceof SchemaVariable sv) { - program.add(getMatchInstructionForSV(sv)); - program.add(gotoNextSiblingInstruction()); - } else { - program.add(getCheckNodeKindInstruction(JTerm.class)); - program.add(gotoNextInstruction()); - switch (op) { - case ParametricFunctionInstance pfi -> { - program.add(getCheckNodeKindInstruction(ParametricFunctionInstance.class)); - program.add(getSimilarParametricFunctionInstruction(pfi)); - program.add(gotoNextInstruction()); - for (int i = 0; i < pfi.getChildCount(); i++) { - var arg = (GenericArgument) pfi.getChild(i); - if (arg.sort() instanceof GenericSort gs) { - program.add(getMatchGenericSortInstruction(gs)); - } else if (arg.sort() instanceof ParametricSortInstance) { - throw new UnsupportedOperationException( - "TODO @ DD: Parametric sort in generic args!"); - } else { - program.add(getMatchIdentityInstruction(arg)); - } - program.add(gotoNextInstruction()); - } - } - case ElementaryUpdate elUp -> { - program.add(getCheckNodeKindInstruction(ElementaryUpdate.class)); - program.add(gotoNextInstruction()); - if (elUp.lhs() instanceof SchemaVariable sv) { - program.add(getMatchInstructionForSV(sv)); - program.add(gotoNextSiblingInstruction()); - } else if (elUp.lhs() instanceof LocationVariable locVar) { - program.add(getMatchIdentityInstruction(locVar)); - program.add(gotoNextInstruction()); - } - } - case Modality mod -> { - program.add(getCheckNodeKindInstruction(Modality.class)); - program.add(gotoNextInstruction()); - if (mod.kind() instanceof ModalOperatorSV modKindSV) { - program.add(matchModalOperatorSV(modKindSV)); - } else { - program.add(getMatchIdentityInstruction(mod.kind())); - } - program.add(gotoNextInstruction()); - final JavaProgramElement prog = pattern.javaBlock().program(); - final VMInstruction progInstr = - programInstructions ? buildProgramInstruction(prog) : null; - program.add(progInstr != null ? progInstr : matchProgram(prog)); - program.add(gotoNextSiblingInstruction()); - } - default -> { - program.add(getMatchIdentityInstruction(op)); - program.add(gotoNextInstruction()); - } - } - } - - if (!boundVars.isEmpty()) { - for (int i = 0; i < boundVars.size(); i++) { - program.add(gotoNextSiblingInstruction()); - } - } - - for (int i = 0; i < pattern.arity(); i++) { - createProgram(pattern.sub(i), program, programInstructions); - } - - if (!boundVars.isEmpty()) { - program.add(unbindVariables(boundVars)); - } - } - /** * Builds the instruction matching the Java program {@code prog} of a modality by direct tree * navigation, or returns {@code null} if {@code prog} uses a construct the converter does not diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java index 808001d2ae2..49d29c6843c 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramTest.java @@ -14,6 +14,7 @@ import org.key_project.logic.Name; import org.key_project.prover.rules.instantiation.MatchResultInfo; +import org.key_project.prover.rules.matcher.vm.MatchProgram; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -24,12 +25,12 @@ import static org.junit.jupiter.api.Assertions.assertTrue; /** - * Unit tests for the cursor-free {@link CompiledMatchProgram} find-matcher, the compiled - * counterpart of {@link VMTacletMatcherTest} (which covers the interpreter). For the same taclets - * and the same matching / non-matching terms it asserts that the compiled matcher produces the - * expected result -- success with the expected schema-variable instantiations, and {@code null} on - * the failure cases -- so the compiled path is checked independently of the differential test, and - * in particular against explicit expectations rather than only against the interpreter. + * Unit tests for the cursor-free compiled find-matcher built by the match-plan framework + * ({@link JavaMatchPlanBuilder#compiledProgram}), the compiled counterpart of + * {@link VMTacletMatcherTest} (which covers the interpreter). For the same taclets and the same + * matching / non-matching terms it asserts that the compiled matcher produces the expected result + * -- success with the expected schema-variable instantiations, and {@code null} on the failure + * cases -- so the compiled path is checked against explicit expectations. * *

* Coverage focuses on term-level matching (propositional / function patterns) and, importantly, @@ -63,20 +64,20 @@ private static FindTaclet findTaclet(ProofAggregate pa, String name) { return (FindTaclet) t; } - /** compiles the find expression; the taclets here are all within the compiler's coverage. */ - private static CompiledMatchProgram compile(FindTaclet t) { - final CompiledMatchProgram p = CompiledMatchProgram.compile((JTerm) t.find()); + /** compiles the find expression; the taclets here are all within the framework's coverage. */ + private static MatchProgram compile(FindTaclet t) { + final MatchProgram p = JavaMatchPlanBuilder.compiledProgram((JTerm) t.find()); assertNotNull(p, "find pattern of " + t.name() + " was expected to compile"); return p; } - private MatchResultInfo match(CompiledMatchProgram p, String term) throws ParserException { + private MatchResultInfo match(MatchProgram p, String term) throws ParserException { return p.match(services.getTermBuilder().parseTerm(term), EMPTY, services); } @Test public void compiledPropositionalMatching() throws ParserException { - final CompiledMatchProgram p = compile(propositional); + final MatchProgram p = compile(propositional); final JTerm toMatch = services.getTermBuilder().parseTerm("A & B"); final MatchResultInfo mc = p.match(toMatch, EMPTY, services); @@ -97,7 +98,7 @@ public void compiledPropositionalMatching() throws ParserException { @Test public void compiledFunctionMatching() throws ParserException { - final CompiledMatchProgram p = compile(function); + final MatchProgram p = compile(function); for (String matching : new String[] { "f(1, 1, 2)", "f(c, c, d)" }) { assertNotNull(match(p, matching), "compiled matcher should match " + matching); @@ -111,7 +112,7 @@ public void compiledFunctionMatching() throws ParserException { @Test public void compiledBoundVariableMatching() throws ParserException { - final CompiledMatchProgram p = compile(binder); + final MatchProgram p = compile(binder); assertNotNull(match(p, "\\forall int x; x + 1 > 0"), "compiled matcher should match the bound-variable pattern"); From 5b11b828f2720a38fbd24924dfbc249d5de3cbfd Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 02:11:38 +0200 Subject: [PATCH 11/26] test(dev): drop the oracle-based differential from the PR; framework-only benchmark The differential test and the context benchmark depend on the hand-written matchers as an independent oracle, which no longer exist in this branch. They are retained on a separate development branch (with the reference interpreter) as the regression net, and removed here. CompiledMatchProgramBenchmark is retargeted to compare the framework's interpreter vs its compiled matcher (no oracle). --- .../vm/CompiledMatchProgramBenchmark.java | 96 ++---- .../rule/match/vm/ContextMatchBenchmark.java | 261 ----------------- .../vm/ProgramMatchDifferentialTest.java | 277 ------------------ 3 files changed, 25 insertions(+), 609 deletions(-) delete mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java delete mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java index 43693238211..4f01ffafe5e 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/CompiledMatchProgramBenchmark.java @@ -26,19 +26,10 @@ import static org.junit.jupiter.api.Assertions.assertEquals; /** - * Isolated micro-benchmark for the find matcher (no taclet index, strategy or proof pipeline), over - * the subset of the real taclet base that the compiler handles. It serves two purposes: - * - *

    - *
  1. the headline comparison {@link VMProgramInterpreter} vs {@link CompiledMatchProgram} (the - * cursor-free win), and
  2. - *
  3. the no-overhead check for the match-plan framework: the matchers built through - * {@link JavaMatchPlanBuilder} (one description, two back-ends) are timed alongside the - * hand-written - * ones. Since the plan is lowered once at construction to the same {@code VMInstruction[]} / - * cursor-free closures, the framework-built matchers must run at parity with the hand-written ones - * (the IR adds no per-match cost).
  4. - *
+ * Isolated micro-benchmark for the find matcher (no taclet index, strategy or proof pipeline): it + * compares the interpreter ({@link VMProgramInterpreter} over the framework's instruction program) + * against the cursor-free compiled matcher, both built by the match-plan framework + * ({@link JavaMatchPlanBuilder}), over the real taclet base. * *

* By default it runs on the self-contained {@code tacletMatch1.key}. Point it at a wider set (e.g. @@ -57,12 +48,7 @@ public class CompiledMatchProgramBenchmark { "1 + 2 * 3 = 7", "\\forall int x; \\forall int y; (x + y = y + x)" }; - /** - * the four matchers built per compilable find-taclet: hand-written and framework, each - * back-end. - */ - private record Task(List handInterps, List handComps, - List planInterps, List planComps, + private record Task(List interps, List comps, List corpus, Services services) { } @@ -82,52 +68,32 @@ public void benchmarkInterpreterVsCompiled() { // warmup for (int pass = 0; pass < 5; pass++) { for (Task t : tasks) { - run(t.handInterps, t); - run(t.handComps, t); - run(t.planInterps, t); - run(t.planComps, t); + run(t.interps, t); + run(t.comps, t); } } // timed: alternate phases per pass to average out JIT / cache effects final int passes = 30; - long handInterpN = 0, handCompN = 0, planInterpN = 0, planCompN = 0; - long handInterpM = 0, handCompM = 0, planInterpM = 0, planCompM = 0; + long interpMatches = 0, compMatches = 0, interpNanos = 0, compNanos = 0; for (int pass = 0; pass < passes; pass++) { for (Task t : tasks) { long t0 = System.nanoTime(); - handInterpM += run(t.handInterps, t); - handInterpN += System.nanoTime() - t0; - - t0 = System.nanoTime(); - planInterpM += run(t.planInterps, t); - planInterpN += System.nanoTime() - t0; + interpMatches += run(t.interps, t); + interpNanos += System.nanoTime() - t0; t0 = System.nanoTime(); - handCompM += run(t.handComps, t); - handCompN += System.nanoTime() - t0; - - t0 = System.nanoTime(); - planCompM += run(t.planComps, t); - planCompN += System.nanoTime() - t0; + compMatches += run(t.comps, t); + compNanos += System.nanoTime() - t0; } } - System.out.printf("[isolated matcher, %d problem(s)]%n", tasks.size()); - System.out.printf( - " interpreter : hand-written=%.1f ms framework=%.1f ms (overhead %+.1f%%)%n", - handInterpN / 1e6, planInterpN / 1e6, - 100.0 * (planInterpN - handInterpN) / handInterpN); - System.out.printf( - " compiled : hand-written=%.1f ms framework=%.1f ms (overhead %+.1f%%)%n", - handCompN / 1e6, planCompN / 1e6, 100.0 * (planCompN - handCompN) / handCompN); - System.out.printf(" speedup (framework compiled vs framework interpreter) = %.2fx%n", - (double) planInterpN / planCompN); - // all four matchers must see exactly the same matches - assertEquals(handInterpM, handCompM, "hand-written back-ends must agree on #matches"); - assertEquals(handInterpM, planInterpM, - "framework interpreter must agree with hand-written"); - assertEquals(handInterpM, planCompM, "framework compiled must agree with hand-written"); + System.out.printf("[isolated matcher, %d problem(s)] interpreter=%.1f ms compiled=%.1f ms" + + " speedup=%.2fx (matches interp=%d comp=%d)%n", + tasks.size(), interpNanos / 1e6, compNanos / 1e6, + (double) interpNanos / compNanos, interpMatches / passes, compMatches / passes); + assertEquals(interpMatches, compMatches, + "compiled and interpreter must agree on the number of matches"); } private static List problemPaths() { @@ -160,10 +126,8 @@ private static Task buildTask(String pathStr) { } } - final List handInterps = new ArrayList<>(); - final List handComps = new ArrayList<>(); - final List planInterps = new ArrayList<>(); - final List planComps = new ArrayList<>(); + final List interps = new ArrayList<>(); + final List comps = new ArrayList<>(); int findTaclets = 0; for (Taclet t : pa.getFirstProof().getInitConfig().activatedTaclets()) { if (!(t instanceof FindTaclet ft)) { @@ -171,22 +135,12 @@ private static Task buildTask(String pathStr) { } findTaclets++; final JTerm find = (JTerm) ft.find(); - final MatchProgram handComp = CompiledMatchProgram.compile(find); - final MatchProgram planComp = JavaMatchPlanBuilder.compiledProgram(find); - if (handComp == null || planComp == null) { - continue; // restrict to the compilable subset, identical for both - } - handComps.add(handComp); - planComps.add(planComp); - handInterps.add( - new VMProgramInterpreter(SyntaxElementMatchProgramGenerator.createProgram(find))); - planInterps.add( - new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(find))); + comps.add(JavaMatchPlanBuilder.compiledProgram(find)); + interps.add(new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(find))); } - System.out.printf(" %-22s findTaclets=%4d compilable=%4d (%2.0f%%) corpus=%d%n", - path.getFileName(), findTaclets, handComps.size(), - findTaclets == 0 ? 0 : 100.0 * handComps.size() / findTaclets, corpus.size()); - return new Task(handInterps, handComps, planInterps, planComps, corpus, services); + System.out.printf(" %-22s findTaclets=%4d corpus=%d%n", + path.getFileName(), findTaclets, corpus.size()); + return new Task(interps, comps, corpus, services); } private static long run(List programs, Task t) { diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java deleted file mode 100644 index 17a0c14e470..00000000000 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ContextMatchBenchmark.java +++ /dev/null @@ -1,261 +0,0 @@ -/* This file is part of KeY - https://key-project.org - * KeY is licensed under the GNU General Public License Version 2 - * SPDX-License-Identifier: GPL-2.0-only */ -package de.uka.ilkd.key.rule.match.vm; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import de.uka.ilkd.key.control.DefaultUserInterfaceControl; -import de.uka.ilkd.key.control.KeYEnvironment; -import de.uka.ilkd.key.java.Services; -import de.uka.ilkd.key.logic.JTerm; -import de.uka.ilkd.key.proof.Node; -import de.uka.ilkd.key.proof.Proof; -import de.uka.ilkd.key.rule.FindTaclet; -import de.uka.ilkd.key.rule.MatchConditions; -import de.uka.ilkd.key.rule.Taclet; -import de.uka.ilkd.key.util.HelperClassForTests; -import de.uka.ilkd.key.util.ProofStarter; - -import org.key_project.logic.op.Modality; -import org.key_project.prover.rules.instantiation.MatchResultInfo; -import org.key_project.prover.rules.matcher.vm.MatchProgram; -import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; -import org.key_project.prover.sequent.SequentFormula; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Isolated micro-benchmark for the program (symbolic-execution) find matcher: it compares the - * cursor-based interpreter ({@link VMProgramInterpreter}) against the cursor-free compiled matcher - * ({@link CompiledMatchProgram}), over the subset of program-bearing taclets that the compiler - * handles (modality / context-block patterns; step 3). Both are built from the same find term and - * run directly (no taclet index, strategy or proof pipeline), so this measures only the matcher. - * - *

- * The corpus is harvested by running a bounded amount of symbolic execution on a real proof and - * collecting the modality sub-terms (the redex candidates that drive program matching). By - * default it runs on {@code proofStarter/CC/project.key}; point it at any problem with - * {@code -Dbench.problems=/abs/a.key,/abs/b.key} (e.g. a straight-line problem), bound the harvest - * with {@code -Dbench.steps=N} and the timed passes with {@code -Dbench.passes=N}. Run with - * {@code ./gradlew :key.core:test --tests *ContextMatchBenchmark}. - */ -public class ContextMatchBenchmark { - - private static final MatchResultInfo EMPTY = MatchConditions.EMPTY_MATCHCONDITIONS; - - private static final int STEPS = Integer.getInteger("bench.steps", 6000); - private static final int PASSES = Integer.getInteger("bench.passes", 30); - - private record Task(List interp, List compiled, - List corpus, Services services, String label, int programTaclets, - int[] deepProg, int[] deepTerm) { - } - - @Test - public void benchmarkInterpreterVsCompiled() throws Exception { - final List> envs = new ArrayList<>(); - final List tasks = new ArrayList<>(); - try { - for (String p : problemPaths()) { - final Path path = Path.of(p.trim()); - if (!Files.exists(path)) { - System.out.println(" (skip, not found) " + path); - continue; - } - final KeYEnvironment env = - KeYEnvironment.load(path, null, null, null); - envs.add(env); - tasks.add(buildTask(env, path.getFileName().toString())); - } - if (tasks.isEmpty()) { - return; - } - - // warmup - for (int pass = 0; pass < 5; pass++) { - for (Task t : tasks) { - run(t.interp, t); - run(t.compiled, t); - runDeep(t.interp, t); - runDeep(t.compiled, t); - } - } - - // (A) mixed sweep: every compilable taclet x every modality term (mostly fail-fast, - // the common case in real proving); (B) focused on the deep/matching pairs. - long interpMatches = 0, compMatches = 0, interpNanos = 0, compNanos = 0; - long interpDeepNanos = 0, compDeepNanos = 0; - for (int pass = 0; pass < PASSES; pass++) { - for (Task t : tasks) { - long t0 = System.nanoTime(); - interpMatches += run(t.interp, t); - interpNanos += System.nanoTime() - t0; - - t0 = System.nanoTime(); - compMatches += run(t.compiled, t); - compNanos += System.nanoTime() - t0; - - t0 = System.nanoTime(); - runDeep(t.interp, t); - interpDeepNanos += System.nanoTime() - t0; - - t0 = System.nanoTime(); - runDeep(t.compiled, t); - compDeepNanos += System.nanoTime() - t0; - } - } - - int deepPairs = 0; - for (Task t : tasks) { - deepPairs += t.deepProg.length; - System.out.printf( - " %-26s programTaclets=%d compilable=%d modalityCorpus=%d deepPairs=%d%n", - t.label, t.programTaclets, t.interp.size(), t.corpus.size(), t.deepProg.length); - } - System.out.printf( - "[program matcher, %d task(s), %d passes]%n" - + " (A) mixed sweep interpreter=%.1f ms compiled=%.1f ms speedup=%.2fx%n" - + " (B) deep matches interpreter=%.1f ms compiled=%.1f ms speedup=%.2fx" - + " (%d pairs/pass)%n", - tasks.size(), PASSES, - interpNanos / 1e6, compNanos / 1e6, (double) interpNanos / compNanos, - interpDeepNanos / 1e6, compDeepNanos / 1e6, - (double) interpDeepNanos / compDeepNanos, deepPairs); - assertEquals(interpMatches, compMatches, - "interpreter and compiled matcher must agree on the number of matches"); - } finally { - for (KeYEnvironment env : envs) { - env.dispose(); - } - } - } - - private static Task buildTask(KeYEnvironment env, String label) { - final Proof proof = env.getLoadedProof(); - final Services services = proof.getServices(); - - final ProofStarter ps = new ProofStarter(false); - ps.init(proof); - ps.setMaxRuleApplications(STEPS); - ps.start(); - - final List corpus = harvestModalityCorpus(proof); - - final List interp = new ArrayList<>(); - final List compiled = new ArrayList<>(); - int programTaclets = 0; - for (Taclet t : proof.getInitConfig().activatedTaclets()) { - if (!(t instanceof FindTaclet ft) || !(ft.find() instanceof JTerm find) - || !containsModality(find)) { - continue; - } - programTaclets++; - final CompiledMatchProgram comp = CompiledMatchProgram.compile(find); - if (comp == null) { - continue; // not compilable -> would use the interpreter in production - } - // oracle interpreter for the same find (programInstructions=false: monolithic - // MatchProgramInstruction, the current production interpreter path) - interp.add( - new VMProgramInterpreter( - SyntaxElementMatchProgramGenerator.createProgram(find, false))); - compiled.add(comp); - } - - // collect the (program, term) pairs that actually match -- the deep matches that exercise - // the program/context walk (the mixed sweep is >99% fail-fast and hides them) - final List deep = new ArrayList<>(); - for (int p = 0, np = interp.size(); p < np; p++) { - for (int i = 0, n = corpus.size(); i < n; i++) { - if (interp.get(p).match(corpus.get(i), EMPTY, services) != null) { - deep.add(new int[] { p, i }); - } - } - } - final int[] deepProg = new int[deep.size()]; - final int[] deepTerm = new int[deep.size()]; - for (int k = 0; k < deep.size(); k++) { - deepProg[k] = deep.get(k)[0]; - deepTerm[k] = deep.get(k)[1]; - } - return new Task(interp, compiled, corpus, services, label, programTaclets, deepProg, - deepTerm); - } - - private static long run(List progs, Task t) { - long matches = 0; - for (int p = 0, np = progs.size(); p < np; p++) { - final MatchProgram prog = progs.get(p); - for (int i = 0, n = t.corpus.size(); i < n; i++) { - if (prog.match(t.corpus.get(i), EMPTY, t.services) != null) { - matches++; - } - } - } - return matches; - } - - /** runs only the (program, term) pairs that match -- isolates the deep program/context walk. */ - private static long runDeep(List progs, Task t) { - long matches = 0; - for (int k = 0, n = t.deepProg.length; k < n; k++) { - if (progs.get(t.deepProg[k]).match(t.corpus.get(t.deepTerm[k]), EMPTY, - t.services) != null) { - matches++; - } - } - return matches; - } - - private static List problemPaths() { - final String prop = System.getProperty("bench.problems"); - if (prop != null && !prop.isBlank()) { - return List.of(prop.split(",")); - } - return List.of(HelperClassForTests.TESTCASE_DIRECTORY - .resolve("proofStarter/CC/project.key").toString()); - } - - /** harvests the deduplicated modality sub-terms (redex candidates) from every proof node. */ - private static List harvestModalityCorpus(Proof proof) { - final Set seen = new LinkedHashSet<>(); - final Iterator nodes = proof.root().subtreeIterator(); - while (nodes.hasNext()) { - final Node n = nodes.next(); - for (SequentFormula sf : n.sequent()) { - collectModalities((JTerm) sf.formula(), seen); - } - } - return new ArrayList<>(seen); - } - - private static void collectModalities(JTerm t, Set out) { - if (t.op() instanceof Modality) { - out.add(t); - } - for (int i = 0, n = t.arity(); i < n; i++) { - collectModalities(t.sub(i), out); - } - } - - private static boolean containsModality(JTerm t) { - if (t.op() instanceof Modality) { - return true; - } - for (int i = 0, n = t.arity(); i < n; i++) { - if (containsModality(t.sub(i))) { - return true; - } - } - return false; - } -} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java b/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java deleted file mode 100644 index f292028fc20..00000000000 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/match/vm/ProgramMatchDifferentialTest.java +++ /dev/null @@ -1,277 +0,0 @@ -/* This file is part of KeY - https://key-project.org - * KeY is licensed under the GNU General Public License Version 2 - * SPDX-License-Identifier: GPL-2.0-only */ -package de.uka.ilkd.key.rule.match.vm; - -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashSet; -import java.util.List; -import java.util.Set; - -import de.uka.ilkd.key.control.DefaultUserInterfaceControl; -import de.uka.ilkd.key.control.KeYEnvironment; -import de.uka.ilkd.key.java.Services; -import de.uka.ilkd.key.logic.JTerm; -import de.uka.ilkd.key.proof.Node; -import de.uka.ilkd.key.proof.Proof; -import de.uka.ilkd.key.rule.FindTaclet; -import de.uka.ilkd.key.rule.MatchConditions; -import de.uka.ilkd.key.rule.Taclet; -import de.uka.ilkd.key.rule.match.vm.instructions.MatchContextStatementBlockInstruction; -import de.uka.ilkd.key.rule.match.vm.instructions.MatchSubProgramInstruction; -import de.uka.ilkd.key.util.HelperClassForTests; -import de.uka.ilkd.key.util.ProofStarter; - -import org.key_project.logic.op.Modality; -import org.key_project.prover.rules.instantiation.MatchResultInfo; -import org.key_project.prover.rules.matcher.compiler.MatchPlan; -import org.key_project.prover.rules.matcher.vm.MatchProgram; -import org.key_project.prover.rules.matcher.vm.VMProgramInterpreter; -import org.key_project.prover.rules.matcher.vm.instruction.VMInstruction; -import org.key_project.prover.sequent.SequentFormula; - -import org.junit.jupiter.api.Test; - -import static org.junit.jupiter.api.Assertions.assertEquals; - -/** - * Differential test / oracle for the matcher work. For every find-taclet of the full Java taclet - * base it builds, in the same JVM, the interpreter oracle - * ({@code key.matcher.programInstructions=false}, modality programs matched by the monolithic - * {@code MatchProgramInstruction} delegating to {@code ProgramElement.match}) and, where - * applicable, - * the converted interpreter ({@code =true}: generic programs via - * {@link MatchSubProgramInstruction}, context blocks via - * {@link MatchContextStatementBlockInstruction}) and the cursor-free compiled matcher - * ({@link CompiledMatchProgram}, incl. modality / context-block / bound-variable patterns). All are - * run over a corpus of terms harvested from a real proof and asserted to produce identical results - * (match success/failure and the resulting instantiations, including the context-block - * prefix/suffix - * instantiation). - * - *

- * This guards the converted and compiled matchers against the interpreter at the unit-test level. - * The complementary end-to-end check is identical proof statistics (nodes / branches / rule - * applications) for a full {@code --auto} proof with the flag on vs off (the CLI - * {@code .auto.proof} - * stores only the problem, not the proof tree, so a file diff is not a valid replay check). - */ -public class ProgramMatchDifferentialTest { - - private static final MatchResultInfo EMPTY = MatchConditions.EMPTY_MATCHCONDITIONS; - - /** the symbolic-execution proof whose terms form the matching corpus. */ - private static final Path CORPUS_PROOF = - HelperClassForTests.TESTCASE_DIRECTORY.resolve("proofStarter/CC/project.key"); - - /** cap on symbolic-execution steps run to harvest the corpus (keeps the test fast). */ - private static final int CORPUS_STEPS = 6000; - - @Test - public void convertedMatchesInterpreter() throws Exception { - final KeYEnvironment env = - KeYEnvironment.load(CORPUS_PROOF, null, null, null); - try { - final Proof proof = env.getLoadedProof(); - final Services services = proof.getServices(); - - // run a bounded amount of symbolic execution to populate the proof tree with terms at - // many execution stages (method frames, peeled blocks, loops, ...) - final ProofStarter ps = new ProofStarter(false); - ps.init(proof); - ps.setMaxRuleApplications(CORPUS_STEPS); - ps.start(); - - final List corpus = harvestCorpus(proof); - - int findTaclets = 0; - int programTaclets = 0; - int convertedContext = 0; - int convertedGeneric = 0; - int compiledTaclets = 0; - int compiledBoundVar = 0; - int planTaclets = 0; - long matches = 0; - int comparisons = 0; - for (Taclet t : proof.getInitConfig().activatedTaclets()) { - if (!(t instanceof FindTaclet ft) || !(ft.find() instanceof JTerm find)) { - continue; - } - findTaclets++; - final boolean program = containsModality(find); - final VMProgramInterpreter oracle = new VMProgramInterpreter( - SyntaxElementMatchProgramGenerator.createProgram(find, false)); - // the cursor-free compiled matcher; null if not (yet) compilable - final CompiledMatchProgram compiled = CompiledMatchProgram.compile(find); - if (compiled != null) { - compiledTaclets++; - if (containsBoundVars(find)) { - compiledBoundVar++; - } - } - // the unified match-plan framework (both back-ends from one description); null for - // constructs not yet migrated to the dispatch - final MatchPlan plan = JavaMatchPlanBuilder.buildPlan(find, false); - VMProgramInterpreter planInterp = null; - MatchProgram planCompiled = null; - if (plan != null) { - planTaclets++; - final List planInstr = new ArrayList<>(); - plan.emitInstructions(planInstr); - planInterp = new VMProgramInterpreter(planInstr.toArray(new VMInstruction[0])); - planCompiled = plan.compile(); - } - // also verify the plan's interpreter with program-instruction conversion ON - // (production reads key.matcher.programInstructions; the plan must agree for both) - final MatchPlan planConv = JavaMatchPlanBuilder.buildPlan(find, true); - VMProgramInterpreter planConvInterp = null; - if (planConv != null) { - final List planConvInstr = new ArrayList<>(); - planConv.emitInstructions(planConvInstr); - planConvInterp = - new VMProgramInterpreter(planConvInstr.toArray(new VMInstruction[0])); - } - // the converted interpreter (programInstructions=true) only differs for programs - VMProgramInterpreter converted = null; - if (program) { - programTaclets++; - final VMInstruction[] convertedProg = - SyntaxElementMatchProgramGenerator.createProgram(find, true); - if (contains(convertedProg, MatchContextStatementBlockInstruction.class)) { - convertedContext++; - } - if (contains(convertedProg, MatchSubProgramInstruction.class)) { - convertedGeneric++; - } - converted = new VMProgramInterpreter(convertedProg); - } - - for (JTerm term : corpus) { - final MatchResultInfo oracleRes = oracle.match(term, EMPTY, services); - comparisons++; - if (converted != null) { - assertSameResult(t, term, oracleRes, - converted.match(term, EMPTY, services)); - } - if (compiled != null) { - assertSameResult(t, term, oracleRes, compiled.match(term, EMPTY, services)); - } - if (plan != null) { - assertSameResult(t, term, oracleRes, - planInterp.match(term, EMPTY, services)); - assertSameResult(t, term, oracleRes, - planCompiled.match(term, EMPTY, services)); - } - if (planConv != null) { - assertSameResult(t, term, oracleRes, - planConvInterp.match(term, EMPTY, services)); - } - if (oracleRes != null) { - matches++; - } - } - } - - System.out.printf( - "[program-match differential] findTaclets=%d programTaclets=%d convertedContext=%d " - + "convertedGeneric=%d compiled=%d (boundVar=%d) plan=%d corpus=%d " - + "comparisons=%d matches=%d%n", - findTaclets, programTaclets, convertedContext, convertedGeneric, compiledTaclets, - compiledBoundVar, planTaclets, corpus.size(), comparisons, matches); - // sanity floor: the run must actually exercise the unified match-plan framework - assertEquals(true, planTaclets > 0, - "expected at least some taclets to be built by the match-plan framework"); - // sanity floor: the run must actually exercise the step-2 context-block conversion - assertEquals(true, convertedContext > 0, - "expected at least some taclets to use the converted context-block matcher"); - // sanity floor: the run must actually exercise the compiled program matcher (step 3) - assertEquals(true, compiledTaclets > 0, - "expected at least some program taclets to compile"); - // sanity floor: the run must actually exercise compiled bound-variable matching - assertEquals(true, compiledBoundVar > 0, - "expected at least some bound-variable taclets to compile"); - } finally { - env.dispose(); - } - } - - /** asserts that oracle and converted matcher agree (success/failure and instantiations). */ - private static void assertSameResult(Taclet t, JTerm term, MatchResultInfo oracle, - MatchResultInfo converted) { - final boolean oracleOk = oracle != null; - final boolean convertedOk = converted != null; - assertEquals(oracleOk, convertedOk, - () -> "match success differs for taclet " + t.name() + " on " + term); - if (oracleOk) { - final var oracleInst = ((MatchConditions) oracle).getInstantiations(); - final var convertedInst = ((MatchConditions) converted).getInstantiations(); - assertEquals(oracleInst, convertedInst, - () -> "instantiations differ for taclet " + t.name() + " on " + term - + "\n oracle: " + oracleInst - + "\n converted: " + convertedInst); - // the context instantiation (prefix/suffix positions) is the critical step-2 output - assertEquals( - String.valueOf(oracleInst.getContextInstantiation()), - String.valueOf(convertedInst.getContextInstantiation()), - () -> "context instantiation differs for taclet " + t.name() + " on " + term); - } - } - - /** collects a deduplicated corpus of subterms from every node of the proof tree. */ - private static List harvestCorpus(Proof proof) { - final Set seen = new LinkedHashSet<>(); - final Iterator nodes = proof.root().subtreeIterator(); - while (nodes.hasNext()) { - final Node n = nodes.next(); - for (SequentFormula sf : n.sequent()) { - collectSubterms((JTerm) sf.formula(), seen); - } - } - return new ArrayList<>(seen); - } - - private static void collectSubterms(JTerm t, Set out) { - out.add(t); - for (int i = 0, n = t.arity(); i < n; i++) { - collectSubterms(t.sub(i), out); - } - } - - /** whether the term tree binds any variable (quantifier, substitution, ...). */ - private static boolean containsBoundVars(JTerm t) { - if (!t.boundVars().isEmpty()) { - return true; - } - for (int i = 0, n = t.arity(); i < n; i++) { - if (containsBoundVars(t.sub(i))) { - return true; - } - } - return false; - } - - /** whether the term tree contains a modality (i.e. carries a Java program). */ - private static boolean containsModality(JTerm t) { - if (t.op() instanceof Modality) { - return true; - } - for (int i = 0, n = t.arity(); i < n; i++) { - if (containsModality(t.sub(i))) { - return true; - } - } - return false; - } - - /** whether the generated (top-level) program contains an instruction of the given kind. */ - private static boolean contains(VMInstruction[] program, Class kind) { - for (VMInstruction instr : program) { - if (kind.isInstance(instr)) { - return true; - } - } - return false; - } -} From f263517afe636b2c6c51f8fe3048dac49a983f89 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 06:03:07 +0200 Subject: [PATCH 12/26] matcher: add a "compiled matcher" feature flag (GUI toggle) Selecting the compiled find-matcher previously needed -Dkey.matcher.compiled, which is awkward to pass through Gradle when trying it out. Expose it as a KeY feature flag (Settings -> Feature Flags) too: - VMTacletMatcher.COMPILED_MATCHER_FEATURE ("MATCHER_COMPILED"); the matcher is selected when the system property OR the feature flag is set. The property is kept for headless / CI (testRAP). - SettingsManager references the flag so it is registered and shown in the feature-settings panel on a fresh start, before any proof is loaded. The flag is read per taclet at construction, so it applies to newly loaded proofs (or after reloading the current one); the panel shows a "reload required" notice (restartRequired = true). No on-the-fly switch of an open proof's matchers. --- .../key/rule/match/vm/VMTacletMatcher.java | 22 +++++++++++++++---- .../key/gui/settings/SettingsManager.java | 10 +++++++++ 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java index 3373d4ebe7d..9276d66a53d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java @@ -17,6 +17,7 @@ import de.uka.ilkd.key.rule.match.TacletMatcherKit; import de.uka.ilkd.key.rule.match.vm.instructions.JavaDLMatchVMInstructionSet; import de.uka.ilkd.key.rule.match.vm.instructions.MatchSchemaVariableInstruction; +import de.uka.ilkd.key.settings.FeatureSettings; import org.key_project.logic.LogicServices; import org.key_project.logic.SyntaxElement; @@ -60,13 +61,25 @@ public class VMTacletMatcher implements TacletMatcher { /** * System property ({@code -Dkey.matcher.compiled=true}) selecting the cursor-free compiled find * matcher (direct term navigation where the pattern allows, interpreter otherwise). Default - * {@code false} keeps the pure interpreter. + * {@code false} keeps the pure interpreter. Mainly for headless / CI runs; in the GUI use the + * {@link #COMPILED_MATCHER_FEATURE} feature flag instead. *

* Read in the constructor (i.e. per taclet, when the taclet base is loaded) rather than once at * class load, so toggling it and reloading the proof switches matchers. */ public static final String COMPILE_MATCHERS_PROPERTY = "key.matcher.compiled"; + /** + * Feature flag (Settings → Feature Flags, persistent) selecting the cursor-free compiled + * find matcher, the GUI-friendly equivalent of {@link #COMPILE_MATCHERS_PROPERTY}. Like the + * property it is read per taclet at construction time, so it takes effect for newly loaded + * proofs + * (or after reloading the current one) -- hence {@code restartRequired = true}. + */ + public static final FeatureSettings.Feature COMPILED_MATCHER_FEATURE = + FeatureSettings.createFeature("MATCHER_COMPILED", + "Use the cursor-free compiled taclet find-matcher (reload the proof to apply).", true); + /** the matcher for the find expression of the taclet */ private final MatchProgram findMatchProgram; /** the matcher for the taclet's assumes formulas */ @@ -111,11 +124,12 @@ public VMTacletMatcher(Taclet taclet) { ignoreTopLevelUpdates = taclet.ignoreTopLevelUpdates() && !(findExp.op() instanceof UpdateApplication); // both back-ends are derived from the unified match-plan framework (one dispatch per - // construct, see JavaMatchPlanBuilder), which falls back to the legacy hand-written - // matchers for the few constructs it does not build yet (term labels) + // construct, see JavaMatchPlanBuilder); the compiled matcher is selected by the system + // property or the feature flag, otherwise the interpreter is used final VMProgramInterpreter interpreter = new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(findExp)); - if (Boolean.getBoolean(COMPILE_MATCHERS_PROPERTY)) { + if (Boolean.getBoolean(COMPILE_MATCHERS_PROPERTY) + || FeatureSettings.isFeatureActivated(COMPILED_MATCHER_FEATURE)) { final MatchProgram compiled = JavaMatchPlanBuilder.compiledProgram(findExp); findMatchProgram = compiled != null ? compiled : interpreter; } else { diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java index de5f4c9cc88..ef81451b2be 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java @@ -20,6 +20,7 @@ import de.uka.ilkd.key.gui.keyshortcuts.ShortcutSettings; import de.uka.ilkd.key.gui.smt.settings.SMTSettingsProvider; import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.match.vm.VMTacletMatcher; import de.uka.ilkd.key.settings.*; import org.slf4j.Logger; @@ -40,6 +41,15 @@ public class SettingsManager { public static final ColorSettingsProvider COLOR_SETTINGS = new ColorSettingsProvider(); public static final FeatureSettingsPanel FEATURE_SETTINGS_PANEL = new FeatureSettingsPanel(); + /** + * Registration anchor: referencing a feature flag declared in a lazily-loaded core class (here + * the compiled-matcher flag in {@link VMTacletMatcher}) forces its registration so it shows in + * the {@link FeatureSettingsPanel} on a fresh start, before any proof is loaded. + */ + @SuppressWarnings("unused") + public static final FeatureSettings.Feature COMPILED_MATCHER_FEATURE = + VMTacletMatcher.COMPILED_MATCHER_FEATURE; + private static SettingsManager INSTANCE; private final List settingsProviders = new LinkedList<>(); From 5158a4737451a9b394f3eb644b1adee9e5e9707a Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Wed, 17 Jun 2026 07:31:00 +0200 Subject: [PATCH 13/26] perf(matcher): enable the compiled find-matcher by default The cursor-free compiled find-matcher is now selected by default; the legacy interpreter becomes an opt-out via -Dkey.matcher.interpreter or the MATCHER_INTERPRETER feature flag (and remains the automatic fallback for patterns the compiler does not handle). Differential testing established the two back-ends are byte-identical, so this only changes which one runs, not any proof. --- .../key/rule/match/vm/VMTacletMatcher.java | 39 ++++++++++--------- .../key/gui/settings/SettingsManager.java | 8 ++-- 2 files changed, 25 insertions(+), 22 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java index 9276d66a53d..36b3f2d2f1c 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java @@ -59,26 +59,28 @@ public class VMTacletMatcher implements TacletMatcher { /** - * System property ({@code -Dkey.matcher.compiled=true}) selecting the cursor-free compiled find - * matcher (direct term navigation where the pattern allows, interpreter otherwise). Default - * {@code false} keeps the pure interpreter. Mainly for headless / CI runs; in the GUI use the - * {@link #COMPILED_MATCHER_FEATURE} feature flag instead. + * System property ({@code -Dkey.matcher.interpreter=true}) forcing the legacy cursor-based + * interpreter find-matcher. The cursor-free compiled matcher is the default; this is mainly for + * headless / CI A/B comparison. In the GUI use the {@link #INTERPRETER_MATCHER_FEATURE} feature + * flag instead. *

* Read in the constructor (i.e. per taclet, when the taclet base is loaded) rather than once at * class load, so toggling it and reloading the proof switches matchers. */ - public static final String COMPILE_MATCHERS_PROPERTY = "key.matcher.compiled"; + public static final String INTERPRETER_MATCHER_PROPERTY = "key.matcher.interpreter"; /** - * Feature flag (Settings → Feature Flags, persistent) selecting the cursor-free compiled - * find matcher, the GUI-friendly equivalent of {@link #COMPILE_MATCHERS_PROPERTY}. Like the + * Feature flag (Settings → Feature Flags, persistent) forcing the legacy interpreter + * find-matcher, the GUI-friendly equivalent of {@link #INTERPRETER_MATCHER_PROPERTY}. The + * compiled matcher is the default; activate this to fall back to the interpreter. Like the * property it is read per taclet at construction time, so it takes effect for newly loaded - * proofs - * (or after reloading the current one) -- hence {@code restartRequired = true}. + * proofs (or after reloading the current one) -- hence {@code restartRequired = true}. */ - public static final FeatureSettings.Feature COMPILED_MATCHER_FEATURE = - FeatureSettings.createFeature("MATCHER_COMPILED", - "Use the cursor-free compiled taclet find-matcher (reload the proof to apply).", true); + public static final FeatureSettings.Feature INTERPRETER_MATCHER_FEATURE = + FeatureSettings.createFeature("MATCHER_INTERPRETER", + "Use the legacy interpreter taclet find-matcher instead of the compiled one " + + "(reload the proof to apply).", + true); /** the matcher for the find expression of the taclet */ private final MatchProgram findMatchProgram; @@ -124,16 +126,17 @@ public VMTacletMatcher(Taclet taclet) { ignoreTopLevelUpdates = taclet.ignoreTopLevelUpdates() && !(findExp.op() instanceof UpdateApplication); // both back-ends are derived from the unified match-plan framework (one dispatch per - // construct, see JavaMatchPlanBuilder); the compiled matcher is selected by the system - // property or the feature flag, otherwise the interpreter is used + // construct, see JavaMatchPlanBuilder); the compiled matcher is the default, the + // interpreter is used only when explicitly selected (property/feature flag) or as the + // automatic fallback for a pattern the compiler does not handle final VMProgramInterpreter interpreter = new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(findExp)); - if (Boolean.getBoolean(COMPILE_MATCHERS_PROPERTY) - || FeatureSettings.isFeatureActivated(COMPILED_MATCHER_FEATURE)) { + if (Boolean.getBoolean(INTERPRETER_MATCHER_PROPERTY) + || FeatureSettings.isFeatureActivated(INTERPRETER_MATCHER_FEATURE)) { + findMatchProgram = interpreter; + } else { final MatchProgram compiled = JavaMatchPlanBuilder.compiledProgram(findExp); findMatchProgram = compiled != null ? compiled : interpreter; - } else { - findMatchProgram = interpreter; } } else { diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java index ef81451b2be..494931f62ea 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/settings/SettingsManager.java @@ -43,12 +43,12 @@ public class SettingsManager { /** * Registration anchor: referencing a feature flag declared in a lazily-loaded core class (here - * the compiled-matcher flag in {@link VMTacletMatcher}) forces its registration so it shows in - * the {@link FeatureSettingsPanel} on a fresh start, before any proof is loaded. + * the interpreter-matcher fallback flag in {@link VMTacletMatcher}) forces its registration so + * it shows in the {@link FeatureSettingsPanel} on a fresh start, before any proof is loaded. */ @SuppressWarnings("unused") - public static final FeatureSettings.Feature COMPILED_MATCHER_FEATURE = - VMTacletMatcher.COMPILED_MATCHER_FEATURE; + public static final FeatureSettings.Feature INTERPRETER_MATCHER_FEATURE = + VMTacletMatcher.INTERPRETER_MATCHER_FEATURE; private static SettingsManager INSTANCE; private final List settingsProviders = new LinkedList<>(); From aa9bbada318b8f1531352778444dc9feec8f4178 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Tue, 16 Jun 2026 03:20:00 +0200 Subject: [PATCH 14/26] perf(strategy): cost reuse across createFurtherApps re-expansion Carry a rule-app container's strategy cost forward across the per-round re-expansion instead of recomputing it, when the taclet's cost is a pure function of the app + find subterm (plus the always-refreshed age term and NonDuplicateApp vetoes). Sound-by-construction, annotation-driven classification (CostLocal/CostNonLocal, default non-local); generator-aware so a composite summing over a sequent-scanning generator stays non-local. A development aid -Dkey.strategy.costReuse.verify recomputes the cost and warns on any mismatch. Byte-identical on the perfTest/perfValidation corpus; ~7% automode speedup on cost-bound proofs. --- .../de/uka/ilkd/key/strategy/CostReuse.java | 236 ++++++++++++++++++ .../key/strategy/ModularJavaDLStrategy.java | 23 +- .../ilkd/key/strategy/TacletAppContainer.java | 49 +++- .../strategy/feature/CheckApplyEqFeature.java | 2 + .../feature/ComprehendedSumFeature.java | 5 + .../strategy/feature/FindRightishFeature.java | 2 + .../feature/InstantiatedSVFeature.java | 2 + .../feature/MatchedAssumesFeature.java | 2 + .../feature/MonomialsSmallerThanFeature.java | 2 + .../feature/NoSelfApplicationFeature.java | 2 + .../feature/TermSmallerThanFeature.java | 2 + .../feature/TrivialMonomialLCRFeature.java | 2 + .../termgenerator/SuperTermGenerator.java | 2 + .../costbased/feature/ConstFeature.java | 1 + .../strategy/costbased/feature/CostLocal.java | 36 +++ .../costbased/feature/CostNonLocal.java | 28 +++ .../costbased/feature/FindDepthFeature.java | 1 + .../costbased/feature/LetFeature.java | 4 + .../costbased/feature/ScaleFeature.java | 1 + .../costbased/feature/ShannonFeature.java | 1 + .../costbased/feature/SumFeature.java | 1 + .../costbased/termfeature/ApplyTFFeature.java | 2 + 22 files changed, 404 insertions(+), 2 deletions(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java create mode 100644 key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostLocal.java create mode 100644 key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostNonLocal.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java new file mode 100644 index 00000000000..dc60c51e883 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java @@ -0,0 +1,236 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.strategy; + +import java.lang.reflect.Array; +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.*; +import java.util.concurrent.ConcurrentHashMap; + +import de.uka.ilkd.key.strategy.feature.AbstractNonDuplicateAppFeature; +import de.uka.ilkd.key.strategy.feature.NonDuplicateAppFeature; +import de.uka.ilkd.key.strategy.feature.RuleSetDispatchFeature; + +import org.key_project.prover.rules.Taclet; +import org.key_project.prover.strategy.costbased.feature.CostLocal; +import org.key_project.prover.strategy.costbased.feature.CostNonLocal; +import org.key_project.prover.strategy.costbased.feature.Feature; +import org.key_project.prover.strategy.costbased.termgenerator.TermGenerator; + +import org.jspecify.annotations.Nullable; + +/** + * Phase-2 cost reuse (perf round 3): decide whether a taclet's strategy cost is a pure function of + * the rule app and its find-position subterm (plus the always-recomputed age term and + * {@code NonDuplicateApp}-family vetoes). For such "cost-local" taclets the base re-cost performed + * by {@link TacletAppContainer#createFurtherApps} on every peek round can be replaced by arithmetic + * (carry the stored cost forward, refresh only the age term), avoiding the dominant + * {@code Strategy.computeCost} work. + * + *

Classification (sound-by-construction, default-impure)

+ * A taclet is eligible iff every {@link Feature} reachable in its per-taclet cost bindings resolves + * to local. Per feature, in order: + *
    + *
  1. veto ({@link AbstractNonDuplicateAppFeature}): a 0/Top guard. Collected and re-checked + * at reuse time (an app that became a duplicate must still be dropped); not descended into.
  2. + *
  3. explicit override: {@link CostNonLocal} forces non-local (the author wins).
  4. + *
  5. {@link CostLocal}: local. For a leaf that is the whole story; for a composite it means + * "transparent" -- the walk still recurses into its child features, so it stays local only if they + * all are. There is NO automatic "any feature with children is transparent" guess: a composite is + * trusted only because its author annotated it after checking that its own computation (including + * any non-Feature inputs such as projections/term-generators) is find-local. The transparent + * combinators are annotated once (Sum/Shannon/Scale/Let/ComprehendedSum/...).
  6. + *
  7. otherwise (unannotated): non-local (the SAFE default). A new feature -- leaf or + * composite -- is non-local until someone reviews it and adds {@link CostLocal}; forgetting costs + * only performance, never soundness.
  8. + *
+ * A {@link CostLocal} composite is local only if, in addition to its child features, every child + * {@link TermGenerator} it holds is also {@link CostLocal} -- a generator is a non-Feature input + * that decides locality (e.g. {@code SuperTermGenerator} is find-local, {@code + * SequentFormulasGenerator} reads the whole sequent and is not). The walk descends only through + * {@link Feature}- and {@link TermGenerator}-typed references (never arbitrary objects): the live + * feature tree holds mutable scratch state (e.g. TermBuffers) that must not be traversed. + * + *

+ * Optional verification (-Dkey.strategy.costReuse.verify): when reuse is applied also + * recompute the cost and log a warning on any mismatch -- a development aid to catch a feature that + * is mis-classified local (it should then get {@link CostNonLocal}). + */ +public final class CostReuse { + + public static final boolean VERIFY = Boolean.getBoolean("key.strategy.costReuse.verify"); + + private static final org.slf4j.Logger LOGGER = + org.slf4j.LoggerFactory.getLogger(CostReuse.class); + + private CostReuse() {} + + private static volatile @Nullable List dispatchers; + /** + * taclet -> collected veto features. An ELIGIBLE taclet always has at least the top-level + * NonDuplicateApp veto, so an empty array is used as the INELIGIBLE sentinel (ConcurrentHashMap + * forbids null values). + */ + private static final Map classification = new ConcurrentHashMap<>(); + /** Per-class locality decision, cached (class annotations are stable for the JVM run). */ + private static final Map, Kind> kindCache = new ConcurrentHashMap<>(); + + private enum Kind { + VETO, NON_LOCAL, LOCAL + } + + /** + * @return the {@link AbstractNonDuplicateAppFeature} vetoes to re-check for a cost-local + * taclet, + * or {@code null} if the taclet is not eligible for cost reuse. + */ + public static Feature @Nullable [] vetoesIfEligible(Object strategy, Taclet taclet) { + final Feature[] r = classification.computeIfAbsent(taclet, t -> { + final Feature[] res = classify(strategy, t); + return res == null ? new Feature[0] : res; + }); + return r.length == 0 ? null : r; + } + + private static Feature @Nullable [] classify(Object strategy, Taclet taclet) { + final Set vetoes = Collections.newSetFromMap(new IdentityHashMap<>()); + vetoes.add(NonDuplicateAppFeature.INSTANCE); // top-level veto, applies to every taclet + final boolean[] local = { true }; + final Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + for (RuleSetDispatchFeature d : dispatchers(strategy)) { + var rs = taclet.getRuleSets(); + while (!rs.isEmpty()) { + final Feature f = d.get(rs.head()); + if (f != null) { + walk(f, vetoes, local, seen); + } + rs = rs.tail(); + } + } + return local[0] ? vetoes.toArray(new Feature[0]) : null; + } + + /** + * Classify a feature; for a transparent composite, recurse into its child features and require + * its child term-generators to be local too (see the class comment). + */ + private static void walk(Feature f, Set vetoes, boolean[] local, Set seen) { + if (!local[0] || !seen.add(f)) { + return; + } + switch (kind(f)) { + case VETO -> vetoes.add(f); + case NON_LOCAL -> local[0] = false; + // LOCAL: a leaf is done; a composite stays local iff all its child FEATURES are local + // AND all its child TERM-GENERATORS are local. The generator matters because e.g. + // ComprehendedSumFeature sums its body over a generator: SuperTermGenerator (find + // ancestors) is local, but SequentFormulasGenerator (whole sequent) is not -- and a + // generator is a non-Feature input the feature recursion would otherwise miss. + case LOCAL -> forEachChild(f, child -> { + if (child instanceof Feature cf) { + walk(cf, vetoes, local, seen); + } else if (!isLocal(child.getClass())) { // a TermGenerator (or similar input) + local[0] = false; + } + }); + } + } + + /** + * A non-Feature classifying input (e.g. a TermGenerator) is local only if {@link CostLocal}. + */ + private static boolean isLocal(Class c) { + return !c.isAnnotationPresent(CostNonLocal.class) && c.isAnnotationPresent(CostLocal.class); + } + + /** + * Classify a feature's class (cached). SOUND-by-construction: a feature is treated as local + * ONLY if it is explicitly {@link CostLocal}-annotated (its author asserts it depends only on + * the app + find subterm, modulo its child features) -- there is no structural "any composite + * is transparent" guess. {@link CostNonLocal} forces non-local; everything unannotated is + * non-local (the safe default). + */ + private static Kind kind(Feature f) { + return kindCache.computeIfAbsent(f.getClass(), c -> { + if (c.isAnnotationPresent(CostNonLocal.class)) { + return Kind.NON_LOCAL; // explicit author override wins + } + if (f instanceof AbstractNonDuplicateAppFeature) { + return Kind.VETO; + } + return c.isAnnotationPresent(CostLocal.class) ? Kind.LOCAL : Kind.NON_LOCAL; + }); + } + + /** + * Apply {@code action} to each {@link Feature} and {@link TermGenerator} held one structural + * step inside {@code f}. + */ + private static void forEachChild(Feature f, java.util.function.Consumer action) { + for (Field fld : allFields(f.getClass())) { + if (Modifier.isStatic(fld.getModifiers()) || fld.getType().isPrimitive()) { + continue; + } + try { + fld.setAccessible(true); + follow(fld.get(f), action); + } catch (Throwable ignored) { + } + } + } + + private static void follow(@Nullable Object o, java.util.function.Consumer action) { + if (o == null) { + return; + } + if (o instanceof Feature || o instanceof TermGenerator) { + action.accept(o); + return; + } + Class c = o.getClass(); + if (c.isArray()) { + if (!c.getComponentType().isPrimitive()) { + int n = Array.getLength(o); + for (int i = 0; i < n; i++) { + follow(Array.get(o, i), action); + } + } + return; + } + if (o instanceof Iterable it) { + for (Object e : it) { + follow(e, action); + } + } + // other object types (TermBuffer, ProjectionToTerm, Name, ...) are NOT traversed + } + + /** + * Verification aid (only when {@link #VERIFY}): warn if a reused cost differs from the freshly + * recomputed one, i.e. some feature is mis-classified local and should be {@link CostNonLocal}. + */ + static void warnMismatch(Taclet taclet, Object reused, Object fresh) { + LOGGER.warn("cost-reuse mismatch for taclet {}: a feature is mis-classified local; " + + "annotate it @CostNonLocal (reused={}, fresh={})", taclet.name(), reused, fresh); + } + + private static List dispatchers(Object strategy) { + List d = dispatchers; + if (d == null) { + d = strategy instanceof ModularJavaDLStrategy m ? m.costRuleSetDispatchers() + : List.of(); + dispatchers = d; + } + return d; + } + + private static List allFields(Class c) { + List fs = new ArrayList<>(); + for (Class k = c; k != null && k != Object.class; k = k.getSuperclass()) { + fs.addAll(Arrays.asList(k.getDeclaredFields())); + } + return fs; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java index e7f9bce2798..6079de5766b 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java @@ -57,6 +57,9 @@ public class ModularJavaDLStrategy extends AbstractFeatureStrategy { private final ResponsibleStrategyCache responsibleStrategyCache; + /// the conflict-resolution cost dispatcher; kept for {@link #costRuleSetDispatchers} + private final RuleSetDispatchFeature conflictCostDispatcher; + public ModularJavaDLStrategy(Proof proof, List componentStrategies, StrategyProperties properties) { super(proof); @@ -69,7 +72,7 @@ public ModularJavaDLStrategy(Proof proof, List componentStrat // if more than one strategy is responsible for a _ruleset_ we need to determine how to // resolve the // competing computations - RuleSetDispatchFeature conflictCostDispatcher = resolveConflicts(); + conflictCostDispatcher = resolveConflicts(); final Feature ifMatchedF = ifZero(MatchedAssumesFeature.INSTANCE, longConst(+1)); Feature reduceCostTillMaxF = new ReduceTillMaxFeature(Feature::computeCost, @@ -97,6 +100,24 @@ public ModularJavaDLStrategy(Proof proof, List componentStrat disableInstantiate(); } + /** + * The {@link RuleSetDispatchFeature}s that contribute to a rule's COST (the conflict-resolution + * dispatcher plus each component strategy's cost dispatcher). Exposed for {@link CostReuse}'s + * feature-locality classification, which must NOT reach this via reflection from the strategy + * object (that would traverse the live proof graph the strategy references). + */ + public List costRuleSetDispatchers() { + final List result = new ArrayList<>(); + result.add(conflictCostDispatcher); + for (ComponentStrategy s : strategies) { + final RuleSetDispatchFeature d = s.getDispatcher(StrategyAspect.Cost); + if (d != null) { + result.add(d); + } + } + return result; + } + private record StratAndDispatcher(ComponentStrategy strategy, RuleSetDispatchFeature dispatcher) { } diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java index 22282daa683..6bca45c8f29 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java @@ -18,12 +18,17 @@ import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.strategy.costbased.MutableState; +import org.key_project.prover.strategy.costbased.NumberRuleAppCost; import org.key_project.prover.strategy.costbased.RuleAppCost; import org.key_project.prover.strategy.costbased.TopRuleAppCost; +import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.util.collection.ImmutableList; import org.key_project.util.collection.ImmutableSLList; import org.key_project.util.collection.ImmutableSet; +import org.jspecify.annotations.Nullable; + /** * Instances of this class are immutable */ @@ -88,7 +93,11 @@ public final ImmutableList createFurtherApps(Goal p_goal) { return ImmutableSLList.nil(); } - final TacletAppContainer newCont = createContainer(p_goal); + final TacletAppContainer newCont = costLocalReusedContainerOr(p_goal); + if (newCont == null) { + // a veto fired on the cost-local fast path: the re-costed base would be infinite + return ImmutableSLList.nil(); + } if (newCont.getCost() instanceof TopRuleAppCost) { return ImmutableSLList.nil(); } @@ -183,6 +192,44 @@ private TacletAppContainer createContainer(Goal p_goal) { return createContainer(getTacletApp(), getPosInOccurrence(p_goal), p_goal, false); } + /** + * Re-cost the base app for {@link #createFurtherApps}. On the cost-reuse fast path (taclet + * classified cost-local by {@link CostReuse}, non-initial container, numeric stored cost) the + * full {@link de.uka.ilkd.key.strategy.Strategy#computeCost} is replaced by arithmetic: carry + * the stored cost forward and refresh only its age term ({@code AgeFeature == goal.getTime()}). + * The {@code NonDuplicateApp}-family vetoes that contribute are re-evaluated first; if one + * fires + * the full cost would be {@link TopRuleAppCost}, so {@code null} is returned (drop the app). + * Otherwise, and whenever reuse is disabled/inapplicable, falls back to the normal recompute. + */ + private @Nullable TacletAppContainer costLocalReusedContainerOr(Goal p_goal) { + if (getAge() >= 0 + && getCost() instanceof NumberRuleAppCost storedCost) { + final Feature[] vetoes = + CostReuse.vetoesIfEligible(p_goal.getGoalStrategy(), getTacletApp().taclet()); + if (vetoes != null) { + final PosInOccurrence pos = getPosInOccurrence(p_goal); + final MutableState mState = new MutableState(); + for (Feature veto : vetoes) { + if (veto.computeCost(getTacletApp(), pos, p_goal, + mState) instanceof TopRuleAppCost) { + return null; + } + } + final RuleAppCost reused = + storedCost.add(NumberRuleAppCost.create(p_goal.getTime() - getAge())); + if (CostReuse.VERIFY) { + final RuleAppCost fresh = createContainer(p_goal).getCost(); + if (!reused.equals(fresh)) { + CostReuse.warnMismatch(getTacletApp().taclet(), reused, fresh); + } + } + return createContainer(getTacletApp(), pos, p_goal, reused, false); + } + } + return createContainer(p_goal); + } + /** * Create containers for NoFindTaclets. */ diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/CheckApplyEqFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/CheckApplyEqFeature.java index 6cf5302230b..c9bbf5c347d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/CheckApplyEqFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/CheckApplyEqFeature.java @@ -13,6 +13,7 @@ import org.key_project.prover.sequent.PIOPathIterator; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.strategy.costbased.MutableState; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; /** @@ -20,6 +21,7 @@ * rule application must not be one side of an equation that is the instantiation of the first * if-formula. If the rule application is admissible, zero is returned. */ +@CostLocal public class CheckApplyEqFeature extends BinaryTacletAppFeature { public static final Feature INSTANCE = new CheckApplyEqFeature(); diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/ComprehendedSumFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/ComprehendedSumFeature.java index 2eabf7ed9ce..ec13ad5cd01 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/ComprehendedSumFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/ComprehendedSumFeature.java @@ -11,6 +11,7 @@ import org.key_project.prover.strategy.costbased.NumberRuleAppCost; import org.key_project.prover.strategy.costbased.RuleAppCost; import org.key_project.prover.strategy.costbased.TopRuleAppCost; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.prover.strategy.costbased.termProjection.TermBuffer; import org.key_project.prover.strategy.costbased.termgenerator.TermGenerator; @@ -21,6 +22,10 @@ * A feature that computes the sum of the values of a feature term when a given variable ranges over * a sequence of terms */ +// @CostLocal: transparent -- it sums its (recursed) body over a TermGenerator. CostReuse also +// classifies that generator: it stays local only with a find-local generator (e.g. +// SuperTermGenerator), and is non-local with a sequent-scanning one (SequentFormulasGenerator). +@CostLocal public class ComprehendedSumFeature> implements Feature { private final TermBuffer var; diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/FindRightishFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/FindRightishFeature.java index a3ddddddbb5..cfbf42e18f3 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/FindRightishFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/FindRightishFeature.java @@ -14,6 +14,7 @@ import org.key_project.prover.strategy.costbased.MutableState; import org.key_project.prover.strategy.costbased.NumberRuleAppCost; import org.key_project.prover.strategy.costbased.RuleAppCost; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.jspecify.annotations.NonNull; @@ -23,6 +24,7 @@ * choose the left branch (subterm) and how the right branches. This is used to identify the * upper/righter/bigger summands in a polynomial that is arranged in a left-associated way. */ +@CostLocal public class FindRightishFeature implements Feature { private final Operator add; private final static RuleAppCost one = NumberRuleAppCost.create(1); diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/InstantiatedSVFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/InstantiatedSVFeature.java index 650c6f6abac..30cda232d76 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/InstantiatedSVFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/InstantiatedSVFeature.java @@ -10,6 +10,7 @@ import org.key_project.logic.Name; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.strategy.costbased.MutableState; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.prover.strategy.costbased.termProjection.ProjectionToTerm; @@ -17,6 +18,7 @@ * Feature that returns zero iff a certain schema variable is instantiated. If the schemavariable is * not instantiated schema variable or does not occur in the taclet infinity costs are returned. */ +@CostLocal public class InstantiatedSVFeature extends BinaryTacletAppFeature { private final ProjectionToTerm instProj; diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MatchedAssumesFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MatchedAssumesFeature.java index d712c639ec5..ed53957cd49 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MatchedAssumesFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MatchedAssumesFeature.java @@ -8,12 +8,14 @@ import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.strategy.costbased.MutableState; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; /** * Binary features that returns zero iff the if-formulas of a Taclet are instantiated or the Taclet * does not have any if-formulas. */ +@CostLocal public final class MatchedAssumesFeature extends BinaryTacletAppFeature { public static final Feature INSTANCE = new MatchedAssumesFeature(); diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MonomialsSmallerThanFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MonomialsSmallerThanFeature.java index b9209f7d756..75aacbb0b6a 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MonomialsSmallerThanFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/MonomialsSmallerThanFeature.java @@ -14,6 +14,7 @@ import org.key_project.prover.strategy.costbased.MutableState; import org.key_project.prover.strategy.costbased.NumberRuleAppCost; import org.key_project.prover.strategy.costbased.TopRuleAppCost; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.prover.strategy.costbased.termProjection.ProjectionToTerm; import org.key_project.prover.strategy.costbased.termfeature.BinarySumTermFeature; @@ -28,6 +29,7 @@ * Feature that returns zero iff each monomial of one polynomial is smaller than all monomials of a * second polynomial */ +@CostLocal public class MonomialsSmallerThanFeature extends AbstractMonomialSmallerThanFeature { private final TermFeature hasCoeff; diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/NoSelfApplicationFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/NoSelfApplicationFeature.java index 4b9d1c21a48..6bd69a25079 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/NoSelfApplicationFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/NoSelfApplicationFeature.java @@ -9,6 +9,7 @@ import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.strategy.costbased.MutableState; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.util.collection.ImmutableList; @@ -16,6 +17,7 @@ * This feature checks that the position of application is not contained in the if-formulas. If the * rule application is admissible, zero is returned. */ +@CostLocal public class NoSelfApplicationFeature extends BinaryTacletAppFeature { public static final Feature INSTANCE = new NoSelfApplicationFeature(); diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TermSmallerThanFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TermSmallerThanFeature.java index ba1d7620655..93ce0456a42 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TermSmallerThanFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TermSmallerThanFeature.java @@ -8,12 +8,14 @@ import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.strategy.costbased.MutableState; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.prover.strategy.costbased.termProjection.ProjectionToTerm; /** * Feature that returns zero iff one term is smaller than another term in the current term ordering */ +@CostLocal public class TermSmallerThanFeature extends SmallerThanFeature { private final ProjectionToTerm left, right; diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TrivialMonomialLCRFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TrivialMonomialLCRFeature.java index e211b83ffbc..4382a0578a3 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TrivialMonomialLCRFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/TrivialMonomialLCRFeature.java @@ -10,6 +10,7 @@ import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.strategy.costbased.MutableState; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.prover.strategy.costbased.termProjection.ProjectionToTerm; @@ -20,6 +21,7 @@ *

* "A critical-pair/completion algorithm for finitely generated ideals in rings" */ +@CostLocal public class TrivialMonomialLCRFeature extends BinaryTacletAppFeature { private final ProjectionToTerm a, b; diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/termgenerator/SuperTermGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/termgenerator/SuperTermGenerator.java index 20fe63a4c66..3a953109864 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/termgenerator/SuperTermGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/termgenerator/SuperTermGenerator.java @@ -23,10 +23,12 @@ import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.strategy.costbased.MutableState; import org.key_project.prover.strategy.costbased.TopRuleAppCost; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.termfeature.TermFeature; import org.key_project.prover.strategy.costbased.termgenerator.TermGenerator; import org.key_project.util.collection.ImmutableArray; +@CostLocal public abstract class SuperTermGenerator implements TermGenerator { private final TermFeature cond; diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ConstFeature.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ConstFeature.java index 8d1ba7bf3ed..e7ef17721a4 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ConstFeature.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ConstFeature.java @@ -12,6 +12,7 @@ import org.jspecify.annotations.NonNull; /// A feature that returns a constant value +@CostLocal public class ConstFeature implements Feature { @Override diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostLocal.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostLocal.java new file mode 100644 index 00000000000..7ff7fa360e4 --- /dev/null +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostLocal.java @@ -0,0 +1,36 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.strategy.costbased.feature; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Marks a {@link Feature} whose cost is a pure function of the rule application and the subterm at + * its find position -- i.e. it does NOT depend on other formulas of the sequent, the set of applied + * rules on the branch, or any other goal-global state. + * + *

+ * This lets the strategy-cost reuse (see {@code de.uka.ilkd.key.strategy.CostReuse}) carry a + * container's cost forward across the per-round re-expansion instead of recomputing it, as long as + * the find position is unmodified. The default (no annotation) is the SAFE one: an unannotated leaf + * feature is treated as non-local, so cost reuse simply does not apply to taclets that use it (they + * are re-costed in full). Forgetting to annotate a new feature therefore costs performance, never + * soundness. + * + *

+ * For a composite feature (one that combines child features) this annotation means "transparent": + * the classifier recurses into the child features, so the composite counts as local only when all + * of them are. There is no automatic structural transparency -- a composite is trusted only because + * its author annotated it after checking that its own computation (including any non-Feature inputs + * such as projections or term-generators) is find-local. Use {@link CostNonLocal} to force a + * feature + * to be treated as non-local. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CostLocal { +} diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostNonLocal.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostNonLocal.java new file mode 100644 index 00000000000..d36694796fb --- /dev/null +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/CostNonLocal.java @@ -0,0 +1,28 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package org.key_project.prover.strategy.costbased.feature; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Explicitly marks a {@link Feature} as NON-local for strategy-cost reuse (see + * {@code de.uka.ilkd.key.strategy.CostReuse}): its cost may depend on goal-global state (other + * sequent formulas, applied rules, instantiation context, ...), so it must be recomputed on every + * re-expansion. + * + *

+ * This is an override: it wins over the automatic classification. Use it on a composite feature + * that would otherwise be auto-classified local (because all its children are local) but that + * itself reads goal-global state, or to defensively pin a feature whose locality is in doubt. The + * default for an unannotated leaf feature is already non-local, so this annotation is only needed + * to + * override the automatic decision. + */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface CostNonLocal { +} diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/FindDepthFeature.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/FindDepthFeature.java index a87d7b0c86d..3a2a37f0965 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/FindDepthFeature.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/FindDepthFeature.java @@ -17,6 +17,7 @@ /// depth zero or if not a find taclet) /// /// TODO: eliminate this class and use term features instead +@CostLocal public class FindDepthFeature implements Feature { private static final Feature INSTANCE = new FindDepthFeature(); diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/LetFeature.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/LetFeature.java index bf31a9a4275..f84a957f0ea 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/LetFeature.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/LetFeature.java @@ -18,6 +18,10 @@ /// is generated by a ProjectionToTerm. This is mostly useful to make feature terms /// more /// readable, and to avoid repeated evaluation of projections. +// @CostLocal: a let-binding is transparent -- its bound value is a ProjectionToTerm over the +// app/find term and its body is recursed; local iff the body is. (Reuse only applies while the +// find position is unmodified, so the projected focus term is stable.) +@CostLocal public class LetFeature> implements Feature { private final TermBuffer var; diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ScaleFeature.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ScaleFeature.java index bfdde9c2908..a155cd30e0e 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ScaleFeature.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ScaleFeature.java @@ -147,6 +147,7 @@ protected static boolean isZero(double p) { return Math.abs(p) < 0.0000001; } + @CostLocal private static class MultFeature extends ScaleFeature { /// the coefficient diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ShannonFeature.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ShannonFeature.java index 1e723e2e4cd..3e2253718f5 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ShannonFeature.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/ShannonFeature.java @@ -19,6 +19,7 @@ /// value of the whole expression is f1 (if c returns zero, or more /// general /// if c returns a distinguished value trueCost) or f2 +@CostLocal public class ShannonFeature implements Feature { /// The filter that decides which sub-feature is to be evaluated diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/SumFeature.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/SumFeature.java index 05f4f131360..32b05c15efc 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/SumFeature.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/feature/SumFeature.java @@ -16,6 +16,7 @@ import org.jspecify.annotations.NonNull; /// A feature that computes the sum of a given list (vector) of features +@CostLocal public class SumFeature implements Feature { @Override diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/termfeature/ApplyTFFeature.java b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/termfeature/ApplyTFFeature.java index 1bb33ac65a3..5066fe51aea 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/termfeature/ApplyTFFeature.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/strategy/costbased/termfeature/ApplyTFFeature.java @@ -10,12 +10,14 @@ import org.key_project.prover.strategy.costbased.MutableState; import org.key_project.prover.strategy.costbased.RuleAppCost; import org.key_project.prover.strategy.costbased.TopRuleAppCost; +import org.key_project.prover.strategy.costbased.feature.CostLocal; import org.key_project.prover.strategy.costbased.feature.Feature; import org.key_project.prover.strategy.costbased.termProjection.ProjectionToTerm; import org.jspecify.annotations.NonNull; /// Feature for invoking a term feature on the instantiation of a schema variable +@CostLocal public class ApplyTFFeature> implements Feature { private final ProjectionToTerm proj; From d5a5024910ce1f4481e84c763359b57a28af7987 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Tue, 16 Jun 2026 03:20:01 +0200 Subject: [PATCH 15/26] fix(strategy): make introductionTime deterministic (do not cache -1) introductionTime cached the not-introduced-yet answer (-1); the symbol may be introduced by a later rule, after which the real time would be found, so the frozen -1 made the value depend on whether the symbol was first compared before or after its introduction -- an access-pattern dependence that makes term ordering, and hence OneStepSimplifier rewriting, subtly non-deterministic. Only cache a real introduction time (stable once found). --- .../AbstractMonomialSmallerThanFeature.java | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AbstractMonomialSmallerThanFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AbstractMonomialSmallerThanFeature.java index 3c69ff78ab3..a99ae9ef628 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AbstractMonomialSmallerThanFeature.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AbstractMonomialSmallerThanFeature.java @@ -44,8 +44,19 @@ protected int introductionTime(Operator op, Goal goal) { if (res == null) { res = introductionTimeHelp(op, goal); - synchronized (introductionTimeCache) { - introductionTimeCache.put(op, res); + // Do NOT cache the "not introduced (yet)" answer (-1): op may be introduced by a later + // rule application, after which introductionTimeHelp would find a real time. Caching + // the + // -1 would freeze it, making the value depend on whether op happened to be first + // queried + // before or after its introduction -- i.e. on the access pattern (which features run, + // when). That makes term ordering, and hence OneStepSimplifier rewriting, subtly + // non-deterministic. A real introduction time, once found, is stable (the introducing + // rule stays in the applied-rule prefix), so it is safe to cache. + if (res != -1) { + synchronized (introductionTimeCache) { + introductionTimeCache.put(op, res); + } } } From 6a86b3a45b9a2ff59b6bdedcef19405e5e797883 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Tue, 16 Jun 2026 04:49:15 +0200 Subject: [PATCH 16/26] refactor(strategy): name the cost-reuse INELIGIBLE sentinel Replace the per-call new Feature[0] / r.length==0 idiom with a shared INELIGIBLE constant and identity check. An eligible taclet always carries at least the top-level NonDuplicateApp veto, so identity is the clearer 'not eligible' test. Pure refactor: byte-identical (symmArray 14601 nodes, 0 verify-mode mismatches). --- .../src/main/java/de/uka/ilkd/key/strategy/CostReuse.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java index dc60c51e883..87e1731d3ac 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/CostReuse.java @@ -76,6 +76,8 @@ private CostReuse() {} private static final Map classification = new ConcurrentHashMap<>(); /** Per-class locality decision, cached (class annotations are stable for the JVM run). */ private static final Map, Kind> kindCache = new ConcurrentHashMap<>(); + /** Cached "not eligible for reuse" marker (the map forbids null values). */ + private static final Feature[] INELIGIBLE = new Feature[0]; private enum Kind { VETO, NON_LOCAL, LOCAL @@ -89,9 +91,9 @@ private enum Kind { public static Feature @Nullable [] vetoesIfEligible(Object strategy, Taclet taclet) { final Feature[] r = classification.computeIfAbsent(taclet, t -> { final Feature[] res = classify(strategy, t); - return res == null ? new Feature[0] : res; + return res == null ? INELIGIBLE : res; }); - return r.length == 0 ? null : r; + return r == INELIGIBLE ? null : r; } private static Feature @Nullable [] classify(Object strategy, Taclet taclet) { From cac2896cf1df57d4e6b78d6053db58a77a1af704 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Tue, 16 Jun 2026 12:22:26 +0200 Subject: [PATCH 17/26] perf(strategy): make goal age a first-class container-level cost term Age (goal time) was contributed inside each top-level strategy's computeCost (AgeFeature in ModularJavaDLStrategy's cost/inst sums; getTime() in FIFOStrategy and SimpleFilteredStrategy). Move it out into a single container-level term, RuleAppContainer.withAge, added exactly once when a container is built -- so strategies (and their components) compute only their age-free cost and age is added once regardless of how strategies are composed. AgeFeature is removed. This lets cost reuse carry the age-free base forward verbatim: TacletAppContainer stores the age-free cost and the reuse fast path is just 'base + current age' with no getTime()-getAge() reconstruction and no age>=0 guard (initial containers reuse soundly too). As a side effect the container's age field is decoupled from the cost and is now purely the AssumesInstantiator freshness key. Behaviour-preserving: byte-identical to the parent on SLL, saddleback, symmArray, median (verified by A/B against the legacy age-in-features path before it was removed; isolated timing showed the relocation is performance-neutral, so its value is code quality plus enabling the simpler, broader cost-reuse path). --- .../key/strategy/BuiltInRuleAppContainer.java | 3 +- .../uka/ilkd/key/strategy/FIFOStrategy.java | 4 +- .../key/strategy/FindTacletAppContainer.java | 7 ++- .../key/strategy/ModularJavaDLStrategy.java | 9 +-- .../strategy/NoFindTacletAppContainer.java | 5 +- .../ilkd/key/strategy/RuleAppContainer.java | 13 +++++ .../key/strategy/SimpleFilteredStrategy.java | 4 +- .../ilkd/key/strategy/TacletAppContainer.java | 57 +++++++++++++------ .../ilkd/key/strategy/feature/AgeFeature.java | 34 ----------- 9 files changed, 72 insertions(+), 64 deletions(-) delete mode 100644 key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AgeFeature.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/BuiltInRuleAppContainer.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/BuiltInRuleAppContainer.java index cbcb861dd3f..b52f6df4541 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/BuiltInRuleAppContainer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/BuiltInRuleAppContainer.java @@ -101,7 +101,8 @@ private PosInOccurrence getPosInOccurrence(Goal p_goal) { static RuleAppContainer createAppContainer(IBuiltInRuleApp bir, PosInOccurrence pio, Goal goal) { - final RuleAppCost cost = goal.getGoalStrategy().computeCost(bir, pio, goal); + final RuleAppCost cost = + withAge(goal.getGoalStrategy().computeCost(bir, pio, goal), goal); return new BuiltInRuleAppContainer(bir, pio, cost, goal); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/FIFOStrategy.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/FIFOStrategy.java index 9aaac0ef5a5..90319d42885 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/FIFOStrategy.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/FIFOStrategy.java @@ -40,7 +40,9 @@ private FIFOStrategy() { PosInOccurrence pio, Goal goal, MutableState mState) { - return NumberRuleAppCost.create(((de.uka.ilkd.key.proof.Goal) goal).getTime()); + // FIFO ordering is purely the goal age, which RuleAppContainer.withAge adds once per + // container, so the age-free strategy cost is zero. + return NumberRuleAppCost.getZeroCost(); } /** diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/FindTacletAppContainer.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/FindTacletAppContainer.java index 886be2b0b58..1892a519177 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/FindTacletAppContainer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/FindTacletAppContainer.java @@ -49,14 +49,15 @@ public class FindTacletAppContainer extends TacletAppContainer { * * @param app the taclet application * @param pio the position in occurrence - * @param cost the rule application cost + * @param ageFreeCost the rule application cost without the goal-age term + * @param cost the rule application cost (age-free cost plus the goal-age term) * @param goal the goal to apply the taclet on * @param age the age */ FindTacletAppContainer(NoPosTacletApp app, PosInOccurrence pio, - RuleAppCost cost, Goal goal, + RuleAppCost ageFreeCost, RuleAppCost cost, Goal goal, long age) { - super(app, cost, age); + super(app, ageFreeCost, cost, age); applicationPosition = pio; final FormulaTag posTag = goal.getFormulaTagManager().getTagForPos(pio.topLevel()); diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java index 6079de5766b..9e9923a3024 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/ModularJavaDLStrategy.java @@ -10,7 +10,6 @@ import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.strategy.ComponentStrategy.StrategyAspect; -import de.uka.ilkd.key.strategy.feature.AgeFeature; import de.uka.ilkd.key.strategy.feature.MatchedAssumesFeature; import de.uka.ilkd.key.strategy.feature.NonDuplicateAppFeature; import de.uka.ilkd.key.strategy.feature.RuleSetDispatchFeature; @@ -83,10 +82,12 @@ public ModularJavaDLStrategy(Proof proof, List componentStrat (rule) -> responsibleStrategyCache.getResponsibleStrategies(rule, strategies, StrategyAspect.Instantiation)); - // the feature for the cost computation + // the feature for the cost computation. Age (goal time) is NOT part of the strategy cost: + // it is a first-class container-level term added once by RuleAppContainer.withAge, so the + // cost here is age-free (this lets cost reuse carry the age-free base forward verbatim). totalCost = add(AutomatedRuleFeature.getInstance(), ifMatchedF, NonDuplicateAppFeature.INSTANCE, - reduceCostTillMaxF, conflictCostDispatcher, AgeFeature.INSTANCE); + reduceCostTillMaxF, conflictCostDispatcher); // The feature for instantiateApp, built once instead of on every call. // Note that no conflict dispatcher takes part in this sum: resolveConflicts() @@ -96,7 +97,7 @@ public ModularJavaDLStrategy(Proof proof, List componentStrat enableInstantiate(); totalInstCost = add(AutomatedRuleFeature.getInstance(), ifMatchedF, NonDuplicateAppFeature.INSTANCE, - reduceInstTillMaxF, AgeFeature.INSTANCE); + reduceInstTillMaxF); disableInstantiate(); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/NoFindTacletAppContainer.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/NoFindTacletAppContainer.java index c1ca6c8fb87..9a521d58451 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/NoFindTacletAppContainer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/NoFindTacletAppContainer.java @@ -13,8 +13,9 @@ */ public class NoFindTacletAppContainer extends TacletAppContainer { - NoFindTacletAppContainer(NoPosTacletApp p_app, RuleAppCost p_cost, long p_age) { - super(p_app, p_cost, p_age); + NoFindTacletAppContainer(NoPosTacletApp p_app, RuleAppCost p_ageFreeCost, RuleAppCost p_cost, + long p_age) { + super(p_app, p_ageFreeCost, p_cost, p_age); } /** diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/RuleAppContainer.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/RuleAppContainer.java index 5e622bbb551..cba962e6a06 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/RuleAppContainer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/RuleAppContainer.java @@ -10,6 +10,7 @@ import org.key_project.prover.rules.RuleApp; import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.strategy.costbased.NumberRuleAppCost; import org.key_project.prover.strategy.costbased.RuleAppCost; import org.key_project.util.collection.ImmutableList; import org.key_project.util.collection.ImmutableSLList; @@ -61,6 +62,18 @@ public final RuleAppCost getCost() { return cost; } + /** + * Add the goal-age term to a strategy-computed cost. Age (the goal time, i.e. number of rules + * applied so far) is a single first-class component of every container's cost, contributed here + * rather than inside any {@link de.uka.ilkd.key.strategy.Strategy#computeCost} -- so a strategy + * (and each of its components) computes only its age-free cost, and age is added exactly once + * per queued container regardless of how strategies are composed. {@code Top} stays + * {@code Top}. + */ + protected static RuleAppCost withAge(RuleAppCost ageFreeCost, Goal goal) { + return ageFreeCost.add(NumberRuleAppCost.create(goal.getTime())); + } + /** * Create container for a RuleApp. * diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/SimpleFilteredStrategy.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/SimpleFilteredStrategy.java index 7c20b783df1..b532efeb4b2 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/SimpleFilteredStrategy.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/SimpleFilteredStrategy.java @@ -66,7 +66,9 @@ public Name name() { return res; } - long cost = ((de.uka.ilkd.key.proof.Goal) goal).getTime(); + // The goal-age ordering is added once by RuleAppContainer.withAge; only the age-free + // malus remains in the strategy cost. + long cost = 0; if (app instanceof TacletApp tacletApp && !tacletApp.assumesInstantionsComplete()) { cost += IF_NOT_MATCHED_MALUS; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java index 6bca45c8f29..75ec1d7eab1 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/TacletAppContainer.java @@ -41,13 +41,32 @@ public abstract class TacletAppContainer extends RuleAppContainer { // save any memory (at the moment). // This is because Java's memory alingment. + /** + * Creation time of this container ({@code -1} for an initial/just-loaded container). Since age + * became a first-class container-level cost term ({@link RuleAppContainer#withAge}) this field + * no longer feeds the cost; it is purely the {@link AssumesInstantiator} freshness key (was + * this + * container built before or after a given if-formula). + */ private final long age; + /** + * The age-free strategy cost: {@code getCost()} without the goal-age term that + * {@link RuleAppContainer#withAge} adds. Stored so cost reuse can carry it forward unchanged + * across re-expansion and only re-add the current age, with no reconstruction arithmetic. + */ + private final RuleAppCost ageFreeCost; - protected TacletAppContainer(RuleApp p_app, RuleAppCost p_cost, long p_age) { + protected TacletAppContainer(RuleApp p_app, RuleAppCost p_ageFreeCost, RuleAppCost p_cost, + long p_age) { super(p_app, p_cost); + ageFreeCost = p_ageFreeCost; age = p_age; } + RuleAppCost getAgeFreeCost() { + return ageFreeCost; + } + protected NoPosTacletApp getTacletApp() { return (NoPosTacletApp) getRuleApp(); } @@ -71,14 +90,15 @@ protected static TacletAppContainer createContainer(NoPosTacletApp p_app, private static TacletAppContainer createContainer(NoPosTacletApp p_app, PosInOccurrence p_pio, - Goal p_goal, RuleAppCost p_cost, boolean p_initial) { + Goal p_goal, RuleAppCost p_ageFreeCost, boolean p_initial) { // This relies on the fact that the method Goal.getTime() // never returns a value less than zero final long localage = p_initial ? -1 : p_goal.getTime(); + final RuleAppCost cost = withAge(p_ageFreeCost, p_goal); if (p_pio == null) { - return new NoFindTacletAppContainer(p_app, p_cost, localage); + return new NoFindTacletAppContainer(p_app, p_ageFreeCost, cost, localage); } else { - return new FindTacletAppContainer(p_app, p_pio, p_cost, p_goal, localage); + return new FindTacletAppContainer(p_app, p_pio, p_ageFreeCost, cost, p_goal, localage); } } @@ -194,17 +214,18 @@ private TacletAppContainer createContainer(Goal p_goal) { /** * Re-cost the base app for {@link #createFurtherApps}. On the cost-reuse fast path (taclet - * classified cost-local by {@link CostReuse}, non-initial container, numeric stored cost) the - * full {@link de.uka.ilkd.key.strategy.Strategy#computeCost} is replaced by arithmetic: carry - * the stored cost forward and refresh only its age term ({@code AgeFeature == goal.getTime()}). - * The {@code NonDuplicateApp}-family vetoes that contribute are re-evaluated first; if one - * fires - * the full cost would be {@link TopRuleAppCost}, so {@code null} is returned (drop the app). + * classified cost-local by {@link CostReuse}, numeric age-free cost) the full + * {@link de.uka.ilkd.key.strategy.Strategy#computeCost} is skipped: the stored age-free cost is + * carried forward verbatim and {@link RuleAppContainer#withAge} re-adds the current goal age + * when the new container is built -- no reconstruction arithmetic, and initial containers + * (age {@code -1}) reuse soundly too, since age is no longer part of the stored cost. The + * {@code NonDuplicateApp}-family vetoes that contribute are re-evaluated first; if one fires + * the + * full cost would be {@link TopRuleAppCost}, so {@code null} is returned (drop the app). * Otherwise, and whenever reuse is disabled/inapplicable, falls back to the normal recompute. */ private @Nullable TacletAppContainer costLocalReusedContainerOr(Goal p_goal) { - if (getAge() >= 0 - && getCost() instanceof NumberRuleAppCost storedCost) { + if (getAgeFreeCost() instanceof NumberRuleAppCost base) { final Feature[] vetoes = CostReuse.vetoesIfEligible(p_goal.getGoalStrategy(), getTacletApp().taclet()); if (vetoes != null) { @@ -216,15 +237,15 @@ && getCost() instanceof NumberRuleAppCost storedCost) { return null; } } - final RuleAppCost reused = - storedCost.add(NumberRuleAppCost.create(p_goal.getTime() - getAge())); if (CostReuse.VERIFY) { - final RuleAppCost fresh = createContainer(p_goal).getCost(); - if (!reused.equals(fresh)) { - CostReuse.warnMismatch(getTacletApp().taclet(), reused, fresh); + final RuleAppCost freshBase = + p_goal.getGoalStrategy().computeCost(getTacletApp(), pos, p_goal); + if (!base.equals(freshBase)) { + CostReuse.warnMismatch(getTacletApp().taclet(), base, freshBase); } } - return createContainer(getTacletApp(), pos, p_goal, reused, false); + // carry the age-free base forward; createContainer re-adds the current age + return createContainer(getTacletApp(), pos, p_goal, base, false); } } return createContainer(p_goal); diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AgeFeature.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AgeFeature.java deleted file mode 100644 index 3428ec49609..00000000000 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/feature/AgeFeature.java +++ /dev/null @@ -1,34 +0,0 @@ -/* This file is part of KeY - https://key-project.org - * KeY is licensed under the GNU General Public License Version 2 - * SPDX-License-Identifier: GPL-2.0-only */ -package de.uka.ilkd.key.strategy.feature; - -import org.key_project.prover.proof.ProofGoal; -import org.key_project.prover.rules.RuleApp; -import org.key_project.prover.sequent.PosInOccurrence; -import org.key_project.prover.strategy.costbased.MutableState; -import org.key_project.prover.strategy.costbased.NumberRuleAppCost; -import org.key_project.prover.strategy.costbased.RuleAppCost; -import org.key_project.prover.strategy.costbased.feature.Feature; - -import org.jspecify.annotations.NonNull; - -/** - * Feature that computes the age of the goal (i.e. total number of rules applications that have been - * performed at the goal) to which a rule is supposed to be applied - */ -public class AgeFeature implements Feature { - - public static final Feature INSTANCE = new AgeFeature(); - - private AgeFeature() {} - - @Override - public > RuleAppCost computeCost(RuleApp app, - PosInOccurrence pos, Goal goal, MutableState mState) { - return NumberRuleAppCost.create(((de.uka.ilkd.key.proof.Goal) goal).getTime()); - // return LongRuleAppCost.create ( goal.getTime() / goal.sequent ().size () ); - // return LongRuleAppCost.create ( (long)Math.sqrt ( goal.getTime () ) ); - } - -} From 1590ff8eb777948d4d1619257a21db5bb083b5f0 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Tue, 16 Jun 2026 15:01:01 +0200 Subject: [PATCH 18/26] perf(strategy): precise op-indexed parking of assumes-incomplete bases Park assumes-incomplete taclet bases that re-expand to nothing (97-99.6% of base re-expansions) out of the active queue, and wake them by a precise operator index: index each parked base by the concrete top operator(s) of its \assumes formulas (resolved through the find-match's SV instantiations); wake exactly the bases whose operator matches a formula added/modified that round (Goal.fireSequentChanged -> sequentChanged), walking the changed formula's update-prefix spine (a sound superset, since the assumes matcher strips the update context). Only effectively-indexable bases are parked; unbound-generic tops stay in the queue. Insertion-ordered (LinkedHashMap/Set) for determinism; clone() deep-copies the index. Active by default. Provability-safe on the full real RAP suite (681 goals) once the redundant order-fragile lenOfSeqSubEQ is dropped from automode (see follow-up commit). Not byte-identical (reordering shifts proof shapes, all still close); ~40% faster automode on the perfTest goals. --- .../main/java/de/uka/ilkd/key/proof/Goal.java | 10 + .../strategy/QueueRuleApplicationManager.java | 264 +++++++++++++++++- 2 files changed, 270 insertions(+), 4 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/Goal.java b/key.core/src/main/java/de/uka/ilkd/key/proof/Goal.java index 2e12543c5f1..e5fee125729 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/Goal.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/Goal.java @@ -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; @@ -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 m = ruleAppManager; + while (m instanceof DelegationBasedRuleApplicationManager 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) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/QueueRuleApplicationManager.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/QueueRuleApplicationManager.java index ff7fbb523af..5c56c0cf2a7 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/QueueRuleApplicationManager.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/QueueRuleApplicationManager.java @@ -5,13 +5,27 @@ import java.util.ArrayList; import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; import java.util.concurrent.atomic.AtomicLong; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.UpdateApplication; import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.inst.SVInstantiations; +import org.key_project.logic.op.Operator; +import org.key_project.logic.op.sv.SchemaVariable; import org.key_project.prover.proof.ProofGoal; import org.key_project.prover.rules.RuleApp; +import org.key_project.prover.sequent.FormulaChangeInfo; import org.key_project.prover.sequent.PosInOccurrence; +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.RuleApplicationManager; import org.key_project.prover.strategy.costbased.MutableState; import org.key_project.prover.strategy.costbased.RuleAppCost; @@ -67,6 +81,47 @@ public class QueueRuleApplicationManager implements RuleApplicationManager private long nextRuleTime; + /** + * Parked assumes-incomplete taclet bases, indexed by the concrete top operator(s) of their + * {@code \assumes} formulas. Such bases re-expand to nothing (no new assumes match) on almost + * every round (profiling: 96.8% of queue pops fail at the unmatched {@code \assumes}, and + * 97-99.6% of the resulting re-expansions yield no new instance -- the dominant remaining queue + * churn), so instead of re-popping and re-expanding them each round they are parked here and + * woken only when a formula they could match appears. A base is stored under each of its wake + * operators and woken when any of them is added/modified. Insertion-ordered for determinism; + * {@code null} until the first base is parked. + *

+ * Sound (byte-identical) by construction, unlike the earlier sequent-growth heuristic: + *

    + *
  • Only effectively-indexable bases are parked -- every {@code \assumes} formula's + * top operator is concrete or a schema variable already bound by the find-match (see + * {@link #assumesWakeOps}). Bases with an unbound-generic top (which would match any formula) + * are never parked; they stay in the active queue and re-expand every round exactly as without + * parking.
  • + *
  • A parked base is woken (re-inserted into the active queue) on exactly the round a formula + * with a matching top operator is added or modified ({@link #sequentChanged}/{@code + * pendingWakeOps}) -- the same round its non-parked counterpart would first see that formula as + * "new" and re-expand to the instance. So the instance enters the queue at the identical round, + * with the identical (current) age and cost, and the proof is byte-identical.
  • + *
  • The wake set is a sound superset: it walks the changed formula's update-prefix + * spine of operators (the assumes matcher strips the update context before matching, see + * {@code VMTacletMatcher.matchUpdateContext}). Over-waking is harmless -- a spuriously woken + * base + * is popped, re-expands to nothing, and is re-parked, exactly as a non-parked base would behave + * that round. Only missing a wake would diverge, and that cannot happen for an + * effectively-indexable base.
  • + *
  • All structures are insertion-ordered ({@link LinkedHashMap}/{@link LinkedHashSet}) so + * parking introduces no non-determinism.
  • + *
+ */ + private @Nullable LinkedHashMap> parkedByOp = null; + /** + * Top operators (along the update-prefix spine) of formulas added/modified since the previous + * round; the wake candidates consumed at the start of the next round. Insertion-ordered for + * determinism; {@code null} until the first change is recorded. + */ + private @Nullable LinkedHashSet pendingWakeOps = null; + @Override public void setGoal(Goal p_goal) { goal = p_goal; @@ -79,6 +134,8 @@ public void setGoal(Goal p_goal) { public void clearCache() { queue = null; previousMinimum = null; + parkedByOp = null; + pendingWakeOps = null; if (goal != null) { goal.proof().getServices().getCaches().getIfInstantiationCache().releaseAll(); } @@ -279,11 +336,20 @@ private void computeNextRuleApp(ImmutableHeap<@NonNull RuleAppContainer> further */ ImmutableList workingList = ImmutableSLList.nil(); + // Wake parked assumes-bases whose \assumes top operator matches a formula added/modified + // since the last round, re-inserting them into the active queue so they re-expand during + // THIS + // round -- the identical round their non-parked counterparts would first see that formula + // as + // new. No completeness net is needed (or wanted): an effectively-indexable base is always + // woken on its matching round, and a late re-injection would surface its instance at the + // wrong round, the very divergence parking must avoid. + wakeParkedBases(); + /* * Try to find a rule app that can be completed until both queues are exhausted. */ while (nextRuleApp == null && !(queue.isEmpty() && furtherAppsQueue.isEmpty())) { - /* * Determine the minimum rule app container, ranging over both queues. Putting this into * a separate method would be convenient. But since we are using immutable data @@ -353,11 +419,24 @@ private void computeNextRuleApp(ImmutableHeap<@NonNull RuleAppContainer> further * Create further apps if found in main queue. Rule apps obtained this way will * be considered during the current round. */ + final ImmutableList further = + minRuleAppContainer.createFurtherApps(goal); + // Empty assumes yield (the re-expansion is just the re-costed base itself, with + // the now-current age): if the base is effectively indexable, park it instead + // of + // re-adding so it stops being re-popped every round. Park further.head() (the + // freshly re-costed container) so the parked age advances exactly as the + // non-parked base's would, keeping later assumes matches from re-deriving stale + // instances. A non-indexable base falls through and is re-added unchanged. + if (further.size() == 1 + && further.head() instanceof TacletAppContainer base + && !base.getTacletApp().assumesInstantionsComplete() + && park(base)) { + continue; + } var time = System.nanoTime(); try { - furtherAppsQueue = - push(minRuleAppContainer.createFurtherApps(goal).iterator(), - furtherAppsQueue); + furtherAppsQueue = push(further.iterator(), furtherAppsQueue); } finally { PERF_QUEUE_OPS.addAndGet(System.nanoTime() - time); } @@ -382,6 +461,170 @@ private void computeNextRuleApp(ImmutableHeap<@NonNull RuleAppContainer> further } } + // --------------------------------------------------------------------------------------------- + // Assumes-base parking (see parkedByOp) + // --------------------------------------------------------------------------------------------- + + /** + * Park an assumes-incomplete base, indexing it under the top operator(s) of its + * {@code \assumes} + * formulas so it can be woken when a matching formula appears. + * + * @param base the re-costed base container to park (carries the current age) + * @return {@code true} if the base was parked; {@code false} if it is not effectively indexable + * (an unbound-generic {@code \assumes} top), in which case the caller must keep it in + * the + * active queue + */ + private boolean park(TacletAppContainer base) { + final List ops = assumesWakeOps(base); + if (ops == null) { + return false; + } + if (parkedByOp == null) { + parkedByOp = new LinkedHashMap<>(); + } + for (Operator op : ops) { + parkedByOp.computeIfAbsent(op, k -> new LinkedHashSet<>()).add(base); + } + return true; + } + + /** + * Re-insert into the active queue every parked base waiting on an operator that was added or + * modified since the previous round (see {@link #pendingWakeOps}). Woken bases are collected in + * insertion order (deterministic) and removed from all their index buckets. + */ + private void wakeParkedBases() { + if (pendingWakeOps == null) { + return; + } + if (parkedByOp != null && !parkedByOp.isEmpty()) { + LinkedHashSet woken = null; + for (Operator op : pendingWakeOps) { + final LinkedHashSet bucket = parkedByOp.get(op); + if (bucket != null) { + if (woken == null) { + woken = new LinkedHashSet<>(); + } + woken.addAll(bucket); + } + } + if (woken != null) { + for (RuleAppContainer c : woken) { + unindexParked(c); + } + var time = System.nanoTime(); + try { + queue = queue.insert(woken.iterator()); + } finally { + PERF_QUEUE_OPS.addAndGet(System.nanoTime() - time); + } + } + } + pendingWakeOps.clear(); + } + + /** Remove a woken container from every operator bucket it was parked under. */ + private void unindexParked(RuleAppContainer c) { + if (parkedByOp == null || !(c instanceof TacletAppContainer tac)) { + return; + } + final List ops = assumesWakeOps(tac); + if (ops == null) { + return; + } + for (Operator op : ops) { + final LinkedHashSet bucket = parkedByOp.get(op); + if (bucket != null) { + bucket.remove(c); + if (bucket.isEmpty()) { + parkedByOp.remove(op); + } + } + } + } + + /** + * The concrete top operator(s) of the {@code \assumes} formulas of the given base, resolved + * through the find-match's schema-variable instantiations. + * + * @return the wake operators, or {@code null} if the base is not effectively indexable + * -- i.e. some {@code \assumes} formula has a top that is an unbound schema variable + * (it + * would match any formula, so no precise wake operator exists) or has no + * {@code \assumes} + * formulas at all + */ + private static @Nullable List assumesWakeOps(TacletAppContainer base) { + final NoPosTacletApp app = base.getTacletApp(); + final Sequent assumesSeq = app.taclet().assumesSequent(); + if (assumesSeq.isEmpty()) { + return null; + } + final SVInstantiations insts = app.instantiations(); + final List ops = new ArrayList<>(assumesSeq.size()); + for (SequentFormula sf : assumesSeq) { + Operator op = sf.formula().op(); + if (op instanceof SchemaVariable sv) { + final Object inst = insts.getInstantiation(sv); + if (!(inst instanceof JTerm instTerm)) { + return null; // unbound (or non-term) generic top -> not indexable + } + op = instTerm.op(); + if (op instanceof SchemaVariable) { + return null; // still schematic -> not indexable + } + } + ops.add(op); + } + return ops; + } + + /** + * Record, for the next round's wake-up, the top operators of every formula added or modified by + * this sequent change. The assumes matcher strips the update context before matching, so the + * whole update-prefix spine of each changed formula is recorded -- a sound superset of the + * operators a parked base could match (see {@link #parkedByOp}). Called by {@code Goal} on + * every + * sequent change. + */ + public void sequentChanged(SequentChangeInfo sci) { + recordWakeOps(sci.addedFormulas(true)); + recordWakeOps(sci.addedFormulas(false)); + recordModifiedWakeOps(sci.modifiedFormulas(true)); + recordModifiedWakeOps(sci.modifiedFormulas(false)); + } + + private void recordWakeOps(ImmutableList added) { + for (SequentFormula sf : added) { + recordSpineOps(sf.formula()); + } + } + + private void recordModifiedWakeOps(ImmutableList modified) { + for (FormulaChangeInfo fci : modified) { + recordSpineOps(fci.newFormula().formula()); + } + } + + /** Add the operators along a formula's update-application spine to {@link #pendingWakeOps}. */ + private void recordSpineOps(org.key_project.logic.Term formula) { + if (pendingWakeOps == null) { + pendingWakeOps = new LinkedHashSet<>(); + } + org.key_project.logic.Term t = formula; + while (true) { + final Operator op = t.op(); + pendingWakeOps.add(op); + if (op instanceof UpdateApplication && t instanceof JTerm jt) { + t = UpdateApplication.getTarget(jt); + } else { + break; + } + } + } + @Override public RuleApplicationManager copy() { // noinspection unchecked @@ -393,6 +636,19 @@ public Object clone() { QueueRuleApplicationManager res = new QueueRuleApplicationManager(); res.queue = queue; res.previousMinimum = previousMinimum; + // the parking structures are mutable and goal-local: deep-copy so split goals park/wake + // independently (the contained containers and operators are immutable and shared) + if (parkedByOp != null) { + final LinkedHashMap> copy = + new LinkedHashMap<>(parkedByOp.size()); + for (Map.Entry> e : parkedByOp.entrySet()) { + copy.put(e.getKey(), new LinkedHashSet<>(e.getValue())); + } + res.parkedByOp = copy; + } + if (pendingWakeOps != null) { + res.pendingWakeOps = new LinkedHashSet<>(pendingWakeOps); + } return res; } From 4e8cccde39528c87ee1ffcf0e67c22ba59a572f1 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Tue, 16 Jun 2026 21:39:10 +0200 Subject: [PATCH 19/26] fix(rules): drop redundant order-fragile lenOfSeqSubEQ from automode lenOfSeqSubEQ rewrites seqLen(EQ) via an antecedent equation EQ = seqSub(...). It is redundant for completeness -- the direct lenOfSeqSub suffices (full RAP closes all 681 goals without it). It is also order-fragile: when the negated-goal equation seqSub(s,0,i)=seqSub(s,0,i+5) is reused as an \assumes to simplify, the simplification reproduces that same formula, and the duplicate/cycle guard then refuses to re-apply subSeqEqual -> the goal dead-ends. The original rule order avoided this by luck; any reordering (e.g. assumes-parking) can expose it. Only the \heuristics is commented out, so the taclet stays defined and existing proofs that applied it still load/replay. --- .../resources/de/uka/ilkd/key/proof/rules/sequence/seqRules.key | 2 +- .../src/test/resources/de/uka/ilkd/key/nparser/taclets.old.txt | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/sequence/seqRules.key b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/sequence/seqRules.key index 859e9bf83db..c5249415413 100644 --- a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/sequence/seqRules.key +++ b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/sequence/seqRules.key @@ -494,7 +494,7 @@ \replacewith(\if(from < to) \then(to - from) \else(0)) - \heuristics(simplify, find_term_not_in_assumes) + // \heuristics(simplify, find_term_not_in_assumes) \displayname "lenOfSeqSub" }; diff --git a/key.core/src/test/resources/de/uka/ilkd/key/nparser/taclets.old.txt b/key.core/src/test/resources/de/uka/ilkd/key/nparser/taclets.old.txt index 69b18dc9c6b..7c195cd9116 100644 --- a/key.core/src/test/resources/de/uka/ilkd/key/nparser/taclets.old.txt +++ b/key.core/src/test/resources/de/uka/ilkd/key/nparser/taclets.old.txt @@ -12185,7 +12185,6 @@ lenOfSeqSubEQ { \assumes ([equals(seqSub(seq,from,to),EQ)]==>[]) \find(seqLen(EQ)) \sameUpdateLevel\replacewith(if-then-else(lt(from,to),sub(to,from),Z(0(#)))) -\heuristics(find_term_not_in_assumes, simplify) Choices: sequences:on} ----------------------------------------------------- == lenOfSeqUpd (lenOfSeqUpd) ========================================= From 7d99ba57c7f5268479d6a4c83d2e6d133d273555 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Wed, 17 Jun 2026 00:04:23 +0200 Subject: [PATCH 20/26] perf(label): skip rebuilding unchanged term trees in removeIrrelevantLabels removeIrrelevantLabels rebuilt the whole term tree on every call (stream().map()/filter() .collect() per node), the single biggest allocator during proof search (~20%), even though most subterms have no irrelevant label. Replace with an identity-preserving rebuild (plain loops, lazy sub-array, return the original term when its subtree has no irrelevant label). Behaviour-preserving (terms are immutable; result is structurally identical). --- .../key/logic/label/TermLabelManager.java | 54 +++++++++++++++++-- 1 file changed, 49 insertions(+), 5 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/logic/label/TermLabelManager.java b/key.core/src/main/java/de/uka/ilkd/key/logic/label/TermLabelManager.java index f08bb194f98..b04df10fb86 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/logic/label/TermLabelManager.java +++ b/key.core/src/main/java/de/uka/ilkd/key/logic/label/TermLabelManager.java @@ -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.*; @@ -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 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 labels = term.getLabels(); + ImmutableArray 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); } } From 5dd981973b4349ab93e4d5b283ec116b4fbd8988 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Wed, 17 Jun 2026 00:04:23 +0200 Subject: [PATCH 21/26] perf(util): compute Pair.hashCode without a varargs array Objects.hash(first, second) allocates an Object[] on every call; Pair is heavily used as a hash-map key during proof search. Inline the same hash value without the array. --- .../src/main/java/org/key_project/util/collection/Pair.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/key.util/src/main/java/org/key_project/util/collection/Pair.java b/key.util/src/main/java/org/key_project/util/collection/Pair.java index 29a7d3f9393..bef98dfa5fd 100644 --- a/key.util/src/main/java/org/key_project/util/collection/Pair.java +++ b/key.util/src/main/java/org/key_project/util/collection/Pair.java @@ -55,7 +55,10 @@ public boolean equals(@Nullable Object o) { @Override public int hashCode() { - return Objects.hash(first, second); + // Same value as Objects.hash(first, second) but without allocating the varargs Object[] + // on every call (Pair is heavily used as a hash-map key during proof search). + int result = 31 + (first == null ? 0 : first.hashCode()); + return 31 * result + (second == null ? 0 : second.hashCode()); } /////////////////////////////////////////////////////////// From 701da963a44e914d3fa83f1f52ff1c1d503c7766 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Wed, 17 Jun 2026 00:04:23 +0200 Subject: [PATCH 22/26] perf(strategy): walk the find-position by index in RewriteTacletExecutor applyReplacewithHelper allocated a PiTIterator (posInTerm().iterator()) per rewrite-taclet application and consumed it in the recursive replace(). Thread the PosInTerm + a depth index instead (same indices/order), avoiding the per-application iterator object. --- .../executor/javadl/RewriteTacletExecutor.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/executor/javadl/RewriteTacletExecutor.java b/key.core/src/main/java/de/uka/ilkd/key/rule/executor/javadl/RewriteTacletExecutor.java index 9f5ffd143d1..76a204ec8aa 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/executor/javadl/RewriteTacletExecutor.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/executor/javadl/RewriteTacletExecutor.java @@ -15,7 +15,7 @@ import de.uka.ilkd.key.rule.TacletApp; import de.uka.ilkd.key.rule.tacletbuilder.RewriteTacletGoalTemplate; -import org.key_project.logic.IntIterator; +import org.key_project.logic.PosInTerm; import org.key_project.logic.sort.Sort; import org.key_project.prover.rules.ApplicationRestriction; import org.key_project.prover.rules.instantiation.MatchResultInfo; @@ -36,18 +36,20 @@ public RewriteTacletExecutor(RewriteTaclet taclet) { */ private JTerm replace(JTerm term, JTerm with, TermLabelState termLabelState, TacletLabelHint labelHint, PosInOccurrence posOfFind, - IntIterator it, + PosInTerm pit, int depthIdx, MatchResultInfo mc, Sort maxSort, Goal goal, Services services, TacletApp ruleApp) { - if (it.hasNext()) { - final int indexOfNextSubTerm = it.next(); + // walk the find-position by index instead of via PosInTerm.iterator(), to avoid allocating + // a PiTIterator per rule application (same indices/order as the forward iterator). + if (depthIdx < pit.depth()) { + final int indexOfNextSubTerm = pit.getIndexAt(depthIdx); final JTerm[] subs = new JTerm[term.arity()]; term.subs().arraycopy(0, subs, 0, term.arity()); final Sort newMaxSort = TermHelper.getMaxSort(term, indexOfNextSubTerm); subs[indexOfNextSubTerm] = replace(term.sub(indexOfNextSubTerm), with, termLabelState, - labelHint, posOfFind, it, mc, newMaxSort, goal, services, ruleApp); + labelHint, posOfFind, pit, depthIdx + 1, mc, newMaxSort, goal, services, ruleApp); return services.getTermFactory().createTerm(term.op(), subs, term.boundVars(), term.getLabels()); @@ -71,11 +73,10 @@ private SequentFormula applyReplacewithHelper(Goal goal, MatchResultInfo matchCond, TacletApp ruleApp) { final JTerm term = (JTerm) posOfFind.sequentFormula().formula(); - final IntIterator it = posOfFind.posInTerm().iterator(); final JTerm rwTemplate = gt.replaceWith(); JTerm formula = replace(term, rwTemplate, termLabelState, new TacletLabelHint(rwTemplate), - posOfFind, it, matchCond, term.sort(), goal, services, ruleApp); + posOfFind, posOfFind.posInTerm(), 0, matchCond, term.sort(), goal, services, ruleApp); formula = TermLabelManager.refactorSequentFormula(termLabelState, services, formula, posOfFind, taclet, goal, null, rwTemplate); if (term == formula) { From da74aaa92f142fc85594939faf71a7fe28f1dd56 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Wed, 17 Jun 2026 00:04:37 +0200 Subject: [PATCH 23/26] perf(loader): release the ANTLR parser DFA caches after loading The KeY and JML ANTLR parsers build a prediction (DFA) cache lazily while parsing, held on the generated parsers' static fields, so it stays resident for the whole JVM -- including the (long) proof search, where it is unused (~17 MB retained on a large proof). It is a pure cache that ANTLR rebuilds transparently on the next parse, so dropping it after a problem/proof has finished loading is correctness-safe. Add ParsingFacade.clearParserCaches() (KeY/JavaDL) and JmlFacade.clearCaches() (JML) and call them from AbstractProblemLoader.load(). --- .../de/uka/ilkd/key/nparser/ParsingFacade.java | 18 ++++++++++++++++++ .../key/proof/io/AbstractProblemLoader.java | 7 +++++++ .../uka/ilkd/key/speclang/njml/JmlFacade.java | 15 +++++++++++++++ 3 files changed, 40 insertions(+) diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java index b86472b9215..f5249b0f4a3 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java @@ -112,6 +112,24 @@ private static JavaKeYParser createParser(CharStream stream) { return createParser(createLexer(stream)); } + /** + * Releases the ANTLR prediction (DFA) cache of the KeY parser. + *

+ * 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)); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/io/AbstractProblemLoader.java b/key.core/src/main/java/de/uka/ilkd/key/proof/io/AbstractProblemLoader.java index e4462551b58..9d311ee8108 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/io/AbstractProblemLoader.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/io/AbstractProblemLoader.java @@ -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; @@ -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; @@ -339,6 +341,11 @@ public final void load(Consumer 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(); } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java index 80b93be0ce9..c06f0143751 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java +++ b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java @@ -39,6 +39,21 @@ private JmlFacade() { return new JmlLexer(stream); } + /** + * Releases the ANTLR prediction (DFA) cache of the JML parser. It is a pure, lazily-built cache + * held on the generated parser's static fields, only needed while parsing, not during proof + * search; ANTLR rebuilds it transparently on the next parse. See + * {@code ParsingFacade.clearParserCaches}. + */ + public static void clearCaches() { + try { + new JmlParser(new CommonTokenStream(createLexer(CharStreams.fromString("")))) + .getInterpreter().clearDFA(); + } catch (RuntimeException ignored) { + // best-effort cache release; a failure here only forgoes the memory saving + } + } + /** * Creates a JML lexer for the given string with position. The position information of the lexer * is changed accordingly. From 2387c8e576708b43e646eb80b69808ff0d9babde Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Mon, 15 Jun 2026 17:46:57 +0200 Subject: [PATCH 24/26] perf(checkPrefix): skip the prefix walk when the formula has no transformer RewriteTaclet.checkPrefix walks the whole root-to-position prefix (PIOPathIterator) at every position it is asked about. During taclet-index construction / one-step simplification of a deep term that is O(depth) per position over ~d positions, i.e. O(d^2) -- the dominant cost on deeply nested terms such as chained if-then-else (a JFR profile showed 54% self-time in checkPrefix on a trivial 3-node proof). For an unrestricted (NONE) rewrite taclet -- the common case -- the only prefix-dependent outcome of that walk is a veto when a Transformer occurs on the path; the update/polarity/modality handling is guarded by a non-NONE restriction and the polarity is discarded. So if the formula provably contains no Transformer anywhere, no prefix can, and the walk can be skipped. "Formula contains a Transformer" is computed once and cached per term (JTerm.containsTransformerRecursive, mirroring containsJavaBlockRecursive), giving O(1) amortized and dropping the per-position prefix cost from O(depth) to O(1) in the transformer-free case; the O(d^2) on deep terms becomes O(d). Behaviour-preserving: it only short-circuits a provably-equivalent case; restricted taclets and transformer-bearing formulas still take the full walk. --- .../java/de/uka/ilkd/key/logic/JTerm.java | 10 +++++++++ .../java/de/uka/ilkd/key/logic/TermImpl.java | 22 +++++++++++++++++++ .../de/uka/ilkd/key/rule/RewriteTaclet.java | 8 +++++++ 3 files changed, 40 insertions(+) diff --git a/key.core/src/main/java/de/uka/ilkd/key/logic/JTerm.java b/key.core/src/main/java/de/uka/ilkd/key/logic/JTerm.java index dc84ea2b302..834f47ef7bb 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/logic/JTerm.java +++ b/key.core/src/main/java/de/uka/ilkd/key/logic/JTerm.java @@ -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. */ diff --git a/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java b/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java index 42063167176..505b7a872a5 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java +++ b/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java @@ -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 // ------------------------------------------------------------------------- @@ -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; + } + } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/RewriteTaclet.java b/key.core/src/main/java/de/uka/ilkd/key/rule/RewriteTaclet.java index 95de43b22f2..7d1cd26f955 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/RewriteTaclet.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/RewriteTaclet.java @@ -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. @@ -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 From 2271f36b1cb4e6c65b48d37a8dac5e325fc10cbb Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sat, 13 Jun 2026 19:30:10 +0200 Subject: [PATCH 25/26] test(perf): add perfTest measurement group (opt-in via -Dkey.runallproofs.runOnlyOn=perfTest) Adds the curated 6-problem perfTest group used for the combined benchmark. By default all runAllProofs groups run (full regression, like main); pass -Dkey.runallproofs.runOnlyOn=perfTest to restrict to the perfTest group, and -PrapForks=1 for clean serial timing. --- key.core/build.gradle | 3 +- .../proof/runallproofs/ProofCollections.java | 35 +++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/key.core/build.gradle b/key.core/build.gradle index cdc1edadef3..94de2268192 100644 --- a/key.core/build.gradle +++ b/key.core/build.gradle @@ -236,7 +236,8 @@ tasks.register("testRAP", Test) { dependsOn('generateRAPUnitTests', 'testClasses') forkEvery = 1 - // run the regression proofs on up to 10 parallel JVMs (overridable with -PrapForks=N) + // 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 { diff --git a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java index cdf13ad9404..a34f470e8d0 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java +++ b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java @@ -103,6 +103,14 @@ public static ProofCollection automaticJavaDL() throws IOException { */ // runOnlyOn = group1, group2 (the space after each comma is mandatory) // settings.setRunOnlyOn("performance, performancePOConstruction"); + // perf round 3: the perfTest group (defined below) is the curated measurement set. By + // default ALL groups run (full regression, exactly like main); pass + // -Dkey.runallproofs.runOnlyOn=perfTest to restrict to it, e.g. to reproduce the combined + // benchmark without running the whole suite. + String runOnly = System.getProperty("key.runallproofs.runOnlyOn", ""); + if (!runOnly.isBlank()) { + settings.setRunOnlyOn(runOnly); + } settings.setKeySettings(GenerateUnitTestsUtil.loadFromFile("automaticJAVADL.properties")); @@ -188,6 +196,33 @@ public static ProofCollection automaticJavaDL() throws IOException { // .provable("performance-test/updateSimplification/loop_5000.key"); + // ---------------------------------------------------------------------------------- + // Perf round 3 (cost-memoization / queue-redesign experiment) goal set. + // + // The 12 goals supplied for evaluating strategy-cost memoization, split into a + // development ("test") subset used while iterating on the change and a held-out + // ("validation") subset only consulted to confirm we did not overfit. The split is + // stratified across categories (heap lists, search/sort, arithmetic, information flow, + // static init) so each subset is representative. + // + // Selected to run via setRunOnlyOn("perfTest, perfValidation") above. REVERT before + // merging to main. + var perfTest = c.group("perfTest"); + perfTest.provable("heap/list_seq/SimplifiedLinkedList.remove.key"); + perfTest.provable("standard_key/arith/gemplusDecimal/add.key"); + perfTest.provable("heap/saddleback_search/Saddleback_search.key"); + perfTest.provable("standard_key/java_dl/symmArray.key"); + perfTest.provable("heap/coincidence_count/project.key"); + perfTest.provable("heap/list_seq/ArrayList.remove.1.key"); + + var perfValidation = c.group("perfValidation"); + perfValidation.provable("standard_key/java_dl/jml-information-flow.key"); + perfValidation.provable("heap/quicksort/sort.key"); + perfValidation.provable("heap/list/ArrayList_concatenate.key"); + perfValidation.provable("standard_key/arith/median.key"); + perfValidation.provable("standard_key/staticInitialisation/objectOfErroneousClass.key"); + perfValidation.provable("heap/removeDups/removeDup.key"); + // Tests for rule application restrictions var g = c.group("applicationRestrictions"); g.provable("heap/polarity_tests/wellformed1.key"); From 1d9806ac4d19b4d2e3886faf74ac0075e0df6ac6 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 21 Jun 2026 13:11:39 +0200 Subject: [PATCH 26/26] perf(matcher): compile \assumes formula matching (incl. Java programs) when the compiled matcher is selected When the compiled find-matcher is selected, the taclet's \assumes formulas were still matched by the interpreter (their Java program blocks via the monolithic program instruction). Build them with the same cursor-free compiled matcher (which also compiles the modality program, since the compiled back-end always uses the compiled program hook), falling back to the interpreter for any pattern the compiler has no head for. Opt out for A/B via -Dkey.matcher.interpreterAssumes. ~8% faster automode on a modality-heavy benchmark (behavior_run), byte-identical proofs. --- .../rule/match/vm/JavaMatchPlanBuilder.java | 14 +++++ .../key/rule/match/vm/VMTacletMatcher.java | 59 +++++++++++++------ 2 files changed, 54 insertions(+), 19 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java index 66dbdb8c27b..06471eb8003 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/JavaMatchPlanBuilder.java @@ -82,6 +82,20 @@ public static MatchProgram compiledProgram(JTerm pattern) { return planOrThrow(pattern, false).compile(); } + /** + * Like {@link #compiledProgram(JTerm)}, but returns {@code null} instead of throwing when the + * dispatch has no head for {@code pattern} (so the caller can fall back to the interpreter). + * Used for {@code \assumes} formulas, which are not guaranteed to be among the patterns the + * find-matcher coverage is validated against. + * + * @param pattern the find / assumes pattern + * @return the compiled matcher, or {@code null} if the pattern is not compilable + */ + public static @Nullable MatchProgram compiledProgramOrNull(JTerm pattern) { + final MatchPlan plan = buildPlan(pattern, false); + return plan == null ? null : plan.compile(); + } + private static MatchPlan planOrThrow(JTerm pattern, boolean programInstructions) { final MatchPlan plan = buildPlan(pattern, programInstructions); if (plan == null) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java index 36b3f2d2f1c..b0506e3f103 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/match/vm/VMTacletMatcher.java @@ -82,10 +82,18 @@ public class VMTacletMatcher implements TacletMatcher { + "(reload the proof to apply).", true); + /** + * System property ({@code -Dkey.matcher.interpreterAssumes=true}) forcing the interpreter for + * {@code \assumes} formula matching even when the compiled find-matcher is selected. The + * compiled matcher (incl. the Java program of a modality) is used for assumes by default; this + * is mainly for headless A/B comparison of the compiled-assumes extension. + */ + public static final String INTERPRETER_ASSUMES_PROPERTY = "key.matcher.interpreterAssumes"; + /** the matcher for the find expression of the taclet */ private final MatchProgram findMatchProgram; /** the matcher for the taclet's assumes formulas */ - private final HashMap assumesMatchPrograms = + private final HashMap assumesMatchPrograms = new HashMap<>(); /** @@ -121,35 +129,48 @@ public VMTacletMatcher(Taclet taclet) { boundVars = taclet.getBoundVariables(); varsNotFreeIn = taclet.varsNotFreeIn(); + // both back-ends are derived from the unified match-plan framework (one dispatch per + // construct, see JavaMatchPlanBuilder); the compiled matcher is the default, the + // interpreter is used only when explicitly selected (property/feature flag) or as the + // automatic fallback for a pattern the compiler does not handle + final boolean useInterpreter = Boolean.getBoolean(INTERPRETER_MATCHER_PROPERTY) + || FeatureSettings.isFeatureActivated(INTERPRETER_MATCHER_FEATURE); + if (taclet instanceof final FindTaclet findTaclet) { findExp = findTaclet.find(); ignoreTopLevelUpdates = taclet.ignoreTopLevelUpdates() && !(findExp.op() instanceof UpdateApplication); - // both back-ends are derived from the unified match-plan framework (one dispatch per - // construct, see JavaMatchPlanBuilder); the compiled matcher is the default, the - // interpreter is used only when explicitly selected (property/feature flag) or as the - // automatic fallback for a pattern the compiler does not handle - final VMProgramInterpreter interpreter = - new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(findExp)); - if (Boolean.getBoolean(INTERPRETER_MATCHER_PROPERTY) - || FeatureSettings.isFeatureActivated(INTERPRETER_MATCHER_FEATURE)) { - findMatchProgram = interpreter; - } else { - final MatchProgram compiled = JavaMatchPlanBuilder.compiledProgram(findExp); - findMatchProgram = compiled != null ? compiled : interpreter; - } - + findMatchProgram = matchProgramFor(findExp, useInterpreter); } else { ignoreTopLevelUpdates = false; findExp = null; findMatchProgram = null; } + // The taclet's \assumes formulas use the same back-end as the find: when the compiled + // matcher is selected they are compiled too (cursor-free, including the Java program of a + // modality), unless -Dkey.matcher.interpreterAssumes forces the interpreter for them. + final boolean assumesInterpreter = + useInterpreter || Boolean.getBoolean(INTERPRETER_ASSUMES_PROPERTY); for (final SequentFormula sf : assumesSequent) { assumesMatchPrograms.put(sf.formula(), - new VMProgramInterpreter( - JavaMatchPlanBuilder.interpreterProgram((JTerm) sf.formula()))); + matchProgramFor((JTerm) sf.formula(), assumesInterpreter)); + } + } + + /** + * Builds the matcher for a find / assumes {@code pattern}: the cursor-free compiled matcher + * unless the interpreter is requested or the compiler has no head for the pattern (then the + * interpreter is used as a fallback). + */ + private static MatchProgram matchProgramFor(JTerm pattern, boolean interpreter) { + if (!interpreter) { + final MatchProgram compiled = JavaMatchPlanBuilder.compiledProgramOrNull(pattern); + if (compiled != null) { + return compiled; + } } + return new VMProgramInterpreter(JavaMatchPlanBuilder.interpreterProgram(pattern)); } /** @@ -164,7 +185,7 @@ public VMTacletMatcher(Taclet taclet) { @NonNull Term p_template, @NonNull MatchResultInfo p_matchCond, @NonNull LogicServices p_services) { - VMProgramInterpreter interpreter = assumesMatchPrograms.get(p_template); + MatchProgram program = assumesMatchPrograms.get(p_template); final var mc = (MatchConditions) p_matchCond; ImmutableList resFormulas = ImmutableSLList.nil(); @@ -186,7 +207,7 @@ public VMTacletMatcher(Taclet taclet) { } if (formula != null) {// update context not present or update context match succeeded final MatchResultInfo newMC = - checkConditions(interpreter.match(formula, mc, p_services), p_services); + checkConditions(program.match(formula, mc, p_services), p_services); if (newMC != null) { resFormulas = resFormulas.prepend(cf);