From 5fdc180c8dd736ca9766eda64fa13331d76bfa66 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 13:29:51 +0200 Subject: [PATCH 01/20] Spike: taclet-generating transformers (lemma introduction rules) Instead of performing many proof steps programmatically (term transformers, built-in rules), a LemmaTacletGenerator packages the transformation as a specialized, metaconstruct-free taclet. A LemmaIntroductionRule (built-in) introduces the generated taclet goal-locally together with a soundness proof obligation created via the existing taclet lemma machinery; rewriting with the lemma is then an ordinary, separately recorded taclet application. Design properties validated by the end-to-end test: - two-step recording (builtin introduction + taclet app) saves and replays; replaying the introduction re-runs the deterministic generator, and the recorded taclet application resolves against the regenerated taclet by its content-derived name - the taclet carries a GeneratedLemmaJustification pointing to its soundness proof (not an axiom, not justified by the introducing rule) - per-proof GeneratedLemmaRegistry guarantees one taclet instance and one proof obligation per lemma name (reuse across branches and after pruning) - the soundness proof obligation closes automatically Toy generator: AddLiteralsLemmaGenerator folds sums of integer literals, mirroring the add_literals/#add system taclet whose right-hand side is computed by Java code at application time. The system taclet stays untouched. Created with AI tooling support --- .../uka/ilkd/key/proof/init/JavaProfile.java | 4 +- .../rule/lemma/AddLiteralsLemmaGenerator.java | 83 +++++++++++ .../AddLiteralsLemmaIntroductionRule.java | 22 +++ .../ilkd/key/rule/lemma/GeneratedLemma.java | 22 +++ .../lemma/GeneratedLemmaJustification.java | 57 ++++++++ .../ilkd/key/rule/lemma/GeneratedLemmaPO.java | 52 +++++++ .../rule/lemma/GeneratedLemmaRegistry.java | 123 +++++++++++++++++ .../key/rule/lemma/LemmaIntroductionRule.java | 93 +++++++++++++ .../key/rule/lemma/LemmaTacletGenerator.java | 66 +++++++++ .../key/rule/lemma/TestLemmaIntroduction.java | 130 ++++++++++++++++++ .../testcase/lemmagen/addLiterals.key | 1 + 11 files changed, 652 insertions(+), 1 deletion(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaIntroductionRule.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaPO.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java create mode 100644 key.core/src/test/resources/testcase/lemmagen/addLiterals.key diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java b/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java index 27fc080a30b..9d8941f418c 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java @@ -18,6 +18,7 @@ import de.uka.ilkd.key.rule.label.OriginTermLabelRefactoring; import de.uka.ilkd.key.rule.label.TermLabelPolicy; import de.uka.ilkd.key.rule.label.TermLabelRefactoring; +import de.uka.ilkd.key.rule.lemma.AddLiteralsLemmaIntroductionRule; import de.uka.ilkd.key.rule.merge.MergeRule; import de.uka.ilkd.key.smt.newsmt2.DefinedSymbolsHandler; import de.uka.ilkd.key.strategy.ModularJavaDLStrategyFactory; @@ -180,7 +181,8 @@ protected ImmutableList initBuiltInRules() { .prepend(LoopApplyHeadRule.INSTANCE).prepend(JmlAssertRule.ASSERT_INSTANCE) .prepend(JmlAssertRule.ASSUME_INSTANCE) .prepend(SetStatementRule.INSTANCE) - .prepend(ObserverToUpdateRule.INSTANCE); + .prepend(ObserverToUpdateRule.INSTANCE) + .prepend(AddLiteralsLemmaIntroductionRule.INSTANCE); // contract insertion rule, ATTENTION: ProofMgt relies on the fact // that Contract insertion rule is the FIRST element of this list! diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java new file mode 100644 index 00000000000..857700327d6 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java @@ -0,0 +1,83 @@ +/* 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.lemma; + +import java.math.BigInteger; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.ldt.IntegerLDT; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.AbstractTermTransformer; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.RewriteTaclet; +import de.uka.ilkd.key.rule.tacletbuilder.RewriteTacletBuilder; +import de.uka.ilkd.key.util.MiscTools; + +import org.key_project.logic.Name; +import org.key_project.logic.Term; +import org.key_project.prover.rules.RuleSet; +import org.key_project.prover.sequent.PosInOccurrence; + +import org.jspecify.annotations.NullMarked; + +/** + * Toy lemma generator used to evaluate the taclet-generating transformer approach: for a sum of + * two integer literals it produces the ground rewrite taclet {@code \find(i + j) \replacewith(k)} + * with {@code k} the literal denoting the sum. + * + *

+ * This covers the same ground as the system taclet {@code add_literals}, whose right-hand side is + * computed by the metaconstruct {@code #add} at application time and is therefore neither + * inspectable nor amenable to a soundness proof obligation. The taclets produced here are + * metaconstruct-free, so the standard lemma machinery applies. The system taclet remains in the + * rule base untouched; in particular it serves as the ground truth for discharging the generated + * proof obligations. + */ +@NullMarked +public final class AddLiteralsLemmaGenerator implements LemmaTacletGenerator { + + public static final AddLiteralsLemmaGenerator INSTANCE = new AddLiteralsLemmaGenerator(); + + private static final Name NAME = new Name("addLitLemma"); + + private AddLiteralsLemmaGenerator() { + } + + @Override + public Name name() { + return NAME; + } + + @Override + public boolean isApplicable(Goal goal, PosInOccurrence pio) { + final IntegerLDT integerLDT = + goal.proof().getServices().getTypeConverter().getIntegerLDT(); + final Term term = pio.subTerm(); + return term.op() == integerLDT.getAdd() + && term.sub(0).op() == integerLDT.getNumberSymbol() + && term.sub(1).op() == integerLDT.getNumberSymbol(); + } + + @Override + public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { + final Services services = goal.proof().getServices(); + final JTerm find = (JTerm) pio.subTerm(); + + final BigInteger left = + new BigInteger(AbstractTermTransformer.convertToDecimalString(find.sub(0), services)); + final BigInteger right = + new BigInteger(AbstractTermTransformer.convertToDecimalString(find.sub(1), services)); + final JTerm sum = services.getTermBuilder().zTerm(left.add(right).toString()); + + final RewriteTacletBuilder tb = new RewriteTacletBuilder<>(); + // the name is derived from the term content only, so that replay and reuse across + // branches resolve to the same taclet + tb.setName(MiscTools.toValidTacletName(NAME + "_" + left + "_" + right)); + tb.setDisplayName(NAME.toString()); + tb.setFind(find); + tb.addGoalTerm(sum); + tb.addRuleSet(new RuleSet(new Name("concrete"))); + return tb.getTaclet(); + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaIntroductionRule.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaIntroductionRule.java new file mode 100644 index 00000000000..a854de71e37 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaIntroductionRule.java @@ -0,0 +1,22 @@ +/* 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.lemma; + +import org.jspecify.annotations.NullMarked; + +/** + * Introduction rule for {@link AddLiteralsLemmaGenerator}: at a sum of two integer literals it + * introduces the ground rewrite taclet folding the sum, together with its soundness proof + * obligation. + */ +@NullMarked +public final class AddLiteralsLemmaIntroductionRule extends LemmaIntroductionRule { + + public static final AddLiteralsLemmaIntroductionRule INSTANCE = + new AddLiteralsLemmaIntroductionRule(); + + private AddLiteralsLemmaIntroductionRule() { + super(AddLiteralsLemmaGenerator.INSTANCE); + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java new file mode 100644 index 00000000000..654ebf157a6 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -0,0 +1,22 @@ +/* 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.lemma; + +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.RewriteTaclet; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * A taclet created by a {@link LemmaTacletGenerator} together with its soundness proof. + * + * @param taclet the generated taclet; within one proof there is exactly one taclet instance per + * lemma name (see {@link GeneratedLemmaRegistry}) + * @param soundnessProof the proof for the soundness proof obligation of the taclet, or + * {@code null} if the obligation could not be created + */ +@NullMarked +public record GeneratedLemma(RewriteTaclet taclet, @Nullable Proof soundnessProof) { +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java new file mode 100644 index 00000000000..c0419679862 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java @@ -0,0 +1,57 @@ +/* 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.lemma; + +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.mgt.RuleJustification; + +import org.key_project.logic.Name; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Justification for a taclet created by a {@link LemmaTacletGenerator}: the taclet is a lemma + * whose validity is established by a dedicated soundness proof, not an axiom and not a consequence + * of the introducing rule application (which merely computed it). + */ +@NullMarked +public final class GeneratedLemmaJustification implements RuleJustification { + + private final Name generator; + private final @Nullable Proof soundnessProof; + + public GeneratedLemmaJustification(Name generator, @Nullable Proof soundnessProof) { + this.generator = generator; + this.soundnessProof = soundnessProof; + } + + @Override + public boolean isAxiomJustification() { + return false; + } + + /** + * returns the name of the generator that created the justified taclet + */ + public Name getGenerator() { + return generator; + } + + /** + * returns the soundness proof for the justified taclet, or {@code null} if no proof obligation + * could be created + */ + public @Nullable Proof getSoundnessProof() { + return soundnessProof; + } + + @Override + public String toString() { + return "generated lemma (by " + generator + ", " + + (soundnessProof == null ? "no soundness proof" + : (soundnessProof.closed() ? "proven" : "soundness proof open")) + + ")"; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaPO.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaPO.java new file mode 100644 index 00000000000..5c4bbfe0c13 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaPO.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 de.uka.ilkd.key.rule.lemma; + +import de.uka.ilkd.key.java.ast.abstraction.KeYJavaType; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.proof.init.ProofOblInput; + +import org.key_project.logic.Name; + +/** + * Proof obligation input for the soundness proof of a taclet created by a + * {@link LemmaTacletGenerator}. The proof obligation itself is created eagerly when the lemma is + * generated (see {@link GeneratedLemmaRegistry}); this class merely identifies it towards the + * proof environment. + */ +public class GeneratedLemmaPO implements ProofOblInput { + + private final Name tacletName; + private final ProofAggregate po; + + public GeneratedLemmaPO(Name tacletName, ProofAggregate po) { + this.tacletName = tacletName; + this.po = po; + } + + @Override + public String name() { + return "Taclet: " + tacletName; + } + + @Override + public void readProblem() { + // the proof obligation has already been constructed + } + + @Override + public ProofAggregate getPO() { + return po; + } + + @Override + public boolean implies(ProofOblInput po) { + return this == po; + } + + @Override + public KeYJavaType getContainerType() { + return null; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java new file mode 100644 index 00000000000..596ce6688a3 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java @@ -0,0 +1,123 @@ +/* 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.lemma; + +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.proof.init.InitConfig; +import de.uka.ilkd.key.proof.mgt.ProofEnvironment; +import de.uka.ilkd.key.rule.RewriteTaclet; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.taclettranslation.lemma.ProofObligationCreator; + +import org.key_project.logic.Name; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.DefaultImmutableSet; +import org.key_project.util.collection.ImmutableSet; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Proof-local registry of the taclets created by {@link LemmaTacletGenerator}s. + * + *

+ * The registry guarantees that per proof there is exactly one taclet instance (and one soundness + * proof obligation) per lemma name. This makes repeated introductions of the same lemma — on + * sibling branches, or after pruning and redoing the introduction — resolve to the already + * existing taclet instead of creating duplicates. Reusing the identical taclet instance is also + * required by the rule justification bookkeeping, which rejects re-registration of a distinct + * rule under an existing name. + */ +@NullMarked +public final class GeneratedLemmaRegistry { + + private final Map lemmas = new LinkedHashMap<>(); + + private GeneratedLemmaRegistry() { + } + + /** + * returns the registry of the given proof, creating it on first access + */ + public static GeneratedLemmaRegistry get(Proof proof) { + GeneratedLemmaRegistry registry = proof.lookup(GeneratedLemmaRegistry.class); + if (registry == null) { + registry = new GeneratedLemmaRegistry(); + proof.register(registry, GeneratedLemmaRegistry.class); + } + return registry; + } + + /** + * returns the lemma generated by the given generator for the term at the given position. If a + * lemma of the same name has been generated before, the existing one is returned; otherwise + * the taclet is generated, its soundness proof obligation is created and registered in the + * proof environment (if present), and the taclet is registered with a + * {@link GeneratedLemmaJustification}. + * + * @param goal the current goal + * @param pio the position of the term to be transformed + * @param generator the generator computing the lemma taclet + * @return the (possibly cached) generated lemma + */ + public GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, + LemmaTacletGenerator generator) { + final Proof proof = goal.proof(); + final RewriteTaclet taclet = generator.generate(goal, pio); + + final GeneratedLemma existing = lemmas.get(taclet.name()); + if (existing != null) { + return existing; + } + + final Proof soundnessProof = createSoundnessProof(taclet, proof); + final GeneratedLemma lemma = new GeneratedLemma(taclet, soundnessProof); + proof.getInitConfig().registerRule(taclet, + new GeneratedLemmaJustification(generator.name(), soundnessProof)); + lemmas.put(taclet.name(), lemma); + return lemma; + } + + /** + * returns the lemma registered under the given name, or {@code null} if there is none + */ + public @Nullable GeneratedLemma lookup(Name name) { + return lemmas.get(name); + } + + /** + * returns all lemmas generated for this proof so far + */ + public Collection getLemmas() { + return Collections.unmodifiableCollection(lemmas.values()); + } + + /** + * Creates the soundness proof obligation for the given taclet, reusing the standard taclet + * lemma machinery. The proof runs in a copy of the main proof's initial configuration; the + * generated taclet itself is not part of that configuration, so it cannot be used to prove + * itself. + */ + private static Proof createSoundnessProof(RewriteTaclet taclet, Proof mainProof) { + final InitConfig poConfig = mainProof.getInitConfig().deepCopy(); + final ImmutableSet tacletsToProve = DefaultImmutableSet.nil().add(taclet); + + final ProofAggregate po = new ProofObligationCreator().create(tacletsToProve, + new InitConfig[] { poConfig }, DefaultImmutableSet.nil(), List.of()); + + final ProofEnvironment env = mainProof.getEnv(); + if (env != null) { + env.registerProof(new GeneratedLemmaPO(taclet.name(), po), po); + } + return po.getFirstProof(); + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java new file mode 100644 index 00000000000..f4a28b7e31b --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java @@ -0,0 +1,93 @@ +/* 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.lemma; + +import de.uka.ilkd.key.logic.TermServices; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.BuiltInRule; +import de.uka.ilkd.key.rule.DefaultBuiltInRuleApp; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.inst.SVInstantiations; + +import org.key_project.logic.Name; +import org.key_project.prover.rules.RuleApp; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +/** + * Built-in rule that introduces a taclet computed by a {@link LemmaTacletGenerator} as a + * goal-local rule. + * + *

+ * The rule performs the introduction step only: it makes the generated taclet available + * on the resulting goal (registered with a {@link GeneratedLemmaJustification} pointing to its + * soundness proof obligation) but does not change the sequent. Actually rewriting with the lemma + * is an ordinary, separately recorded taclet application — this two-step structure is what makes + * the transformation inspectable and replayable. + * + *

+ * On proof replay, replaying an application of this rule re-runs the generator; since generators + * are deterministic in the term at the application position, the regenerated taclet carries the + * same name and the subsequent taclet application resolves against it. + */ +@NullMarked +public abstract class LemmaIntroductionRule implements BuiltInRule { + + private final LemmaTacletGenerator generator; + private final Name name; + + protected LemmaIntroductionRule(LemmaTacletGenerator generator) { + this.generator = generator; + this.name = new Name("introduce_" + generator.name()); + } + + public LemmaTacletGenerator getGenerator() { + return generator; + } + + @Override + public Name name() { + return name; + } + + @Override + public String toString() { + return displayName(); + } + + @Override + public boolean isApplicable(Goal goal, @Nullable PosInOccurrence pio) { + return pio != null && generator.isApplicable(goal, pio); + } + + @Override + public boolean isApplicableOnSubTerms() { + return true; + } + + @Override + public IBuiltInRuleApp createApp(@Nullable PosInOccurrence pos, TermServices services) { + return new DefaultBuiltInRuleApp<>(this, pos); + } + + @Override + public @NonNull ImmutableList apply(Goal goal, RuleApp ruleApp) { + final PosInOccurrence pio = ruleApp.posInOccurrence(); + final GeneratedLemma lemma = + GeneratedLemmaRegistry.get(goal.proof()).getOrCreate(goal, pio, generator); + + final ImmutableList newGoals = goal.split(1); + final Goal newGoal = newGoals.head(); + final NoPosTacletApp tacletApp = NoPosTacletApp.createFixedNoPosTacletApp(lemma.taclet(), + SVInstantiations.EMPTY_SVINSTANTIATIONS, goal.proof().getServices()); + assert tacletApp != null : "generated lemma taclet could not be turned into a taclet app"; + newGoal.addNoPosTacletApp(tacletApp); + return newGoals; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java new file mode 100644 index 00000000000..31712f8a36e --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java @@ -0,0 +1,66 @@ +/* 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.lemma; + +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.RewriteTaclet; + +import org.key_project.logic.Name; +import org.key_project.prover.sequent.PosInOccurrence; + +import org.jspecify.annotations.NullMarked; + +/** + * A generator that computes a specialized taclet capturing the effect of a programmatic + * transformation at a given position. Instead of performing the transformation directly on the + * proof (as {@link org.key_project.logic.op.TermTransformer term transformers} or many built-in + * rules do), the transformation is packaged as a taclet so that + *

+ * + * Implementations must obey the following contract: + * + */ +@NullMarked +public interface LemmaTacletGenerator { + + /** + * returns the unique name of this generator + */ + Name name(); + + /** + * checks whether this generator can compute a lemma taclet for the term at the given position + * + * @param goal the current goal + * @param pio the position of the term to be transformed + * @return true iff {@link #generate(Goal, PosInOccurrence)} will succeed at the given position + */ + boolean isApplicable(Goal goal, PosInOccurrence pio); + + /** + * computes the lemma taclet for the term at the given position. The result must be + * deterministic in the content of the term at the position (see the class-level contract). + * + * @param goal the current goal + * @param pio the position of the term to be transformed + * @return the generated taclet + */ + RewriteTaclet generate(Goal goal, PosInOccurrence pio); +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java new file mode 100644 index 00000000000..ee5c3a1ca91 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java @@ -0,0 +1,130 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.proof.mgt.RuleJustification; +import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; +import de.uka.ilkd.key.util.MiscTools; + +import org.key_project.logic.Name; +import org.key_project.logic.PosInTerm; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * End-to-end test for the taclet-generating transformer approach on the toy + * {@link AddLiteralsLemmaGenerator}: introduction of the generated taclet, its application, the + * soundness proof obligation, proof save/reload (replay), and reuse of the taclet after pruning. + */ +public class TestLemmaIntroduction { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + private static PosInOccurrence firstSuccedentSubTerm(Goal goal, int subTerm) { + final SequentFormula sf = goal.sequent().succedent().getFirst(); + return new PosInOccurrence(sf, PosInTerm.getTopLevel().down(subTerm), false); + } + + @Test + public void testIntroduceApplyProveReloadAndReuse() throws Exception { + final KeYEnvironment env = KeYEnvironment + .load(TEST_CASE_DIRECTORY.resolve("lemmagen").resolve("addLiterals.key")); + try { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + + // step 1: introduce the lemma taclet at "5 + 7" (left of the equality) + final Goal root = proof.openGoals().head(); + final PosInOccurrence addPos = firstSuccedentSubTerm(root, 0); + assertTrue(AddLiteralsLemmaIntroductionRule.INSTANCE.isApplicable(root, addPos)); + root.apply(AddLiteralsLemmaIntroductionRule.INSTANCE.createApp(addPos, + proof.getServices())); + + final Name lemmaName = MiscTools.toValidTacletName("addLitLemma_5_7"); + final Goal afterIntro = proof.openGoals().head(); + final NoPosTacletApp lemmaApp = afterIntro.indexOfTaclets().lookup(lemmaName); + assertNotNull(lemmaApp, "introduced taclet not found in goal-local taclet index"); + + // the taclet is justified as a generated lemma, not as an axiom + final RuleJustification justification = + proof.getInitConfig().getJustifInfo().getJustification(lemmaApp.taclet()); + assertInstanceOf(GeneratedLemmaJustification.class, justification); + assertFalse(justification.isAxiomJustification()); + + // step 2: apply the lemma; "5 + 7 = 12" becomes "12 = 12" + final PosInOccurrence applyPos = firstSuccedentSubTerm(afterIntro, 0); + final NoPosTacletApp matched = + lemmaApp.matchFind(applyPos, proof.getServices()); + assertNotNull(matched, "generated taclet does not match its origin term"); + final TacletApp positioned = + matched.setPosInOccurrence(applyPos, proof.getServices()); + afterIntro.apply(positioned); + + final Goal afterApply = proof.openGoals().head(); + final JTerm equality = + (JTerm) afterApply.sequent().succedent().getFirst().formula(); + assertEquals(equality.sub(1), equality.sub(0), + "lemma application should fold 5 + 7 to the literal 12"); + + // the soundness proof obligation exists and is provable + final GeneratedLemma lemma = GeneratedLemmaRegistry.get(proof).lookup(lemmaName); + assertNotNull(lemma); + final Proof soundnessProof = lemma.soundnessProof(); + assertNotNull(soundnessProof); + new AutomaticProver().start(soundnessProof, 1000, 30000); + assertTrue(soundnessProof.closed(), "soundness proof obligation did not close"); + + // save and reload: replaying the introduction regenerates the taclet, the recorded + // taclet application resolves against it by name + final Path proofFile = Files.createTempFile("addLitLemma", ".proof"); + try { + final String saveError = new ProofSaver(proof, proofFile).save(); + assertNull(saveError); + final KeYEnvironment reloadedEnv = + KeYEnvironment.load(proofFile); + try { + final Proof reloaded = reloadedEnv.getLoadedProof(); + assertNotNull(reloaded); + assertEquals(proof.countNodes(), reloaded.countNodes(), + "reloaded proof does not replay to the same size"); + } finally { + reloadedEnv.dispose(); + } + } finally { + Files.deleteIfExists(proofFile); + } + + // prune to the root and re-introduce: the identical taclet instance is reused, + // no second justification or proof obligation is created + proof.pruneProof(proof.root()); + final Goal pruned = proof.openGoals().head(); + pruned.apply(AddLiteralsLemmaIntroductionRule.INSTANCE.createApp( + firstSuccedentSubTerm(pruned, 0), proof.getServices())); + final NoPosTacletApp reintroduced = + proof.openGoals().head().indexOfTaclets().lookup(lemmaName); + assertNotNull(reintroduced); + assertSame(lemma.taclet(), reintroduced.taclet(), + "re-introduction after pruning must reuse the cached taclet instance"); + assertEquals(1, GeneratedLemmaRegistry.get(proof).getLemmas().size()); + } finally { + env.dispose(); + } + } +} diff --git a/key.core/src/test/resources/testcase/lemmagen/addLiterals.key b/key.core/src/test/resources/testcase/lemmagen/addLiterals.key new file mode 100644 index 00000000000..d9ca26631b9 --- /dev/null +++ b/key.core/src/test/resources/testcase/lemmagen/addLiterals.key @@ -0,0 +1 @@ +\problem { 5 + 7 = 12 } From 726def6e40165cc433174b374a23351907a3426a Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 13:48:46 +0200 Subject: [PATCH 02/20] Lemma cleanup: lazy soundness POs off the search path, proof status tracking Two refinements to the taclet-generating transformer spike: 1. The soundness proof obligation of a generated lemma is no longer created during the introducing rule application but lazily on first request (GeneratedLemma.getOrCreateSoundnessProof). Rule application - and thus proof search - now only pays for building the taclet and inserting it into the goal-local index; PO creation and discharge become a deferrable batch concern. (A name-recorder probe showed the *AtPre newnames on the introduction node predate the rule application - they are load-time proposals flushed by the first applied rule, pre-existing KeY behavior unrelated to PO creation.) 2. ProofCorrectnessMgt now tracks generated lemmas: a closed proof that used a lemma taclet whose soundness proof is missing, disposed, or open is downgraded to CLOSED_BUT_LEMMAS_LEFT (transitively through the soundness proofs, which live in copied init configs and are inspected directly). The pass runs before the contract fixpoint so dependent contract proofs observe the downgrade. Fixed along the way: ProofPruner removes the justification of goal-locally introduced taclets on pruning; the registry now re-registers the identical justification instance when a cached lemma is re-introduced. The end-to-end test covers laziness, replay-before-PO, the pruning path, and both proof status transitions. Created with AI tooling support --- .../key/proof/mgt/ProofCorrectnessMgt.java | 35 +++++++ .../ilkd/key/rule/lemma/GeneratedLemma.java | 91 ++++++++++++++++++- .../lemma/GeneratedLemmaJustification.java | 32 +++++-- .../rule/lemma/GeneratedLemmaRegistry.java | 52 +++-------- .../key/rule/lemma/TestLemmaIntroduction.java | 72 ++++++++++----- 5 files changed, 208 insertions(+), 74 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/ProofCorrectnessMgt.java b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/ProofCorrectnessMgt.java index 41ee259f925..299e45a0b07 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/ProofCorrectnessMgt.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/ProofCorrectnessMgt.java @@ -16,6 +16,7 @@ import de.uka.ilkd.key.proof.RuleAppListener; import de.uka.ilkd.key.proof.init.ContractPO; import de.uka.ilkd.key.proof.reference.ClosedBy; +import de.uka.ilkd.key.rule.lemma.GeneratedLemmaJustification; import de.uka.ilkd.key.speclang.Contract; import org.key_project.prover.rules.RuleApp; @@ -196,6 +197,15 @@ public void updateProofStatus() { } } + // revert status of all "presumably closed" proofs that use a generated lemma taclet + // whose soundness proof obligation has not been created or has not been closed + for (Proof p : presumablyClosed) { + if (hasUnprovenGeneratedLemma(p, new LinkedHashSet<>())) { + p.mgt().proofStatus = ProofStatus.CLOSED_BUT_LEMMAS_LEFT; + presumablyClosed = presumablyClosed.remove(p); + } + } + // revert status of all "presumably closed" proofs for which at least one // used contract is definitely not proven to "lemmas left" boolean changed = true; @@ -224,6 +234,31 @@ public void updateProofStatus() { } + /** + * Checks (transitively through the soundness proofs) whether the given proof relies on a + * taclet generated by a {@link de.uka.ilkd.key.rule.lemma.LemmaTacletGenerator} whose + * soundness proof obligation has not been created, has been disposed, or is not closed. + * Soundness proofs live in a copied initial configuration and are therefore inspected + * directly instead of via their (possibly never updated) proof status. + */ + private static boolean hasUnprovenGeneratedLemma(Proof p, Set visited) { + if (!visited.add(p)) { + return false; + } + for (RuleApp ruleApp : p.mgt().cachedRuleApps) { + if (p.mgt().getJustification(ruleApp) instanceof GeneratedLemmaJustification just) { + final Proof soundnessProof = just.getSoundnessProof(); + if (soundnessProof == null || soundnessProof.isDisposed() + || !soundnessProof.closed() + || hasUnprovenGeneratedLemma(soundnessProof, visited)) { + return true; + } + } + } + return false; + } + + public void ruleApplied(RuleApp r) { RuleJustification rj = getJustification(r); if (rj == null) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java index 654ebf157a6..9527372f8bb 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -3,8 +3,19 @@ * SPDX-License-Identifier: GPL-2.0-only */ package de.uka.ilkd.key.rule.lemma; +import java.util.List; + import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.proof.init.InitConfig; +import de.uka.ilkd.key.proof.mgt.ProofEnvironment; import de.uka.ilkd.key.rule.RewriteTaclet; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.taclettranslation.lemma.ProofObligationCreator; + +import org.key_project.logic.Name; +import org.key_project.util.collection.DefaultImmutableSet; +import org.key_project.util.collection.ImmutableSet; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -12,11 +23,81 @@ /** * A taclet created by a {@link LemmaTacletGenerator} together with its soundness proof. * - * @param taclet the generated taclet; within one proof there is exactly one taclet instance per - * lemma name (see {@link GeneratedLemmaRegistry}) - * @param soundnessProof the proof for the soundness proof obligation of the taclet, or - * {@code null} if the obligation could not be created + *

+ * The soundness proof obligation is created lazily on first request and in particular + * not during the rule application that introduces the taclet: proof search must not pay + * for the proof obligation, and rule applications should not have side effects beyond the proof + * tree. As long as the soundness proof has not been created (or not been closed), any proof using + * the taclet is only closed modulo this lemma (see + * {@link de.uka.ilkd.key.proof.mgt.ProofCorrectnessMgt}). */ @NullMarked -public record GeneratedLemma(RewriteTaclet taclet, @Nullable Proof soundnessProof) { +public final class GeneratedLemma { + + private final RewriteTaclet taclet; + private final Proof mainProof; + private final GeneratedLemmaJustification justification; + private @Nullable Proof soundnessProof; + + GeneratedLemma(RewriteTaclet taclet, Proof mainProof, Name generatorName) { + this.taclet = taclet; + this.mainProof = mainProof; + this.justification = new GeneratedLemmaJustification(generatorName, this); + } + + /** + * returns the justification of the taclet; the identical instance is (re-)registered whenever + * the lemma is introduced, since pruning removes the justification together with the + * goal-locally introduced taclet + */ + public GeneratedLemmaJustification justification() { + return justification; + } + + /** + * returns the generated taclet; within one proof there is exactly one taclet instance per + * lemma name (see {@link GeneratedLemmaRegistry}) + */ + public RewriteTaclet taclet() { + return taclet; + } + + /** + * returns the soundness proof if it has already been created, and {@code null} otherwise; the + * proof obligation is not created by this method + */ + public synchronized @Nullable Proof getSoundnessProofIfPresent() { + return soundnessProof; + } + + /** + * returns the proof for the soundness proof obligation of the taclet, creating (and, if a + * proof environment is present, registering) it on first call + */ + public synchronized Proof getOrCreateSoundnessProof() { + if (soundnessProof == null || soundnessProof.isDisposed()) { + soundnessProof = createSoundnessProof(); + } + return soundnessProof; + } + + /** + * Creates the soundness proof obligation for the taclet, reusing the standard taclet lemma + * machinery. The proof runs in a copy of the main proof's initial configuration; the + * generated taclet itself is not part of that configuration, so it cannot be used to prove + * itself. + */ + private Proof createSoundnessProof() { + final InitConfig poConfig = mainProof.getInitConfig().deepCopy(); + final ImmutableSet tacletsToProve = DefaultImmutableSet.nil().add(taclet); + + final ProofAggregate po = new ProofObligationCreator().create(tacletsToProve, + new InitConfig[] { poConfig }, DefaultImmutableSet.nil(), List.of()); + + final ProofEnvironment env = mainProof.getEnv(); + if (env != null) { + env.registerProof(new GeneratedLemmaPO(taclet.name(), po), po); + } + return po.getFirstProof(); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java index c0419679862..a55a547befd 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java @@ -20,11 +20,11 @@ public final class GeneratedLemmaJustification implements RuleJustification { private final Name generator; - private final @Nullable Proof soundnessProof; + private final GeneratedLemma lemma; - public GeneratedLemmaJustification(Name generator, @Nullable Proof soundnessProof) { + public GeneratedLemmaJustification(Name generator, GeneratedLemma lemma) { this.generator = generator; - this.soundnessProof = soundnessProof; + this.lemma = lemma; } @Override @@ -40,17 +40,35 @@ public Name getGenerator() { } /** - * returns the soundness proof for the justified taclet, or {@code null} if no proof obligation - * could be created + * returns the justified lemma + */ + public GeneratedLemma getLemma() { + return lemma; + } + + /** + * returns the soundness proof for the justified taclet, or {@code null} if it has not been + * created yet */ public @Nullable Proof getSoundnessProof() { - return soundnessProof; + return lemma.getSoundnessProofIfPresent(); + } + + /** + * returns true iff the soundness proof obligation of the justified taclet has been created + * and closed (possibly itself depending on further unproven lemmas, which is tracked by + * {@link de.uka.ilkd.key.proof.mgt.ProofCorrectnessMgt}) + */ + public boolean isProven() { + final Proof soundnessProof = lemma.getSoundnessProofIfPresent(); + return soundnessProof != null && !soundnessProof.isDisposed() && soundnessProof.closed(); } @Override public String toString() { + final Proof soundnessProof = lemma.getSoundnessProofIfPresent(); return "generated lemma (by " + generator + ", " - + (soundnessProof == null ? "no soundness proof" + + (soundnessProof == null ? "soundness proof obligation not yet created" : (soundnessProof.closed() ? "proven" : "soundness proof open")) + ")"; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java index 596ce6688a3..215a3e2dd14 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java @@ -6,22 +6,14 @@ import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; -import java.util.List; import java.util.Map; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; -import de.uka.ilkd.key.proof.ProofAggregate; -import de.uka.ilkd.key.proof.init.InitConfig; -import de.uka.ilkd.key.proof.mgt.ProofEnvironment; import de.uka.ilkd.key.rule.RewriteTaclet; -import de.uka.ilkd.key.rule.Taclet; -import de.uka.ilkd.key.taclettranslation.lemma.ProofObligationCreator; import org.key_project.logic.Name; import org.key_project.prover.sequent.PosInOccurrence; -import org.key_project.util.collection.DefaultImmutableSet; -import org.key_project.util.collection.ImmutableSet; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -60,29 +52,33 @@ public static GeneratedLemmaRegistry get(Proof proof) { /** * returns the lemma generated by the given generator for the term at the given position. If a * lemma of the same name has been generated before, the existing one is returned; otherwise - * the taclet is generated, its soundness proof obligation is created and registered in the - * proof environment (if present), and the taclet is registered with a - * {@link GeneratedLemmaJustification}. + * the taclet is generated and registered with a {@link GeneratedLemmaJustification}. The + * soundness proof obligation is not created here but lazily on first request (see + * {@link GeneratedLemma#getOrCreateSoundnessProof()}), keeping it off the proof search path. * * @param goal the current goal * @param pio the position of the term to be transformed * @param generator the generator computing the lemma taclet * @return the (possibly cached) generated lemma */ - public GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, + public synchronized GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, LemmaTacletGenerator generator) { final Proof proof = goal.proof(); final RewriteTaclet taclet = generator.generate(goal, pio); final GeneratedLemma existing = lemmas.get(taclet.name()); if (existing != null) { + // pruning removes the justification together with the goal-locally introduced + // taclet; re-register the identical justification instance in that case + if (proof.getInitConfig().getJustifInfo() + .getJustification(existing.taclet()) == null) { + proof.getInitConfig().registerRule(existing.taclet(), existing.justification()); + } return existing; } - final Proof soundnessProof = createSoundnessProof(taclet, proof); - final GeneratedLemma lemma = new GeneratedLemma(taclet, soundnessProof); - proof.getInitConfig().registerRule(taclet, - new GeneratedLemmaJustification(generator.name(), soundnessProof)); + final GeneratedLemma lemma = new GeneratedLemma(taclet, proof, generator.name()); + proof.getInitConfig().registerRule(taclet, lemma.justification()); lemmas.put(taclet.name(), lemma); return lemma; } @@ -90,34 +86,14 @@ public GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, /** * returns the lemma registered under the given name, or {@code null} if there is none */ - public @Nullable GeneratedLemma lookup(Name name) { + public synchronized @Nullable GeneratedLemma lookup(Name name) { return lemmas.get(name); } /** * returns all lemmas generated for this proof so far */ - public Collection getLemmas() { + public synchronized Collection getLemmas() { return Collections.unmodifiableCollection(lemmas.values()); } - - /** - * Creates the soundness proof obligation for the given taclet, reusing the standard taclet - * lemma machinery. The proof runs in a copy of the main proof's initial configuration; the - * generated taclet itself is not part of that configuration, so it cannot be used to prove - * itself. - */ - private static Proof createSoundnessProof(RewriteTaclet taclet, Proof mainProof) { - final InitConfig poConfig = mainProof.getInitConfig().deepCopy(); - final ImmutableSet tacletsToProve = DefaultImmutableSet.nil().add(taclet); - - final ProofAggregate po = new ProofObligationCreator().create(tacletsToProve, - new InitConfig[] { poConfig }, DefaultImmutableSet.nil(), List.of()); - - final ProofEnvironment env = mainProof.getEnv(); - if (env != null) { - env.registerProof(new GeneratedLemmaPO(taclet.name(), po), po); - } - return po.getFirstProof(); - } } diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java index ee5c3a1ca91..00dfa4cd24c 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaIntroduction.java @@ -12,6 +12,7 @@ import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.proof.mgt.ProofStatus; import de.uka.ilkd.key.proof.mgt.RuleJustification; import de.uka.ilkd.key.rule.NoPosTacletApp; import de.uka.ilkd.key.rule.TacletApp; @@ -30,8 +31,9 @@ /** * End-to-end test for the taclet-generating transformer approach on the toy - * {@link AddLiteralsLemmaGenerator}: introduction of the generated taclet, its application, the - * soundness proof obligation, proof save/reload (replay), and reuse of the taclet after pruning. + * {@link AddLiteralsLemmaGenerator}: introduction of the generated taclet, its application, proof + * save/reload (replay), reuse of the taclet after pruning, the lazily created soundness proof + * obligation, and the proof status tracking of unproven lemmas. */ public class TestLemmaIntroduction { @@ -42,6 +44,14 @@ private static PosInOccurrence firstSuccedentSubTerm(Goal goal, int subTerm) { return new PosInOccurrence(sf, PosInTerm.getTopLevel().down(subTerm), false); } + private static void applyLemma(Goal goal, NoPosTacletApp lemmaApp, PosInOccurrence pos, + Proof proof) { + final NoPosTacletApp matched = lemmaApp.matchFind(pos, proof.getServices()); + assertNotNull(matched, "generated taclet does not match its origin term"); + final TacletApp positioned = matched.setPosInOccurrence(pos, proof.getServices()); + goal.apply(positioned); + } + @Test public void testIntroduceApplyProveReloadAndReuse() throws Exception { final KeYEnvironment env = KeYEnvironment @@ -62,20 +72,18 @@ public void testIntroduceApplyProveReloadAndReuse() throws Exception { final NoPosTacletApp lemmaApp = afterIntro.indexOfTaclets().lookup(lemmaName); assertNotNull(lemmaApp, "introduced taclet not found in goal-local taclet index"); - // the taclet is justified as a generated lemma, not as an axiom + // the taclet is justified as a generated lemma, not as an axiom; the soundness + // proof obligation is not created during the rule application final RuleJustification justification = proof.getInitConfig().getJustifInfo().getJustification(lemmaApp.taclet()); - assertInstanceOf(GeneratedLemmaJustification.class, justification); - assertFalse(justification.isAxiomJustification()); + final GeneratedLemmaJustification lemmaJustification = + assertInstanceOf(GeneratedLemmaJustification.class, justification); + assertFalse(lemmaJustification.isAxiomJustification()); + assertNull(lemmaJustification.getSoundnessProof(), + "soundness proof obligation must not be created on the proof search path"); // step 2: apply the lemma; "5 + 7 = 12" becomes "12 = 12" - final PosInOccurrence applyPos = firstSuccedentSubTerm(afterIntro, 0); - final NoPosTacletApp matched = - lemmaApp.matchFind(applyPos, proof.getServices()); - assertNotNull(matched, "generated taclet does not match its origin term"); - final TacletApp positioned = - matched.setPosInOccurrence(applyPos, proof.getServices()); - afterIntro.apply(positioned); + applyLemma(afterIntro, lemmaApp, firstSuccedentSubTerm(afterIntro, 0), proof); final Goal afterApply = proof.openGoals().head(); final JTerm equality = @@ -83,16 +91,9 @@ public void testIntroduceApplyProveReloadAndReuse() throws Exception { assertEquals(equality.sub(1), equality.sub(0), "lemma application should fold 5 + 7 to the literal 12"); - // the soundness proof obligation exists and is provable - final GeneratedLemma lemma = GeneratedLemmaRegistry.get(proof).lookup(lemmaName); - assertNotNull(lemma); - final Proof soundnessProof = lemma.soundnessProof(); - assertNotNull(soundnessProof); - new AutomaticProver().start(soundnessProof, 1000, 30000); - assertTrue(soundnessProof.closed(), "soundness proof obligation did not close"); - // save and reload: replaying the introduction regenerates the taclet, the recorded - // taclet application resolves against it by name + // taclet application resolves against it by name; note that no soundness proof + // exists at this point - replay does not depend on it final Path proofFile = Files.createTempFile("addLitLemma", ".proof"); try { final String saveError = new ProofSaver(proof, proofFile).save(); @@ -112,17 +113,40 @@ public void testIntroduceApplyProveReloadAndReuse() throws Exception { } // prune to the root and re-introduce: the identical taclet instance is reused, - // no second justification or proof obligation is created + // no second justification is created proof.pruneProof(proof.root()); final Goal pruned = proof.openGoals().head(); pruned.apply(AddLiteralsLemmaIntroductionRule.INSTANCE.createApp( firstSuccedentSubTerm(pruned, 0), proof.getServices())); - final NoPosTacletApp reintroduced = - proof.openGoals().head().indexOfTaclets().lookup(lemmaName); + final Goal afterReintro = proof.openGoals().head(); + final NoPosTacletApp reintroduced = afterReintro.indexOfTaclets().lookup(lemmaName); assertNotNull(reintroduced); + final GeneratedLemma lemma = GeneratedLemmaRegistry.get(proof).lookup(lemmaName); + assertNotNull(lemma); assertSame(lemma.taclet(), reintroduced.taclet(), "re-introduction after pruning must reuse the cached taclet instance"); assertEquals(1, GeneratedLemmaRegistry.get(proof).getLemmas().size()); + + // close the main proof using the lemma + applyLemma(afterReintro, reintroduced, firstSuccedentSubTerm(afterReintro, 0), + proof); + new AutomaticProver().start(proof, 1000, 30000); + assertTrue(proof.closed()); + + // the proof relies on the yet unproven lemma: closed, but lemmas left + proof.mgt().updateProofStatus(); + assertEquals(ProofStatus.CLOSED_BUT_LEMMAS_LEFT, proof.mgt().getStatus(), + "proof status must report the unproven generated lemma"); + + // create and discharge the soundness proof obligation + final Proof soundnessProof = lemma.getOrCreateSoundnessProof(); + assertSame(soundnessProof, lemma.getSoundnessProofIfPresent()); + new AutomaticProver().start(soundnessProof, 1000, 30000); + assertTrue(soundnessProof.closed(), "soundness proof obligation did not close"); + + // now the proof is closed without remaining lemmas + proof.mgt().updateProofStatus(); + assertEquals(ProofStatus.CLOSED, proof.mgt().getStatus()); } finally { env.dispose(); } From f755a252e8ad0dd63c9d8cd7c514247e94bf819c Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 14:31:53 +0200 Subject: [PATCH 03/20] OneStepSimplifier: expose side-effect-free simplification core Extract a public entry point computeSimplification(Goal, PosInOccurrence, Protocol) returning the aggregated simplification result (simplified formula, number of applied rules, used context formulas) without modifying goal or proof. This is the OSS core computation exactly as apply() performs it, usable by clients that want to capture the aggregated transformation in a different form (e.g., as a generated lemma taclet). No change to the rule's own behavior; requires the simplifier to be active for the proof. Created with AI tooling support --- .../uka/ilkd/key/rule/OneStepSimplifier.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java index 35c6d0fd325..daed990cdc3 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java @@ -668,6 +668,51 @@ public String toString() { return displayName(); } + /** + * Result of a one-step-simplification computation performed by + * {@link #computeSimplification(Goal, PosInOccurrence, Protocol)}. + * + * @param simplified the fully simplified sequent formula + * @param numAppliedRules the number of rule applications aggregated in the result + * @param usedContextFormulas the positions of the context formulas used by replace-known + * steps (the assumptions the result depends on; empty if the simplification is a pure + * equivalence transformation) + */ + public record SimplificationResult(SequentFormula simplified, int numAppliedRules, + ImmutableList usedContextFormulas) { + } + + /** + * Computes the overall simplification result for the formula at the given position exactly as + * an application of this rule would, but without modifying the goal or the proof. This is the + * side-effect-free core of the one step simplifier, usable by clients that want to capture + * the aggregated transformation in a different form (e.g., as a generated lemma taclet). + * + *

+ * Requires the simplifier to be active for the goal's proof (see {@link #refreshOSS(Proof)}). + * + * @param goal the goal whose sequent provides the context formulas for replace-known + * @param pos the position of the (top level) formula to simplify + * @param protocol if non-null, the performed rule applications are reported to this protocol + * @return the simplification result, or {@code null} if the simplifier is not active for this + * proof or the formula cannot be simplified + */ + public synchronized SimplificationResult computeSimplification(Goal goal, + PosInOccurrence pos, Protocol protocol) { + if (!active || indices == null || goal.proof() != lastProof) { + return null; + } + // a rule app is required for term label instantiation during replace-known + final OneStepSimplifierRuleApp ruleApp = createApp(pos, goal.proof().getServices()); + final Instantiation inst = + computeInstantiation(pos, goal.sequent(), protocol, goal, ruleApp); + if (inst.getCf() == null || inst.getCf().equals(pos.sequentFormula())) { + return null; + } + return new SimplificationResult(inst.getCf(), inst.getNumAppliedRules(), + inst.getIfInsts()); + } + /** * Gets an immutable set containing all the taclets captured by the OSS. * From 5154c25196779fd3a90e61bd249e63c91031fe9f Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 14:31:53 +0200 Subject: [PATCH 04/20] OSS as lemma generator: capture one-step-simplifications as provable taclets OssLemmaGenerator packages an aggregated one-step-simplification F ~> F' as a ground rewrite taclet: the context formulas used by replace-known steps become an \assumes clause combined with \inSequentState (mirroring the discipline OSS itself follows: replace-known stops at update and modality boundaries); without replace-known the taclet is an unrestricted equivalence rewrite. Introduced interactively via OssLemmaIntroductionRule (registered in JavaProfile; automation-inert), reusing the LemmaIntroductionRule two-step recording. Taclet names are content-derived: SHA-256 over a canonical serialization of find, assumptions (with polarity), and replacewith - term hash codes are not guaranteed stable across JVM runs, and replay resolves taclets by name. Including the result means a changed simplifier outcome surfaces on reload as a missing taclet rather than a silently different rewrite. Generator applicability excludes formulas containing modal operators (outside the lemma PO fragment); update-laden formulas are supported - the end-to-end test discharges the proof obligation ({i:=1}(i=1)) <-> true, confirming UpdateApplication passes the taclet lemma translation. Second test covers the assumes-carrying case including assumption-instantiated application and save/reload replay of both steps. Created with AI tooling support --- .../uka/ilkd/key/proof/init/JavaProfile.java | 4 +- .../key/rule/lemma/OssLemmaGenerator.java | 171 ++++++++++++++ .../rule/lemma/OssLemmaIntroductionRule.java | 21 ++ .../rule/lemma/TestOssLemmaIntroduction.java | 214 ++++++++++++++++++ .../resources/testcase/lemmagen/ossLemma.key | 5 + .../testcase/lemmagen/ossLemmaAssumes.key | 6 + key.ui/lemmagen-demo.key.csv | 17 ++ lemmagen-demo.auto.0.proof | 82 +++++++ lemmagen-demo.auto.proof | 82 +++++++ lemmagen-demo.key | 16 ++ 10 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaIntroductionRule.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestOssLemmaIntroduction.java create mode 100644 key.core/src/test/resources/testcase/lemmagen/ossLemma.key create mode 100644 key.core/src/test/resources/testcase/lemmagen/ossLemmaAssumes.key create mode 100644 key.ui/lemmagen-demo.key.csv create mode 100644 lemmagen-demo.auto.0.proof create mode 100644 lemmagen-demo.auto.proof create mode 100644 lemmagen-demo.key diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java b/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java index 9d8941f418c..a92c6cae07d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/init/JavaProfile.java @@ -19,6 +19,7 @@ import de.uka.ilkd.key.rule.label.TermLabelPolicy; import de.uka.ilkd.key.rule.label.TermLabelRefactoring; import de.uka.ilkd.key.rule.lemma.AddLiteralsLemmaIntroductionRule; +import de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule; import de.uka.ilkd.key.rule.merge.MergeRule; import de.uka.ilkd.key.smt.newsmt2.DefinedSymbolsHandler; import de.uka.ilkd.key.strategy.ModularJavaDLStrategyFactory; @@ -182,7 +183,8 @@ protected ImmutableList initBuiltInRules() { .prepend(JmlAssertRule.ASSUME_INSTANCE) .prepend(SetStatementRule.INSTANCE) .prepend(ObserverToUpdateRule.INSTANCE) - .prepend(AddLiteralsLemmaIntroductionRule.INSTANCE); + .prepend(AddLiteralsLemmaIntroductionRule.INSTANCE) + .prepend(OssLemmaIntroductionRule.INSTANCE); // contract insertion rule, ATTENTION: ProofMgt relies on the fact // that Contract insertion rule is the FIRST element of this list! diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java new file mode 100644 index 00000000000..c6321d2f54f --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java @@ -0,0 +1,171 @@ +/* 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.lemma; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.calculus.JavaDLSequentKit; +import de.uka.ilkd.key.proof.io.OutputStreamProofSaver; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.RewriteTaclet; +import de.uka.ilkd.key.rule.tacletbuilder.RewriteTacletBuilder; +import de.uka.ilkd.key.util.MiscTools; + +import org.key_project.logic.Name; +import org.key_project.logic.Term; +import org.key_project.logic.op.Modality; +import org.key_project.prover.rules.ApplicationRestriction; +import org.key_project.prover.rules.RuleSet; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.NullMarked; + +/** + * Lemma generator capturing an aggregated one-step-simplification as a taclet: for a top level + * formula {@code F} that the {@link OneStepSimplifier} would simplify to {@code F'} using the + * context formulas {@code phi_1, ..., phi_k}, it produces the ground taclet + * + *

+ * ossLemma_<hash> {
+ *     \assumes (phi_a1, ... ==> phi_s1, ...)   // the used context formulas, by polarity
+ *     \find (F) \replacewith (F')
+ *     \inSequentState                            // only if the assumes clause is nonempty
+ * }
+ * 
+ * + * The pure rewrite steps aggregated by the simplifier are state-independent equivalences; the + * replace-known steps are only valid where the assumed formulas hold, which is exactly what the + * {@code \assumes} clause combined with the {@code \inSequentState} restriction enforces (the + * simplifier itself stops replace-known at update and modality boundaries). Without replace-known + * steps the taclet is an unrestricted equivalence rewrite. + * + *

+ * The taclet name is derived from a canonical serialization of find, assumptions, and + * replacewith, so replaying the introducing rule application reproduces the name, and a changed + * simplifier result surfaces as a missing taclet instead of a silently different rewrite. + * + *

+ * Formulas containing modal operators are not accepted: their taclets would fall outside the + * fragment supported by the taclet soundness proof obligation machinery. Symbolic execution + * formulas remain the business of the stock one step simplifier. + */ +@NullMarked +public final class OssLemmaGenerator implements LemmaTacletGenerator { + + public static final OssLemmaGenerator INSTANCE = new OssLemmaGenerator(); + + private static final Name NAME = new Name("ossLemma"); + + private OssLemmaGenerator() { + } + + @Override + public Name name() { + return NAME; + } + + @Override + public boolean isApplicable(Goal goal, PosInOccurrence pio) { + if (!pio.isTopLevel()) { + return false; + } + final OneStepSimplifier simplifier = MiscTools.findOneStepSimplifier(goal.proof()); + return simplifier != null && simplifier.isApplicable(goal, pio) + && !containsModality(pio.sequentFormula().formula()); + } + + @Override + public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { + final Services services = goal.proof().getServices(); + final OneStepSimplifier simplifier = MiscTools.findOneStepSimplifier(goal.proof()); + if (simplifier == null) { + throw new IllegalStateException( + "no one step simplifier found in profile of proof " + goal.proof().name()); + } + final OneStepSimplifier.SimplificationResult result = + simplifier.computeSimplification(goal, pio, new OneStepSimplifier.Protocol()); + if (result == null) { + throw new IllegalStateException("one step simplification of " + + pio.sequentFormula().formula() + + " failed (is the one step simplifier active for this proof?)"); + } + + final JTerm find = (JTerm) pio.sequentFormula().formula(); + final JTerm simplified = (JTerm) result.simplified().formula(); + + ImmutableList assumesAntec = ImmutableList.nil(); + ImmutableList assumesSucc = ImmutableList.nil(); + for (final PosInOccurrence context : result.usedContextFormulas()) { + if (context.isInAntec()) { + assumesAntec = assumesAntec.append(context.sequentFormula()); + } else { + assumesSucc = assumesSucc.append(context.sequentFormula()); + } + } + + final RewriteTacletBuilder tb = new RewriteTacletBuilder<>(); + tb.setName(MiscTools.toValidTacletName( + contentHashName(services, find, assumesAntec, assumesSucc, simplified))); + tb.setDisplayName(NAME.toString()); + tb.setFind(find); + tb.addGoalTerm(simplified); + tb.addRuleSet(new RuleSet(new Name("concrete"))); + if (!assumesAntec.isEmpty() || !assumesSucc.isEmpty()) { + tb.setAssumesSequent(JavaDLSequentKit.createSequent(assumesAntec, assumesSucc)); + tb.setApplicationRestriction( + new ApplicationRestriction(ApplicationRestriction.IN_SEQUENT_STATE)); + } + return tb.getTaclet(); + } + + private static boolean containsModality(Term term) { + if (term.op() instanceof Modality) { + return true; + } + for (final Term sub : term.subs()) { + if (containsModality(sub)) { + return true; + } + } + return false; + } + + /** + * Derives the taclet name from a canonical serialization of the taclet content. The printed + * form is used instead of hash codes of the terms, since the latter are not guaranteed to be + * stable across JVM runs, and replay resolves the taclet by name. + */ + private static String contentHashName(Services services, JTerm find, + ImmutableList assumesAntec, ImmutableList assumesSucc, + JTerm replacewith) { + final StringBuilder content = new StringBuilder(); + content.append("find:").append(OutputStreamProofSaver.printTerm(find, services)); + for (final SequentFormula sf : assumesAntec) { + content.append("\nassumeA:") + .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + } + for (final SequentFormula sf : assumesSucc) { + content.append("\nassumeS:") + .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + } + content.append("\nreplacewith:") + .append(OutputStreamProofSaver.printTerm(replacewith, services)); + + try { + final MessageDigest digest = MessageDigest.getInstance("SHA-256"); + final byte[] hash = digest.digest(content.toString().getBytes(StandardCharsets.UTF_8)); + return NAME + "_" + HexFormat.of().formatHex(hash, 0, 6); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("SHA-256 not available", e); + } + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaIntroductionRule.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaIntroductionRule.java new file mode 100644 index 00000000000..f3b85e02f44 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaIntroductionRule.java @@ -0,0 +1,21 @@ +/* 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.lemma; + +import org.jspecify.annotations.NullMarked; + +/** + * Introduction rule for {@link OssLemmaGenerator}: at a top level formula that the one step + * simplifier can simplify, it introduces the taclet capturing the aggregated simplification, + * together with its soundness proof obligation. + */ +@NullMarked +public final class OssLemmaIntroductionRule extends LemmaIntroductionRule { + + public static final OssLemmaIntroductionRule INSTANCE = new OssLemmaIntroductionRule(); + + private OssLemmaIntroductionRule() { + super(OssLemmaGenerator.INSTANCE); + } +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestOssLemmaIntroduction.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestOssLemmaIntroduction.java new file mode 100644 index 00000000000..7f05c3815c2 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestOssLemmaIntroduction.java @@ -0,0 +1,214 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.logic.Name; +import org.key_project.logic.PosInTerm; +import org.key_project.prover.rules.ApplicationRestriction; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * End-to-end tests for {@link OssLemmaGenerator}: aggregated one-step-simplifications are + * captured as generated taclets, their soundness proof obligations are created and discharged, + * and the proofs replay after save/reload. Covered are a context-free simplification of an + * update-laden formula (pure equivalence rewrite, proof obligation containing updates) and a + * replace-known simplification (taclet with {@code \assumes} clause and in-sequent-state + * restriction, applied with assumption instantiation). + */ +public class TestOssLemmaIntroduction { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + /** + * Applies the taclet of the given app at the given position, instantiating its assumptions + * from the goal's sequent if necessary, and returns the resulting open goal. + */ + private static Goal applyTaclet(Goal goal, NoPosTacletApp app, PosInOccurrence pos) { + final Proof proof = goal.proof(); + final NoPosTacletApp matched = app.matchFind(pos, proof.getServices()); + assertNotNull(matched, "taclet " + app.taclet().name() + " does not match at " + pos); + TacletApp positioned = matched.setPosInOccurrence(pos, proof.getServices()); + if (!positioned.complete()) { + final var instantiated = positioned + .findIfFormulaInstantiations(goal.sequent(), proof.getServices()); + assertFalse(instantiated.isEmpty(), + "no assumption instantiation found for " + app.taclet().name()); + positioned = instantiated.head(); + } + goal.apply(positioned); + return proof.openGoals().head(); + } + + @Test + public void testOssLemmaWithAssumptions() throws Exception { + final KeYEnvironment env = KeYEnvironment + .load(TEST_CASE_DIRECTORY.resolve("lemmagen").resolve("ossLemmaAssumes.key")); + try { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + OneStepSimplifier.refreshOSS(proof); + + // p -> ((q & true) & p) ==> after impRight ==> p ==> (q & true) & p + final Goal root = proof.openGoals().head(); + final NoPosTacletApp impRight = + root.indexOfTaclets().lookup(new Name("impRight")); + assertNotNull(impRight); + final Goal afterImp = applyTaclet(root, impRight, new PosInOccurrence( + root.sequent().succedent().getFirst(), PosInTerm.getTopLevel(), false)); + + // introduce the lemma at (q & true) & p; the simplification uses the context + // formula p (replace-known), so the taclet carries an assumes clause and the + // in-sequent-state restriction + final PosInOccurrence target = new PosInOccurrence( + afterImp.sequent().succedent().getFirst(), PosInTerm.getTopLevel(), false); + assertTrue(OssLemmaIntroductionRule.INSTANCE.isApplicable(afterImp, target)); + afterImp.apply( + OssLemmaIntroductionRule.INSTANCE.createApp(target, proof.getServices())); + + final GeneratedLemma lemma = + GeneratedLemmaRegistry.get(proof).getLemmas().iterator().next(); + assertFalse(lemma.taclet().assumesSequent().isEmpty(), + "replace-known was used, so the taclet must record its assumptions"); + assertEquals(1, lemma.taclet().assumesSequent().antecedent().size(), + "exactly the context formula p is assumed (in the antecedent)"); + assertTrue(lemma.taclet().applicationRestriction() + .matches(ApplicationRestriction.IN_SEQUENT_STATE), + "assumption-carrying lemmas must be restricted to the sequent state"); + + // apply the lemma (with assumption instantiation): the formula becomes q + final Goal afterIntro = proof.openGoals().head(); + final NoPosTacletApp lemmaApp = + afterIntro.indexOfTaclets().lookup(lemma.taclet().name()); + assertNotNull(lemmaApp); + final Goal afterApply = applyTaclet(afterIntro, lemmaApp, new PosInOccurrence( + afterIntro.sequent().succedent().getFirst(), PosInTerm.getTopLevel(), false)); + assertEquals("q", + afterApply.sequent().succedent().getFirst().formula().toString(), + "simplification under the assumption p should yield q"); + + // the soundness proof obligation p -> (((q & true) & p) <-> q) closes + final Proof soundnessProof = lemma.getOrCreateSoundnessProof(); + new AutomaticProver().start(soundnessProof, 1000, 30000); + assertTrue(soundnessProof.closed(), "soundness proof obligation did not close"); + + // save and reload: both the introduction and the assumption-instantiated + // taclet application replay + final Path proofFile = Files.createTempFile("ossLemmaAssumes", ".proof"); + try { + final String saveError = new ProofSaver(proof, proofFile).save(); + assertNull(saveError); + final KeYEnvironment reloadedEnv = + KeYEnvironment.load(proofFile); + try { + final Proof reloaded = reloadedEnv.getLoadedProof(); + assertNotNull(reloaded); + assertEquals(proof.countNodes(), reloaded.countNodes(), + "reloaded proof does not replay to the same size"); + } finally { + reloadedEnv.dispose(); + } + } finally { + Files.deleteIfExists(proofFile); + } + } finally { + env.dispose(); + } + } + + @Test + public void testOssLemmaOnUpdateFormula() throws Exception { + final KeYEnvironment env = KeYEnvironment + .load(TEST_CASE_DIRECTORY.resolve("lemmagen").resolve("ossLemma.key")); + try { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + OneStepSimplifier.refreshOSS(proof); + + // introduce the lemma at the top level succedent formula {i := 1}(i = 1) + final Goal root = proof.openGoals().head(); + final SequentFormula sf = root.sequent().succedent().getFirst(); + final PosInOccurrence topLevel = + new PosInOccurrence(sf, PosInTerm.getTopLevel(), false); + assertTrue(OssLemmaIntroductionRule.INSTANCE.isApplicable(root, topLevel), + "introduction rule not applicable - is the one step simplifier active?"); + root.apply( + OssLemmaIntroductionRule.INSTANCE.createApp(topLevel, proof.getServices())); + + // exactly one lemma has been generated; it is a context-free equivalence rewrite + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + assertEquals(1, registry.getLemmas().size()); + final GeneratedLemma lemma = registry.getLemmas().iterator().next(); + assertTrue(lemma.taclet().name().toString().startsWith("ossLemma_"), + "content-hash name expected, got: " + lemma.taclet().name()); + assertTrue(lemma.taclet().assumesSequent().isEmpty(), + "no replace-known steps involved, so the taclet must not have assumptions"); + assertNull(lemma.getSoundnessProofIfPresent()); + + // apply the lemma: the formula becomes true + final Goal afterIntro = proof.openGoals().head(); + final NoPosTacletApp lemmaApp = + afterIntro.indexOfTaclets().lookup(lemma.taclet().name()); + assertNotNull(lemmaApp, "introduced taclet not found in goal-local taclet index"); + final PosInOccurrence applyPos = new PosInOccurrence( + afterIntro.sequent().succedent().getFirst(), PosInTerm.getTopLevel(), false); + final NoPosTacletApp matched = lemmaApp.matchFind(applyPos, proof.getServices()); + assertNotNull(matched, "generated taclet does not match its origin formula"); + final TacletApp positioned = + matched.setPosInOccurrence(applyPos, proof.getServices()); + afterIntro.apply(positioned); + + final Goal afterApply = proof.openGoals().head(); + assertTrue( + afterApply.sequent().succedent().getFirst().formula().op() == proof.getServices() + .getTermBuilder().tt().op(), + "one step simplification of {i := 1}(i = 1) should yield true"); + + // the soundness proof obligation (with updates) is created and closes + final Proof soundnessProof = lemma.getOrCreateSoundnessProof(); + new AutomaticProver().start(soundnessProof, 1000, 30000); + assertTrue(soundnessProof.closed(), "soundness proof obligation did not close"); + + // save and reload: replaying the introduction re-runs the simplifier and + // regenerates the taclet under the same content-derived name + final Path proofFile = Files.createTempFile("ossLemma", ".proof"); + try { + final String saveError = new ProofSaver(proof, proofFile).save(); + assertNull(saveError); + final KeYEnvironment reloadedEnv = + KeYEnvironment.load(proofFile); + try { + final Proof reloaded = reloadedEnv.getLoadedProof(); + assertNotNull(reloaded); + assertEquals(proof.countNodes(), reloaded.countNodes(), + "reloaded proof does not replay to the same size"); + } finally { + reloadedEnv.dispose(); + } + } finally { + Files.deleteIfExists(proofFile); + } + } finally { + env.dispose(); + } + } +} diff --git a/key.core/src/test/resources/testcase/lemmagen/ossLemma.key b/key.core/src/test/resources/testcase/lemmagen/ossLemma.key new file mode 100644 index 00000000000..d06c70b7b00 --- /dev/null +++ b/key.core/src/test/resources/testcase/lemmagen/ossLemma.key @@ -0,0 +1,5 @@ +\programVariables { + int i; +} + +\problem { {i := 1} (i = 1) } diff --git a/key.core/src/test/resources/testcase/lemmagen/ossLemmaAssumes.key b/key.core/src/test/resources/testcase/lemmagen/ossLemmaAssumes.key new file mode 100644 index 00000000000..032536a8e07 --- /dev/null +++ b/key.core/src/test/resources/testcase/lemmagen/ossLemmaAssumes.key @@ -0,0 +1,6 @@ +\predicates { + p; + q; +} + +\problem { p -> ((q & true) & p) } diff --git a/key.ui/lemmagen-demo.key.csv b/key.ui/lemmagen-demo.key.csv new file mode 100644 index 00000000000..4b51c9a0a09 --- /dev/null +++ b/key.ui/lemmagen-demo.key.csv @@ -0,0 +1,17 @@ +open goals;0 +Nodes;10 +Branches;1 +Interactive steps;0 +Symbolic execution steps;0 +Automode time;32ms +Avg. time per step;3.555ms +Rule applications +Quantifier instantiations;0 +One-step Simplifier apps;3 +SMT solver apps;0 +Dependency Contract apps;0 +Operation Contract apps;0 +Block/Loop Contract apps;0 +Loop invariant apps;0 +Merge Rule apps;0 +Total rule apps;11 diff --git a/lemmagen-demo.auto.0.proof b/lemmagen-demo.auto.0.proof new file mode 100644 index 00000000000..7f06277b4d1 --- /dev/null +++ b/lemmagen-demo.auto.0.proof @@ -0,0 +1,82 @@ +\profile "Java Profile"; + +\settings { + "Choice" : { + "JavaCard" : "JavaCard:off", + "Strings" : "Strings:on", + "assertions" : "assertions:safe", + "bigint" : "bigint:on", + "finalFields" : "finalFields:immutable", + "floatRules" : "floatRules:strictfpOnly", + "initialisation" : "initialisation:disableStaticInitialisation", + "intRules" : "intRules:arithmeticSemanticsIgnoringOF", + "integerSimplificationRules" : "integerSimplificationRules:full", + "javaLoopTreatment" : "javaLoopTreatment:efficient", + "mergeGenerateIsWeakeningGoal" : "mergeGenerateIsWeakeningGoal:off", + "methodExpansion" : "methodExpansion:modularOnly", + "modelFields" : "modelFields:treatAsAxiom", + "moreSeqRules" : "moreSeqRules:off", + "permissions" : "permissions:on", + "programRules" : "programRules:Java", + "reach" : "reach:on", + "runtimeExceptions" : "runtimeExceptions:ban", + "sequences" : "sequences:on", + "soundDefaultContracts" : "soundDefaultContracts:on" + }, + "Labels" : { + "UseOriginLabels" : true + }, + "NewSMT" : { + + }, + "SMTSettings" : { + "SelectedTaclets" : [ + + ], + "UseBuiltUniqueness" : false, + "explicitTypeHierarchy" : false, + "instantiateHierarchyAssumptions" : true, + "integersMaximum" : 2147483645, + "integersMinimum" : -2147483645, + "invariantForall" : false, + "maxGenericSorts" : 2, + "useConstantsForBigOrSmallIntegers" : true, + "useUninterpretedMultiplication" : true + }, + "Strategy" : { + "ActiveStrategy" : "Modular JavaDL Strategy", + "MaximumNumberOfAutomaticApplications" : 10000, + "Timeout" : -1, + "options" : { + "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", + "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", + "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_FREE", + "DEP_OPTIONS_KEY" : "DEP_ON", + "LOOP_OPTIONS_KEY" : "LOOP_SCOPE_INV_TACLET", + "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", + "MPS_OPTIONS_KEY" : "MPS_MERGE", + "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_NONE", + "OSS_OPTIONS_KEY" : "OSS_ON", + "QUANTIFIERS_OPTIONS_KEY" : "QUANTIFIERS_NON_SPLITTING_WITH_PROGS", + "QUERYAXIOM_OPTIONS_KEY" : "QUERYAXIOM_ON", + "QUERY_NEW_OPTIONS_KEY" : "QUERY_OFF", + "SPLITTING_OPTIONS_KEY" : "SPLITTING_DELAYED", + "STOPMODE_OPTIONS_KEY" : "STOPMODE_DEFAULT", + "SYMBOLIC_EXECUTION_ALIAS_CHECK_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_ALIAS_CHECK_NEVER", + "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OFF", + "USER_TACLETS_OPTIONS_KEY1" : "USER_TACLETS_OFF", + "USER_TACLETS_OPTIONS_KEY2" : "USER_TACLETS_OFF", + "USER_TACLETS_OPTIONS_KEY3" : "USER_TACLETS_OFF", + "VBT_PHASE" : "VBT_SYM_EX" + } + } +} + + + +\problem { + add(Z(5(#)), Z(7(#))) = Z(2(1(#))) +& mul(Z(2(#)), add(Z(5(#)), Z(7(#)))) = Z(4(2(#))) +& add(add(Z(5(#)), Z(7(#))), Z(1(#))) = Z(3(1(#))) +} + diff --git a/lemmagen-demo.auto.proof b/lemmagen-demo.auto.proof new file mode 100644 index 00000000000..7f06277b4d1 --- /dev/null +++ b/lemmagen-demo.auto.proof @@ -0,0 +1,82 @@ +\profile "Java Profile"; + +\settings { + "Choice" : { + "JavaCard" : "JavaCard:off", + "Strings" : "Strings:on", + "assertions" : "assertions:safe", + "bigint" : "bigint:on", + "finalFields" : "finalFields:immutable", + "floatRules" : "floatRules:strictfpOnly", + "initialisation" : "initialisation:disableStaticInitialisation", + "intRules" : "intRules:arithmeticSemanticsIgnoringOF", + "integerSimplificationRules" : "integerSimplificationRules:full", + "javaLoopTreatment" : "javaLoopTreatment:efficient", + "mergeGenerateIsWeakeningGoal" : "mergeGenerateIsWeakeningGoal:off", + "methodExpansion" : "methodExpansion:modularOnly", + "modelFields" : "modelFields:treatAsAxiom", + "moreSeqRules" : "moreSeqRules:off", + "permissions" : "permissions:on", + "programRules" : "programRules:Java", + "reach" : "reach:on", + "runtimeExceptions" : "runtimeExceptions:ban", + "sequences" : "sequences:on", + "soundDefaultContracts" : "soundDefaultContracts:on" + }, + "Labels" : { + "UseOriginLabels" : true + }, + "NewSMT" : { + + }, + "SMTSettings" : { + "SelectedTaclets" : [ + + ], + "UseBuiltUniqueness" : false, + "explicitTypeHierarchy" : false, + "instantiateHierarchyAssumptions" : true, + "integersMaximum" : 2147483645, + "integersMinimum" : -2147483645, + "invariantForall" : false, + "maxGenericSorts" : 2, + "useConstantsForBigOrSmallIntegers" : true, + "useUninterpretedMultiplication" : true + }, + "Strategy" : { + "ActiveStrategy" : "Modular JavaDL Strategy", + "MaximumNumberOfAutomaticApplications" : 10000, + "Timeout" : -1, + "options" : { + "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", + "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", + "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_FREE", + "DEP_OPTIONS_KEY" : "DEP_ON", + "LOOP_OPTIONS_KEY" : "LOOP_SCOPE_INV_TACLET", + "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", + "MPS_OPTIONS_KEY" : "MPS_MERGE", + "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_NONE", + "OSS_OPTIONS_KEY" : "OSS_ON", + "QUANTIFIERS_OPTIONS_KEY" : "QUANTIFIERS_NON_SPLITTING_WITH_PROGS", + "QUERYAXIOM_OPTIONS_KEY" : "QUERYAXIOM_ON", + "QUERY_NEW_OPTIONS_KEY" : "QUERY_OFF", + "SPLITTING_OPTIONS_KEY" : "SPLITTING_DELAYED", + "STOPMODE_OPTIONS_KEY" : "STOPMODE_DEFAULT", + "SYMBOLIC_EXECUTION_ALIAS_CHECK_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_ALIAS_CHECK_NEVER", + "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OFF", + "USER_TACLETS_OPTIONS_KEY1" : "USER_TACLETS_OFF", + "USER_TACLETS_OPTIONS_KEY2" : "USER_TACLETS_OFF", + "USER_TACLETS_OPTIONS_KEY3" : "USER_TACLETS_OFF", + "VBT_PHASE" : "VBT_SYM_EX" + } + } +} + + + +\problem { + add(Z(5(#)), Z(7(#))) = Z(2(1(#))) +& mul(Z(2(#)), add(Z(5(#)), Z(7(#)))) = Z(4(2(#))) +& add(add(Z(5(#)), Z(7(#))), Z(1(#))) = Z(3(1(#))) +} + diff --git a/lemmagen-demo.key b/lemmagen-demo.key new file mode 100644 index 00000000000..7026896ab20 --- /dev/null +++ b/lemmagen-demo.key @@ -0,0 +1,16 @@ +// Demo for the taclet-generating transformer spike (branch bubel/lemma-taclet-spike). +// +// Right-click exactly on a "5 + 7" subterm and apply "introduce_addLitLemma": +// a new node appears, the sequent is unchanged, but the goal-local taclet +// addLitLemma_5_7 { \find(5 + 7) \replacewith(12) } is now available. +// Right-click "5 + 7" again to apply it like any ordinary taclet. +// +// The same single introduction serves all three occurrences (and, after andRight, +// all branches below the introduction node). Introducing again after pruning or +// on a parallel branch reuses the identical taclet - no duplicate is created. + +\problem { + 5 + 7 = 12 + & 2 * (5 + 7) = 24 + & (5 + 7) + 1 = 13 +} From f19a188dc30344be335a383b69c045a4346d1435 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 14:50:05 +0200 Subject: [PATCH 05/20] Add OSS lemma reuse measurement probe (runOssReuseProbe) Runs bounded automode with the stock one step simplifier on a selection of example problems, keys every OSS application by the registry's content key (printed find formula + used context formulas with polarity), and reports total vs. distinct applications restricted to the lemma-eligible (modality-free) subset, plus the aggregated micro steps a result cache would have skipped. Findings on the current selection: 91% of OSS applications are lemma-eligible; 35% of eligible applications are content-duplicates (check_jdiv 46%, ReverseArray 24%, arrayMax 18%); 13.5% of aggregated micro steps are savable (a lower bound - cache hits also skip the fixpoint's unsuccessful scanning, invisible post-hoc). The lemma path is performance neutral-to-positive while adding inspectability and provability. Created with AI tooling support --- key.core/build.gradle | 14 ++ .../key/rule/lemma/OssLemmaGenerator.java | 6 +- .../key/rule/lemma/OssLemmaReuseProbe.java | 194 ++++++++++++++++++ 3 files changed, 213 insertions(+), 1 deletion(-) create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/OssLemmaReuseProbe.java diff --git a/key.core/build.gradle b/key.core/build.gradle index 26f2e0a6ab7..0f2aa5bedff 100644 --- a/key.core/build.gradle +++ b/key.core/build.gradle @@ -285,3 +285,17 @@ tasks.register("testRAP", Test) { sourceSets.test.java.srcDirs(rapDir) sourcesJar.dependsOn(runAntlr4Jml, runAntlr4Key) + +// Measurement probe for the lemma reuse potential of One Step Simplifier applications +// (see de.uka.ilkd.key.rule.lemma.OssLemmaReuseProbe). Not part of the regular test suite. +tasks.register('runOssReuseProbe', Test) { + group = "verification" + description = "Measures potential lemma reuse of OSS applications on example proofs." + useJUnitPlatform { includeTags 'performance' } + filter { includeTestsMatching "de.uka.ilkd.key.rule.lemma.OssLemmaReuseProbe" } + testClassesDirs = sourceSets.test.output.classesDirs + classpath = sourceSets.test.runtimeClasspath + systemProperty "key.disregardSettings", "true" + maxHeapSize = "4g" + testLogging.showStandardStreams = true +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java index c6321d2f54f..59e9f7d3130 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java @@ -127,7 +127,11 @@ public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { return tb.getTaclet(); } - private static boolean containsModality(Term term) { + /** + * returns true iff the term contains a modal operator anywhere; such formulas fall outside + * the fragment supported by the taclet soundness proof obligation machinery + */ + static boolean containsModality(Term term) { if (term.op() instanceof Modality) { return true; } diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/OssLemmaReuseProbe.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/OssLemmaReuseProbe.java new file mode 100644 index 00000000000..91c5c77174a --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/OssLemmaReuseProbe.java @@ -0,0 +1,194 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.TreeSet; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +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.proof.io.OutputStreamProofSaver; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * Measurement probe (not a regression test) for the lemma reuse hypothesis: the + * {@link GeneratedLemmaRegistry} acts as a result cache that the stock + * {@link OneStepSimplifier} does not have, so one-step-simplifications whose content (formula + * plus used context formulas) recurs within a proof would be computed once and replayed from the + * registry afterwards. + * + *

+ * The probe runs bounded automode with the stock simplifier on a selection of example problems, + * then keys every OSS application by the printed find formula and used context formulas — the + * same content key the lemma registry would use — and reports total vs. distinct applications, + * restricted to the lemma-eligible (modality-free) subset, together with the number of aggregated + * micro steps (protocol lengths) that a cache hit would have skipped. + * + *

+ * Run with: {@code ./gradlew :key.core:runOssReuseProbe} + */ +@Tag("performance") +public class OssLemmaReuseProbe { + + private static final String[] PROBLEMS = { + "standard_key/java_dl/arrayMax.key", + "standard_key/java_dl/arrayUpdateSimp.key", + "standard_key/arith/binomial1.key", + "standard_key/arith/computation.key", + "standard_key/arith/check_jdiv.key", + "firstTouch/05-ReverseArray/ReverseArray.key", + "heap/Map/existenceInfiniteMaps.key", + }; + + private static final int MAX_RULES = 20000; + private static final long TIMEOUT_MILLIS = 120000; + + private record Row(String problem, int nodes, boolean closed, int ossApps, int eligible, + int distinctEligible, long stepsTotal, long stepsSavable) { + } + + @Test + public void measureReusePotential() { + final Path examples = FindResources.getExampleDirectory(); + assertNotNull(examples, "example directory not found"); + + final List rows = new ArrayList<>(); + for (final String problem : PROBLEMS) { + final Path file = examples.resolve(problem); + if (!Files.exists(file)) { + System.out.println("SKIP (not found): " + problem); + continue; + } + try { + rows.add(measure(problem, file)); + } catch (Exception e) { + System.out.println("SKIP (failed): " + problem + " -- " + e); + } + } + + System.out.println(); + System.out.printf("%-45s %7s %6s %8s %9s %9s %8s %10s %10s%n", "problem", "nodes", + "closed", "ossApps", "eligible", "distinct", "hitRate", "microSteps", "savable"); + long ossApps = 0, eligible = 0, distinct = 0, steps = 0, savable = 0; + for (final Row r : rows) { + System.out.printf("%-45s %7d %6s %8d %9d %9d %7.1f%% %10d %10d%n", r.problem, r.nodes, + r.closed, r.ossApps, r.eligible, r.distinctEligible, hitRate(r), r.stepsTotal, + r.stepsSavable); + ossApps += r.ossApps; + eligible += r.eligible; + distinct += r.distinctEligible; + steps += r.stepsTotal; + savable += r.stepsSavable; + } + System.out.printf("%-45s %7s %6s %8d %9d %9d %7.1f%% %10d %10d%n", "TOTAL", "", "", + ossApps, eligible, distinct, + eligible == 0 ? 0.0 : 100.0 * (eligible - distinct) / eligible, steps, savable); + } + + private static double hitRate(Row r) { + return r.eligible == 0 ? 0.0 : 100.0 * (r.eligible - r.distinctEligible) / r.eligible; + } + + private static Row measure(String problem, Path file) throws Exception { + final KeYEnvironment env = KeYEnvironment.load(file); + try { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_ON); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + OneStepSimplifier.refreshOSS(proof); + + try { + new AutomaticProver().start(proof, MAX_RULES, TIMEOUT_MILLIS); + } catch (InterruptedException e) { + // partial proofs are fine for the statistics + } + + final Map eligibleKeyCounts = new LinkedHashMap<>(); + final Map keySteps = new LinkedHashMap<>(); + int ossApps = 0; + int eligible = 0; + long stepsTotal = 0; + + final var nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node node = nodes.next(); + if (!(node.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp app)) { + continue; + } + ossApps++; + final long protocolSize = app.getProtocol() == null ? 0 : app.getProtocol().size(); + stepsTotal += protocolSize; + + final JTerm find = (JTerm) app.posInOccurrence().sequentFormula().formula(); + if (OssLemmaGenerator.containsModality(find)) { + continue; + } + eligible++; + final String key = contentKey(proof, find, app.assumesInsts()); + eligibleKeyCounts.merge(key, 1, Integer::sum); + keySteps.merge(key, protocolSize, Long::sum); + } + + // micro steps that a result cache would have skipped: all but the first + // aggregation per distinct key + long stepsSavable = 0; + for (final Map.Entry entry : eligibleKeyCounts.entrySet()) { + final int count = entry.getValue(); + if (count > 1) { + stepsSavable += keySteps.get(entry.getKey()) / count * (count - 1); + } + } + + return new Row(problem, proof.countNodes(), proof.closed(), ossApps, eligible, + eligibleKeyCounts.size(), stepsTotal, stepsSavable); + } finally { + env.dispose(); + } + } + + /** + * The content key mirroring the lemma registry's reuse key: printed find formula plus the + * used context formulas with polarity (canonically ordered). + */ + private static String contentKey(Proof proof, JTerm find, + Iterable assumesInsts) { + final StringBuilder key = new StringBuilder(); + key.append(OutputStreamProofSaver.printTerm(find, proof.getServices())); + if (assumesInsts != null) { + final TreeSet assumed = new TreeSet<>(); + for (final PosInOccurrence context : assumesInsts) { + assumed.add((context.isInAntec() ? "A:" : "S:") + OutputStreamProofSaver + .printTerm((JTerm) context.sequentFormula().formula(), + proof.getServices())); + } + for (final String s : assumed) { + key.append('\n').append(s); + } + } + return key.toString(); + } +} From 94016412866dd16dd667325f28250def3e313017 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 15:09:59 +0200 Subject: [PATCH 06/20] Mode 2: a-posteriori lemma elaboration of finished proofs LemmaElaboratingProofReplayer copies a proof onto a fresh proof of the same problem, replacing every lemma-eligible one step simplifier application by the two-step lemma form (introduce_ossLemma + generated taclet application); non-eligible applications (modal operators, unlocatable positions) are copied unchanged, so elaboration never fails a proof that copies cleanly. This decouples proof search from transparency: search runs with the fast opaque rule, the finished proof is elaborated into a form where every aggregated simplification is a declarative, separately provable artifact. Position relocation uses AbstractProofReplayer.findInNewSequent (widened to protected), which compares modulo proof irrelevancy - strict term equality fails as soon as term labels diverge between original and elaborated proof. End-to-end test on standard_key/arith/computation.key: 36 OSS applications, 16 elaborated (exactly the modality-free subset), 20 copied, 16 generated lemmas all certified by discharging their soundness proof obligations, the elaborated proof closes, and each elaborated step adds exactly one node (597 -> 613). Created with AI tooling support --- .../proof/replay/AbstractProofReplayer.java | 2 +- .../lemma/LemmaElaboratingProofReplayer.java | 137 ++++++++++++++++++ .../key/rule/lemma/TestLemmaElaboration.java | 128 ++++++++++++++++ 3 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaElaboratingProofReplayer.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaElaboration.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/replay/AbstractProofReplayer.java b/key.core/src/main/java/de/uka/ilkd/key/proof/replay/AbstractProofReplayer.java index e5817e32ee8..c28ad491c6d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/replay/AbstractProofReplayer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/replay/AbstractProofReplayer.java @@ -323,7 +323,7 @@ private TacletApp constructTacletApp(Node originalStep, Goal currGoal) { * @param newSequent sequent * @return the formula in the sequent, or null if not found */ - private PosInOccurrence findInNewSequent(PosInOccurrence oldPos, + protected PosInOccurrence findInNewSequent(PosInOccurrence oldPos, Sequent newSequent) { SequentFormula oldFormula = oldPos.sequentFormula(); Semisequent semiSeq = oldPos.isInAntec() ? newSequent.antecedent() diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaElaboratingProofReplayer.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaElaboratingProofReplayer.java new file mode 100644 index 00000000000..4ed36b8cf56 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaElaboratingProofReplayer.java @@ -0,0 +1,137 @@ +/* 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.lemma; + +import java.util.HashSet; + +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.io.IntermediateProofReplayer; +import de.uka.ilkd.key.proof.replay.CopyingProofReplayer; +import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.rule.TacletApp; + +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +/** + * A-posteriori proof elaboration ("mode 2" of the taclet-generating transformer approach): copies + * a proof onto a fresh proof of the same problem, replacing every eligible application of the one + * step simplifier by the two-step lemma form — an introduction of the generated taclet (see + * {@link OssLemmaGenerator}) followed by an ordinary application of that taclet. + * + *

+ * This decouples proof search from transparency: the proof is found with the fast opaque rule, + * and the finished proof is then elaborated into a form in which every aggregated simplification + * is a declarative, separately provable artifact. Simplifier applications that cannot be + * elaborated (formulas containing modal operators, or positions that cannot be relocated) are + * copied unchanged, so elaboration never fails a proof that copies cleanly. + */ +public class LemmaElaboratingProofReplayer extends CopyingProofReplayer { + + private int elaborated = 0; + private int copiedOss = 0; + + public LemmaElaboratingProofReplayer(Proof originalProof, Proof target) { + super(originalProof, target); + } + + /** + * Elaborates the original proof onto the target proof (a fresh proof of the same problem, + * open at its root). + * + * @param original the proof to elaborate + * @param target the fresh target proof; must have the same root sequent + * @return the replayer, for querying {@link #getElaboratedCount()} and + * {@link #getCopiedOssCount()} + */ + public static LemmaElaboratingProofReplayer elaborate(Proof original, Proof target) + throws IntermediateProofReplayer.BuiltInConstructionException { + final LemmaElaboratingProofReplayer replayer = + new LemmaElaboratingProofReplayer(original, target); + replayer.copy(original.root(), target.getOpenGoal(target.root()), new HashSet<>()); + return replayer; + } + + /** + * returns the number of one step simplifier applications replaced by introduction plus + * taclet application + */ + public int getElaboratedCount() { + return elaborated; + } + + /** + * returns the number of one step simplifier applications copied unchanged (not + * lemma-eligible) + */ + public int getCopiedOssCount() { + return copiedOss; + } + + @Override + protected ImmutableList reApplyRuleApp(Node node, Goal openGoal) + throws IntermediateProofReplayer.BuiltInConstructionException { + if (node.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp ossApp + && ossApp.posInOccurrence() != null) { + final PosInOccurrence pos = + findInNewSequent(ossApp.posInOccurrence(), openGoal.sequent()); + if (pos != null + && OssLemmaIntroductionRule.INSTANCE.isApplicable(openGoal, pos)) { + final ImmutableList result = elaborateOssStep(openGoal, pos); + if (result != null) { + elaborated++; + return result; + } + } + copiedOss++; + } + return super.reApplyRuleApp(node, openGoal); + } + + /** + * Replaces one simplifier application by lemma introduction plus taclet application. + * Returns the resulting goals, or {@code null} if the introduction is not possible (in which + * case the goal is untouched and the caller falls back to copying the original step). + */ + private ImmutableList elaborateOssStep(Goal openGoal, PosInOccurrence pos) { + final var services = openGoal.proof().getServices(); + + final ImmutableList afterIntro = openGoal + .apply(OssLemmaIntroductionRule.INSTANCE.createApp(pos, services)); + if (afterIntro == null) { + return null; + } + final Goal introGoal = afterIntro.head(); + + // the taclet introduced by the step just performed + final var introducedRules = introGoal.node().getLocalIntroducedRules().iterator(); + if (!introducedRules.hasNext()) { + throw new IllegalStateException( + "lemma introduction did not introduce a taclet at " + pos); + } + final NoPosTacletApp lemmaApp = introducedRules.next(); + + // the introduction step leaves the sequent unchanged, so the position stays valid + final NoPosTacletApp matched = lemmaApp.matchFind(pos, services); + if (matched == null) { + throw new IllegalStateException( + "generated lemma " + lemmaApp.taclet().name() + " does not match at " + pos); + } + TacletApp application = matched.setPosInOccurrence(pos, services); + if (!application.complete()) { + final ImmutableList instantiated = + application.findIfFormulaInstantiations(introGoal.sequent(), services); + if (instantiated.isEmpty()) { + throw new IllegalStateException("no assumption instantiation found for lemma " + + lemmaApp.taclet().name() + " although it was generated from this sequent"); + } + application = instantiated.head(); + } + return introGoal.apply(application); + } + +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaElaboration.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaElaboration.java new file mode 100644 index 00000000000..eaceff8834f --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaElaboration.java @@ -0,0 +1,128 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * End-to-end test for {@link LemmaElaboratingProofReplayer} ("mode 2"): a proof is found with + * the fast opaque one step simplifier, then elaborated onto a fresh proof in which every + * lemma-eligible simplifier application is replaced by taclet introduction plus taclet + * application; non-eligible applications (modal operators) are copied unchanged. The elaborated + * proof closes, and all generated lemmas are certified by discharging their soundness proof + * obligations. + */ +public class TestLemmaElaboration { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + private static void activateOss(Proof proof) { + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_ON); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + OneStepSimplifier.refreshOSS(proof); + } + + private static int countOssApps(Proof proof) { + int count = 0; + final var nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + if (nodes.next().getAppliedRuleApp() instanceof OneStepSimplifierRuleApp) { + count++; + } + } + return count; + } + + private static int countIntroApps(Proof proof) { + int count = 0; + final var nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node node = nodes.next(); + if (node.getAppliedRuleApp() != null + && node.getAppliedRuleApp().rule() == OssLemmaIntroductionRule.INSTANCE) { + count++; + } + } + return count; + } + + @Test + public void testElaborateComputationProof() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + assertTrue(Files.exists(file), "example problem not found: " + file); + + try (KeYEnvironment env = KeYEnvironment.load(file); + KeYEnvironment targetEnv = + KeYEnvironment.load(file)) { + + // find the proof with the fast opaque simplifier + final Proof original = env.getLoadedProof(); + assertNotNull(original); + activateOss(original); + new AutomaticProver().start(original, 20000, 120000); + assertTrue(original.closed(), "automode did not close the original proof"); + final int originalOssApps = countOssApps(original); + assertTrue(originalOssApps > 0, + "test problem should exercise the one step simplifier"); + + // elaborate onto a fresh proof of the same problem + final Proof target = targetEnv.getLoadedProof(); + assertNotNull(target); + activateOss(target); + final LemmaElaboratingProofReplayer replayer = + LemmaElaboratingProofReplayer.elaborate(original, target); + + assertTrue(target.closed(), "elaborated proof did not close"); + assertTrue(replayer.getElaboratedCount() > 0, + "no simplifier application was elaborated"); + assertEquals(originalOssApps, + replayer.getElaboratedCount() + replayer.getCopiedOssCount(), + "every original simplifier application is either elaborated or copied"); + assertEquals(replayer.getCopiedOssCount(), countOssApps(target), + "only non-eligible simplifier applications remain in the elaborated proof"); + assertEquals(replayer.getElaboratedCount(), countIntroApps(target), + "each elaborated application contributes one introduction step"); + assertEquals(original.countNodes() + replayer.getElaboratedCount(), + target.countNodes(), + "each elaborated application adds exactly one node"); + + // certify: every generated lemma's soundness proof obligation closes + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(target); + System.out.printf( + "ELABORATION: ossApps=%d elaborated=%d copied=%d distinctLemmas=%d " + + "nodes %d -> %d%n", + originalOssApps, replayer.getElaboratedCount(), replayer.getCopiedOssCount(), + registry.getLemmas().size(), original.countNodes(), target.countNodes()); + assertFalse(registry.getLemmas().isEmpty()); + assertTrue(registry.getLemmas().size() <= replayer.getElaboratedCount(), + "reuse: at most one lemma per elaborated application"); + for (final GeneratedLemma lemma : registry.getLemmas()) { + final Proof po = lemma.getOrCreateSoundnessProof(); + new AutomaticProver().start(po, 10000, 60000); + assertTrue(po.closed(), + "soundness proof obligation of " + lemma.taclet().name() + + " did not close"); + } + } + } +} From c53330e4b7696d11487cdcb9dfc7f7b94de2e5f3 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 15:49:11 +0200 Subject: [PATCH 07/20] Transparent OSS: strategy option, CLI --save-transparent, GUI actions Three user-facing integrations of the taclet-generating transformer machinery for the One Step Simplifier. Strategy option: One Step Simplification gains a third value 'Transparent' (OSS_TRANSPARENT) besides Enabled/Disabled. In transparent mode the simplifier machinery stays active but yields lemma-eligible (modality-free) formulas to OssLemmaIntroductionRule, which the strategy applies in its place (costed identically via the OSS feature; the introduced concrete-ruleset taclet is then applied normally). Formulas with modal operators are still simplified by the opaque rule. OneStepSimplifier.isApplicable now partitions on transparent mode; canSimplify exposes the unpartitioned notion for the generator. The lemma introduction rule gained a guard so an already-available lemma is not re-introduced (avoids non-termination under automation). CLI: --save-transparent additionally writes .transparent.proof in batch mode via the new TransparentProofSaver (elaborates onto a fresh proof of the same problem and saves; original untouched). GUI: SaveTransparentProofAction in the File menu, the proof/task-list context menu, and as a button in the proof-closed statistics dialog (background worker, status-line feedback). Created with AI tooling support --- .../uka/ilkd/key/rule/OneStepSimplifier.java | 42 +++++- .../key/rule/lemma/LemmaIntroductionRule.java | 37 ++++- .../key/rule/lemma/OssLemmaGenerator.java | 7 +- .../key/rule/lemma/TransparentProofSaver.java | 79 ++++++++++ .../de/uka/ilkd/key/strategy/FOLStrategy.java | 15 +- .../strategy/JavaCardDLStrategyFactory.java | 12 +- .../ilkd/key/strategy/StrategyProperties.java | 10 +- .../key/rule/lemma/TestTransparentMode.java | 140 ++++++++++++++++++ .../rule/lemma/TestTransparentProofSaver.java | 95 ++++++++++++ key.ui/computation.key.csv | 17 +++ .../main/java/de/uka/ilkd/key/core/Main.java | 12 ++ .../java/de/uka/ilkd/key/gui/MainWindow.java | 2 + .../actions/SaveTransparentProofAction.java | 107 +++++++++++++ .../key/gui/actions/ShowProofStatistics.java | 6 + .../key/ui/ConsoleUserInterfaceControl.java | 26 ++++ 15 files changed, 594 insertions(+), 13 deletions(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentProofSaver.java create mode 100644 key.ui/computation.key.csv create mode 100644 key.ui/src/main/java/de/uka/ilkd/key/gui/actions/SaveTransparentProofAction.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java index daed990cdc3..450f9d54e46 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java @@ -23,6 +23,7 @@ import de.uka.ilkd.key.proof.TacletIndexKit; import de.uka.ilkd.key.proof.calculus.JavaDLSequentKit; import de.uka.ilkd.key.rule.inst.SVInstantiations; +import de.uka.ilkd.key.rule.lemma.OssLemmaGenerator; import de.uka.ilkd.key.settings.ProofSettings; import de.uka.ilkd.key.strategy.StrategyProperties; import de.uka.ilkd.key.util.MiscTools; @@ -88,6 +89,14 @@ public static final class Protocol extends ArrayList { private TacletIndex[] indices; private Map[] notSimplifiableCaches; private boolean active; + /** + * In transparent mode (strategy option {@link StrategyProperties#OSS_TRANSPARENT}), the + * simplifier machinery stays active but lemma-eligible formulas are not simplified by this + * rule; they are handled by + * {@link de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule} instead, so that the + * aggregated simplification becomes an inspectable, separately provable taclet. + */ + private boolean transparent; // ------------------------------------------------------------------------- // constructors @@ -538,12 +547,17 @@ private synchronized void refresh(Proof proof) { settings = ProofSettings.DEFAULT_SETTINGS; } - final boolean newActive = settings.getStrategySettings().getActiveStrategyProperties() - .get(StrategyProperties.OSS_OPTIONS_KEY).equals(StrategyProperties.OSS_ON); + final Object ossMode = settings.getStrategySettings().getActiveStrategyProperties() + .get(StrategyProperties.OSS_OPTIONS_KEY); + final boolean newActive = StrategyProperties.OSS_ON.equals(ossMode) + || StrategyProperties.OSS_TRANSPARENT.equals(ossMode); + final boolean newTransparent = StrategyProperties.OSS_TRANSPARENT.equals(ossMode); - if (active != newActive || lastProof != proof || // The setting or proof has changed. - (isShutdown() && !proof.closed())) { // A closed proof was pruned. + if (active != newActive || transparent != newTransparent || lastProof != proof + // The setting or proof has changed. + || (isShutdown() && !proof.closed())) { // A closed proof was pruned. active = newActive; + transparent = newTransparent; if (active && proof != null && !proof.closed()) { initIndices(proof); } else { @@ -571,8 +585,13 @@ public static void refreshOSS(Proof proof) { } } - @Override - public boolean isApplicable(Goal goal, PosInOccurrence pio) { + /** + * Tells whether the simplifier machinery could simplify the formula at the given position, + * regardless of the transparent mode partition (see {@link #isApplicable(Goal, + * PosInOccurrence)}). This is the applicability notion used by + * {@link de.uka.ilkd.key.rule.lemma.OssLemmaGenerator}. + */ + public boolean canSimplify(Goal goal, PosInOccurrence pio) { // abort if switched off if (!active) { return false; @@ -594,6 +613,17 @@ public boolean isApplicable(Goal goal, PosInOccurrence pio) { null); } + @Override + public boolean isApplicable(Goal goal, PosInOccurrence pio) { + if (!canSimplify(goal, pio)) { + return false; + } + // in transparent mode, lemma-eligible formulas are handled by the lemma introduction + // rule; this rule keeps handling only the formulas outside the lemma fragment + return !transparent + || OssLemmaGenerator.containsModality(pio.sequentFormula().formula()); + } + @Override public synchronized @NonNull ImmutableList apply(Goal goal, RuleApp ruleApp) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java index f4a28b7e31b..307709b8388 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java @@ -9,9 +9,11 @@ import de.uka.ilkd.key.rule.DefaultBuiltInRuleApp; import de.uka.ilkd.key.rule.IBuiltInRuleApp; import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.TacletApp; import de.uka.ilkd.key.rule.inst.SVInstantiations; import org.key_project.logic.Name; +import org.key_project.prover.proof.rulefilter.TacletFilter; import org.key_project.prover.rules.RuleApp; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.util.collection.ImmutableList; @@ -63,9 +65,42 @@ public String toString() { @Override public boolean isApplicable(Goal goal, @Nullable PosInOccurrence pio) { - return pio != null && generator.isApplicable(goal, pio); + return pio != null && generator.isApplicable(goal, pio) + && !lemmaAlreadyAvailable(goal, pio); } + /** + * Checks whether a lemma taclet of this generator is already available and usable at the + * given position. In that case the introduction is not offered again: re-introduction would + * be redundant, and — since the introduction step does not change the sequent — automated + * application would not terminate without this check. + */ + private boolean lemmaAlreadyAvailable(Goal goal, PosInOccurrence pio) { + final var services = goal.proof().getServices(); + final ImmutableList candidates = goal.indexOfTaclets() + .getRewriteTaclet(pio, GENERATED_BY_THIS_GENERATOR, services); + for (final NoPosTacletApp candidate : candidates) { + if (candidate.taclet().assumesSequent().isEmpty()) { + return true; + } + // assumption-carrying lemmas count as available only if their assumptions can + // actually be instantiated in the current sequent + final TacletApp positioned = candidate.setPosInOccurrence(pio, services); + if (positioned != null && (positioned.complete() || !positioned + .findIfFormulaInstantiations(goal.sequent(), services).isEmpty())) { + return true; + } + } + return false; + } + + private final TacletFilter GENERATED_BY_THIS_GENERATOR = new TacletFilter() { + @Override + protected boolean filter(org.key_project.prover.rules.Taclet taclet) { + return generator.name().toString().equals(taclet.displayName()); + } + }; + @Override public boolean isApplicableOnSubTerms() { return true; diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java index 59e9f7d3130..68b35488533 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java @@ -79,7 +79,7 @@ public boolean isApplicable(Goal goal, PosInOccurrence pio) { return false; } final OneStepSimplifier simplifier = MiscTools.findOneStepSimplifier(goal.proof()); - return simplifier != null && simplifier.isApplicable(goal, pio) + return simplifier != null && simplifier.canSimplify(goal, pio) && !containsModality(pio.sequentFormula().formula()); } @@ -129,9 +129,10 @@ public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { /** * returns true iff the term contains a modal operator anywhere; such formulas fall outside - * the fragment supported by the taclet soundness proof obligation machinery + * the fragment supported by the taclet soundness proof obligation machinery and are + * therefore not lemma-eligible */ - static boolean containsModality(Term term) { + public static boolean containsModality(Term term) { if (term.op() instanceof Modality) { return true; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java new file mode 100644 index 00000000000..8c736334055 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java @@ -0,0 +1,79 @@ +/* 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.lemma; + +import java.nio.file.Path; + +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.init.InitConfig; +import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.strategy.StrategyProperties; + +import org.jspecify.annotations.NullMarked; + +/** + * Saves a proof in its transparent form: the proof is elaborated onto a fresh proof of the same + * problem (see {@link LemmaElaboratingProofReplayer}), in which every lemma-eligible one step + * simplifier application is replaced by the introduction and application of a generated, + * separately provable taclet, and that proof is saved. + * + *

+ * The original proof is not modified. The elaborated proof lives in a copy of the original's + * initial configuration and is disposed after saving. + */ +@NullMarked +public final class TransparentProofSaver { + + /** + * Statistics of a transparent save. + * + * @param elaborated number of simplifier applications replaced by lemma introduction plus + * taclet application + * @param copiedOss number of simplifier applications copied unchanged (not lemma-eligible) + * @param lemmas number of distinct generated lemmas + */ + public record Result(int elaborated, int copiedOss, int lemmas) { + } + + private TransparentProofSaver() { + } + + /** + * Elaborates the given proof into its transparent form and saves it to the given file. + * + * @param proof the proof to save (left unmodified) + * @param file the target file + * @return statistics of the elaboration + * @throws Exception if the elaboration or saving fails + */ + public static Result save(Proof proof, Path file) throws Exception { + final InitConfig config = proof.getInitConfig().deepCopy(); + final Proof target = new Proof(proof.name().toString(), proof.root().sequent(), + proof.header(), config.createTacletIndex(), config.createBuiltInRuleIndex(), config); + try { + // the lemma generator needs an active simplifier on the target proof + final StrategyProperties sp = + target.getSettings().getStrategySettings().getActiveStrategyProperties(); + if (StrategyProperties.OSS_OFF + .equals(sp.getProperty(StrategyProperties.OSS_OPTIONS_KEY))) { + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_ON); + target.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + } + OneStepSimplifier.refreshOSS(target); + + final LemmaElaboratingProofReplayer replayer = + LemmaElaboratingProofReplayer.elaborate(proof, target); + + final String error = new ProofSaver(target, file).save(); + if (error != null) { + throw new IllegalStateException("saving transparent proof failed: " + error); + } + return new Result(replayer.getElaboratedCount(), replayer.getCopiedOssCount(), + GeneratedLemmaRegistry.get(target).getLemmas().size()); + } finally { + target.dispose(); + } + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java index 2d0680f0089..4624db08df6 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java @@ -13,6 +13,7 @@ import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.rule.BuiltInRule; import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule; import de.uka.ilkd.key.strategy.feature.*; import de.uka.ilkd.key.strategy.quantifierHeuristics.*; import de.uka.ilkd.key.strategy.termProjection.AssumptionProjection; @@ -89,6 +90,9 @@ private Feature setUpGlobalF(RuleSetDispatchFeature d) { private Feature oneStepSimplificationFeature(Feature cost) { SetRuleFilter filter = new SetRuleFilter(); filter.addRuleToSet(MiscTools.findOneStepSimplifier(getProof())); + // in transparent mode, the lemma introduction rule takes the simplifier's place on + // lemma-eligible formulas and is costed identically + filter.addRuleToSet(OssLemmaIntroductionRule.INSTANCE); return ConditionalFeature.createConditional(filter, cost); } @@ -622,6 +626,15 @@ private boolean normalSplitting() { @Override public boolean isResponsibleFor(BuiltInRule rule) { - return rule instanceof OneStepSimplifier; + if (rule instanceof OneStepSimplifier) { + return true; + } + // in transparent mode, aggregated simplifications of lemma-eligible formulas are + // performed via generated lemma taclets: the strategy applies the introduction rule + // (the simplifier itself yields those formulas, see OneStepSimplifier#isApplicable), + // and the introduced taclet is then applied through its "concrete" rule set + return rule instanceof OssLemmaIntroductionRule + && StrategyProperties.OSS_TRANSPARENT.equals( + strategyProperties.getProperty(StrategyProperties.OSS_OPTIONS_KEY)); } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java index 873eb4757b5..9cb0d296059 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java @@ -33,6 +33,14 @@ public class JavaCardDLStrategyFactory implements StrategyFactory { + "larger, but more transparent proof trees, since each
" + "simplification step is realized in one single rule
" + "application, with all instantiations clearly visible." + ""; + public static final String TOOL_TIP_OSS_TRANSPARENT = + "" + "One Step Simplification via generated lemma taclets:
" + + "each aggregated simplification of a formula without modal
" + + "operators is captured as a taclet that is introduced and
" + + "then applied, so the performed transformation is inspectable
" + + "and can be certified by a separate soundness proof.
" + + "Formulas containing modal operators are still simplified
" + + "by the ordinary OSS rule." + ""; public static final String TOOL_TIP_PROOF_SPLITTING_FREE = "" + "Split formulas (if-then-else expressions,
" + "disjunctions in the antecedent, conjunctions in
" @@ -147,7 +155,9 @@ private static OneOfStrategyPropertyDefinition getOssUsage() { new StrategyPropertyValueDefinition(StrategyProperties.OSS_ON, "Enabled", TOOL_TIP_OSS_ON), new StrategyPropertyValueDefinition(StrategyProperties.OSS_OFF, "Disabled", - TOOL_TIP_OSS_OFF)); + TOOL_TIP_OSS_OFF), + new StrategyPropertyValueDefinition(StrategyProperties.OSS_TRANSPARENT, "Transparent", + TOOL_TIP_OSS_TRANSPARENT)); } private static OneOfStrategyPropertyDefinition getProofSplitting() { diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/StrategyProperties.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/StrategyProperties.java index 3ccc76b424e..781537bd777 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/StrategyProperties.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/StrategyProperties.java @@ -71,6 +71,13 @@ public final class StrategyProperties extends Properties { public static final String OSS_OPTIONS_KEY = "OSS_OPTIONS_KEY"; public static final String OSS_ON = "OSS_ON"; public static final String OSS_OFF = "OSS_OFF"; + /** + * One step simplification in transparent mode: aggregated simplifications of formulas + * without modal operators are performed via generated, separately provable lemma taclets + * (see {@link de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule}) instead of the opaque + * built-in rule; formulas outside that fragment are still simplified by the built-in rule. + */ + public static final String OSS_TRANSPARENT = "OSS_TRANSPARENT"; public static final String QUANTIFIERS_OPTIONS_KEY = "QUANTIFIERS_OPTIONS_KEY"; public static final String QUANTIFIERS_NONE = "QUANTIFIERS_NONE"; @@ -168,7 +175,8 @@ public final class StrategyProperties extends Properties { MPS_SKIP, MPS_NONE, DEP_OPTIONS_KEY, DEP_ON, DEP_OFF, QUERY_OPTIONS_KEY, QUERY_ON, QUERY_RESTRICTED, QUERY_OFF, QUERYAXIOM_OPTIONS_KEY, QUERYAXIOM_ON, QUERYAXIOM_OFF, NON_LIN_ARITH_OPTIONS_KEY, NON_LIN_ARITH_NONE, NON_LIN_ARITH_DEF_OPS, - NON_LIN_ARITH_COMPLETION, OSS_OPTIONS_KEY, OSS_ON, OSS_OFF, QUANTIFIERS_OPTIONS_KEY, + NON_LIN_ARITH_COMPLETION, OSS_OPTIONS_KEY, OSS_ON, OSS_OFF, OSS_TRANSPARENT, + QUANTIFIERS_OPTIONS_KEY, QUANTIFIERS_NONE, QUANTIFIERS_NON_SPLITTING, QUANTIFIERS_NON_SPLITTING_WITH_PROGS, QUANTIFIERS_INSTANTIATE, VBT_PHASE, VBT_SYM_EX, VBT_QUAN_INST, VBT_MODEL_GEN, CLASS_AXIOM_OFF, CLASS_AXIOM_DELAYED, CLASS_AXIOM_FREE, AUTO_INDUCTION_OPTIONS_KEY, diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java new file mode 100644 index 00000000000..1c95b14b345 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java @@ -0,0 +1,140 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests for the {@code OSS_TRANSPARENT} strategy option ("mode 1"): automated proof search + * performs aggregated simplifications of lemma-eligible formulas via generated lemma taclets + * (introduction plus taclet application) instead of the opaque one step simplifier, which + * remains responsible for formulas containing modal operators. The resulting proof saves and + * replays. + */ +public class TestTransparentMode { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + private static void setOssMode(Proof proof, String mode) { + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, mode); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + // the active strategy caches its own property clone, so it must be renewed as well + proof.setActiveStrategy(proof.getServices().getProfile() + .getDefaultStrategyFactory().create(proof, sp)); + OneStepSimplifier.refreshOSS(proof); + } + + private record Counts(int oss, int intro, int lemmaApps) { + } + + private static Counts count(Proof proof) { + int oss = 0; + int intro = 0; + int lemmaApps = 0; + final var nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node node = nodes.next(); + if (node.getAppliedRuleApp() == null) { + continue; + } + if (node.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp) { + oss++; + } else if (node.getAppliedRuleApp().rule() == OssLemmaIntroductionRule.INSTANCE) { + intro++; + } else if (node.getAppliedRuleApp().rule().name().toString() + .startsWith("ossLemma_")) { + lemmaApps++; + } + } + return new Counts(oss, intro, lemmaApps); + } + + @Test + public void testTransparentAutomode() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + assertTrue(Files.exists(file)); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + setOssMode(proof, StrategyProperties.OSS_TRANSPARENT); + + new AutomaticProver().start(proof, 20000, 120000); + assertTrue(proof.closed(), "automode in transparent mode did not close the proof"); + + final Counts counts = count(proof); + assertTrue(counts.intro() > 0, + "transparent mode should introduce lemma taclets during search"); + assertEquals(counts.intro(), counts.lemmaApps(), + "each introduced lemma is applied exactly once per introduction"); + // the opaque rule keeps handling only formulas with modal operators + final var nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node node = nodes.next(); + if (node.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp app) { + assertTrue( + OssLemmaGenerator.containsModality( + app.posInOccurrence().sequentFormula().formula()), + "opaque simplifier application on a lemma-eligible formula in " + + "transparent mode"); + } + } + + // the transparent proof saves and replays + final Path proofFile = Files.createTempFile("transparentMode", ".proof"); + try { + assertNull(new ProofSaver(proof, proofFile).save()); + try (KeYEnvironment reloadedEnv = + KeYEnvironment.load(proofFile)) { + final Proof reloaded = reloadedEnv.getLoadedProof(); + assertNotNull(reloaded); + assertEquals(proof.countNodes(), reloaded.countNodes(), + "transparent proof does not replay to the same size"); + assertTrue(reloaded.closed()); + } + } finally { + Files.deleteIfExists(proofFile); + } + } + } + + @Test + public void testStockModeUnaffected() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + setOssMode(proof, StrategyProperties.OSS_ON); + + new AutomaticProver().start(proof, 20000, 120000); + assertTrue(proof.closed()); + + final Counts counts = count(proof); + assertTrue(counts.oss() > 0, "stock mode should still use the opaque simplifier"); + assertEquals(0, counts.intro(), + "stock mode must not introduce lemma taclets automatically"); + } + } +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentProofSaver.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentProofSaver.java new file mode 100644 index 00000000000..66d08603a76 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentProofSaver.java @@ -0,0 +1,95 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Test for {@link TransparentProofSaver}: a proof found with the opaque one step simplifier is + * saved in its transparent form; the saved file loads, replays, is closed, and contains lemma + * introductions instead of the eligible opaque steps. The original proof is unchanged. + */ +public class TestTransparentProofSaver { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + @Test + public void testSaveTransparentAndReload() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + assertTrue(Files.exists(file)); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_ON); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + OneStepSimplifier.refreshOSS(proof); + + new AutomaticProver().start(proof, 20000, 120000); + assertTrue(proof.closed()); + final int originalNodes = proof.countNodes(); + + final Path outFile = Files.createTempFile("transparent", ".proof"); + try { + final TransparentProofSaver.Result result = + TransparentProofSaver.save(proof, outFile); + assertTrue(result.elaborated() > 0, "nothing was elaborated"); + assertTrue(result.lemmas() > 0); + + // the original proof is untouched + assertEquals(originalNodes, proof.countNodes()); + assertTrue(proof.closed()); + + // the saved transparent proof loads, replays, and is closed + try (KeYEnvironment reloadedEnv = + KeYEnvironment.load(outFile)) { + final Proof reloaded = reloadedEnv.getLoadedProof(); + assertNotNull(reloaded); + assertTrue(reloaded.closed(), "transparent proof did not replay to closed"); + assertEquals(originalNodes + result.elaborated(), reloaded.countNodes()); + + int intros = 0; + int oss = 0; + final var nodes = reloaded.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node node = nodes.next(); + if (node.getAppliedRuleApp() == null) { + continue; + } + if (node.getAppliedRuleApp() + .rule() instanceof OssLemmaIntroductionRule) { + intros++; + } else if (node + .getAppliedRuleApp() instanceof OneStepSimplifierRuleApp) { + oss++; + } + } + assertEquals(result.elaborated(), intros); + assertEquals(result.copiedOss(), oss); + } + } finally { + Files.deleteIfExists(outFile); + } + } + } +} diff --git a/key.ui/computation.key.csv b/key.ui/computation.key.csv new file mode 100644 index 00000000000..a3302d72131 --- /dev/null +++ b/key.ui/computation.key.csv @@ -0,0 +1,17 @@ +open goals;0 +Nodes;597 +Branches;3 +Interactive steps;0 +Symbolic execution steps;62 +Automode time;567ms +Avg. time per step;0.951ms +Rule applications +Quantifier instantiations;0 +One-step Simplifier apps;36 +SMT solver apps;0 +Dependency Contract apps;0 +Operation Contract apps;0 +Block/Loop Contract apps;0 +Loop invariant apps;0 +Merge Rule apps;0 +Total rule apps;705 diff --git a/key.ui/src/main/java/de/uka/ilkd/key/core/Main.java b/key.ui/src/main/java/de/uka/ilkd/key/core/Main.java index d83cf09ba7a..1aa509881d7 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/core/Main.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/core/Main.java @@ -140,6 +140,17 @@ public final class Main implements Callable { description = "save all selected contracts for automatic execution") private boolean isSaveAllContracts = false; + /** + * If set, batch mode additionally saves each proof in its transparent form: eligible One + * Step Simplifier applications are elaborated into generated, separately provable lemma + * taclets (introduction plus taclet application). + */ + @Option(names = "--save-transparent", + description = "in batch mode, additionally save each proof in its transparent form " + + "(.transparent.proof): One Step Simplifier applications on formulas without " + + "modal operators are elaborated into generated, separately provable lemma taclets") + private boolean isSaveTransparent = false; + @Option(names = "--timeout", paramLabel = "INT", description = "timeout for each automatic proof of a problem in ms (default: " + LemmataAutoModeOptions.DEFAULT_TIMEOUT + ", i.e., no timeout)") @@ -350,6 +361,7 @@ public Integer call() throws Exception { } else { ui.setMacro(autoMacro); ui.setSaveOnly(isSaveAllContracts); + ConsoleUserInterfaceControl.setSaveTransparentProofs(isSaveTransparent); for (Path f : inputFiles) { ui.loadProblem(f); } diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/MainWindow.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/MainWindow.java index 57281711516..8d7f63e9a2d 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/MainWindow.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/MainWindow.java @@ -989,6 +989,7 @@ private JMenu createFileMenu() { fileMenu.add(editMostRecentFileAction); fileMenu.add(saveFileAction); fileMenu.add(saveBundleAction); + fileMenu.add(new SaveTransparentProofAction(this)); fileMenu.add(quickSaveAction); fileMenu.add(quickLoadAction); fileMenu.addSeparator(); @@ -1106,6 +1107,7 @@ public void menuCanceled(MenuEvent e) { proof.add(goalBack); proof.add(new PruneProofAction(this)); } + proof.add(new SaveTransparentProofAction(this, selected)); proof.add(new AbandonTaskAction(this, selected)); if (selected == null) { proof.addSeparator(); diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/actions/SaveTransparentProofAction.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/actions/SaveTransparentProofAction.java new file mode 100644 index 00000000000..c56b6cb5225 --- /dev/null +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/actions/SaveTransparentProofAction.java @@ -0,0 +1,107 @@ +/* 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.gui.actions; + +import java.awt.Component; +import java.awt.event.ActionEvent; +import java.io.File; +import java.nio.file.Path; +import javax.swing.JFileChooser; +import javax.swing.JOptionPane; +import javax.swing.SwingWorker; + +import de.uka.ilkd.key.gui.KeYFileChooser; +import de.uka.ilkd.key.gui.MainWindow; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.lemma.TransparentProofSaver; +import de.uka.ilkd.key.util.MiscTools; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Saves the selected proof in its transparent form: eligible one step simplifier applications + * are elaborated into generated, separately provable lemma taclets (introduction plus taclet + * application) before saving. The proof itself is left unchanged. + * + * @see TransparentProofSaver + */ +public final class SaveTransparentProofAction extends MainWindowAction { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = + LoggerFactory.getLogger(SaveTransparentProofAction.class); + + /** + * The proof to save; if {@code null}, the currently selected proof is saved (used for the + * task list context menu, which acts on the proof under the mouse pointer). + */ + private final Proof proof; + + public SaveTransparentProofAction(MainWindow mainWindow) { + this(mainWindow, null); + } + + public SaveTransparentProofAction(MainWindow mainWindow, Proof proof) { + super(mainWindow); + this.proof = proof; + setName("Save Transparent Proof..."); + setTooltip("Save the proof with One Step Simplification steps elaborated into " + + "generated, separately provable lemma taclets."); + if (proof == null) { + mainWindow.getMediator().enableWhenProofLoaded(this); + } + } + + @Override + public void actionPerformed(ActionEvent e) { + final Proof toSave = + proof != null ? proof : mainWindow.getMediator().getSelectedProof(); + if (toSave == null) { + mainWindow.popupWarning("No proof.", "Oops..."); + return; + } + saveTransparent(mainWindow, toSave, mainWindow); + } + + /** + * Asks for a file name and saves the given proof in its transparent form (in the + * background, since elaboration replays the whole proof). + */ + public static void saveTransparent(MainWindow mainWindow, Proof proof, Component parent) { + final KeYFileChooser fileChooser = + KeYFileChooser.getFileChooser("Choose filename to save transparent proof"); + fileChooser.setSelectedFile(new File( + MiscTools.toValidFileName(proof.name().toString()) + ".transparent.proof")); + if (fileChooser.showSaveDialog(parent) != JFileChooser.APPROVE_OPTION) { + return; + } + final Path file = fileChooser.getSelectedFile().toPath(); + + mainWindow.setStatusLine("Elaborating proof into its transparent form ..."); + new SwingWorker() { + @Override + protected TransparentProofSaver.Result doInBackground() throws Exception { + return TransparentProofSaver.save(proof, file); + } + + @Override + protected void done() { + try { + final TransparentProofSaver.Result result = get(); + mainWindow.setStatusLine("Transparent proof saved to " + file + " (" + + result.elaborated() + " simplifier application(s) elaborated into " + + result.lemmas() + " lemma(s), " + result.copiedOss() + + " copied unchanged)"); + } catch (Exception exception) { + LOGGER.error("Saving transparent proof failed", exception); + mainWindow.setStatusLine("Saving transparent proof failed"); + JOptionPane.showMessageDialog(parent, + "Saving the transparent proof failed:\n" + exception.getMessage(), + "Save Transparent Proof", JOptionPane.ERROR_MESSAGE); + } + } + }.execute(); + } +} diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/actions/ShowProofStatistics.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/actions/ShowProofStatistics.java index 5bd3bc299bc..dbac1d03ff1 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/actions/ShowProofStatistics.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/actions/ShowProofStatistics.java @@ -309,12 +309,18 @@ private void init(MainWindow mainWindow, String stats) { JButton saveBundleButton = new JButton("Save proof bundle"); saveBundleButton .addActionListener(e -> mainWindow.getUserInterface().saveProofBundle(proof)); + JButton saveTransparentButton = new JButton("Save transparent proof"); + saveTransparentButton.setToolTipText("Save the proof with One Step Simplification " + + "steps elaborated into generated, separately provable lemma taclets."); + saveTransparentButton.addActionListener( + e -> SaveTransparentProofAction.saveTransparent(mainWindow, proof, this)); buttonPane.add(okButton); buttonPane.add(csvButton); buttonPane.add(htmlButton); buttonPane2.add(saveButton); buttonPane2.add(saveBundleButton); + buttonPane2.add(saveTransparentButton); // spotless:off /* diff --git a/key.ui/src/main/java/de/uka/ilkd/key/ui/ConsoleUserInterfaceControl.java b/key.ui/src/main/java/de/uka/ilkd/key/ui/ConsoleUserInterfaceControl.java index 1046403af7d..00e4be425d4 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/ui/ConsoleUserInterfaceControl.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/ui/ConsoleUserInterfaceControl.java @@ -36,6 +36,7 @@ import de.uka.ilkd.key.proof.io.ProofSaver; import de.uka.ilkd.key.prover.impl.DefaultTaskStartedInfo; import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.lemma.TransparentProofSaver; import de.uka.ilkd.key.scripts.ProofScriptEngine; import de.uka.ilkd.key.speclang.PositionedString; import de.uka.ilkd.key.util.ExceptionTools; @@ -429,6 +430,16 @@ public void reportWarnings(ImmutableSet warnings) { * @param keyProblemFile the key problem file * @return true, if successful */ + /** + * If set, {@link #saveProof(Object, Proof, Path)} additionally saves each proof in its + * transparent form (see {@link de.uka.ilkd.key.rule.lemma.TransparentProofSaver}). + */ + private static volatile boolean saveTransparentProofs = false; + + public static void setSaveTransparentProofs(boolean value) { + saveTransparentProofs = value; + } + public static boolean saveProof(Object result, Proof proof, Path keyProblemFile) { if (result instanceof Throwable) { throw new RuntimeException("Error in batchmode.", (Throwable) result); @@ -466,6 +477,21 @@ public static boolean saveProof(Object result, Proof proof, Path keyProblemFile) } catch (IOException e) { LOGGER.error("Failed to write proof stats", e); } + + if (saveTransparentProofs) { + final Path transparentFile = Path.of(baseName + ".transparent.proof"); + try { + final TransparentProofSaver.Result stats = + TransparentProofSaver.save(proof, transparentFile); + LOGGER.info( + "Transparent proof saved to {}: {} simplifier application(s) elaborated " + + "into {} lemma(s), {} copied unchanged", + transparentFile, stats.elaborated(), stats.lemmas(), stats.copiedOss()); + } catch (Exception e) { + LOGGER.error("Failed to save transparent proof", e); + return false; + } + } // Says true if all Proofs have succeeded, // or false if there is at least one open Proof return proof.openGoals().isEmpty(); From cd09697532a684be3639fb63245d8a8dcd626ec6 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 15:59:41 +0200 Subject: [PATCH 08/20] Missing Lemmas tab: load lemma soundness POs as side proofs from the GUI Extends the proof management dialog (the proof obligation browser) with a 'Missing Lemmas' tab listing, for the selected proof, the generated lemmas it still depends on whose soundness proof obligation has not been discharged (not created, or created but open). Entries can be selected individually or all at once and loaded as side proofs into the same proof environment, where they appear in the task tree and can be proved like any other obligation. Core support: - GeneratedLemma retains the soundness ProofAggregate and exposes getOrCreateSoundnessProofAggregate() (what a UI registers), plus isSoundnessProofPresent()/isProven() status. - GeneratedLemmaRegistry.getMissingLemmas() enumerates the not-yet-proven lemmas; getIfPresent(proof) inspects without attaching a registry. GUI: - MissingLemmasPanel (multi-select list, 'Select all' / 'Load selected as side proofs'); creating an aggregate registers it in the environment (done by GeneratedLemma) and the panel adds it to the UI via registerProofAggregate. - ProofManagementDialog gains the tab, refreshed from the selected proof. Test TestMissingLemmas drives the lifecycle headlessly: all lemmas initially missing, creating a side proof registers it in the environment and leaves the (open) lemma missing, discharging it removes it from the missing set, and re-requesting returns the same proof (no duplicate). Created with AI tooling support --- .../ilkd/key/rule/lemma/GeneratedLemma.java | 33 +++- .../lemma/GeneratedLemmaJustification.java | 3 +- .../rule/lemma/GeneratedLemmaRegistry.java | 32 ++++ .../key/rule/lemma/TestMissingLemmas.java | 98 ++++++++++++ .../uka/ilkd/key/gui/MissingLemmasPanel.java | 145 ++++++++++++++++++ .../ilkd/key/gui/ProofManagementDialog.java | 25 ++- 6 files changed, 329 insertions(+), 7 deletions(-) create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java create mode 100644 key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java index 9527372f8bb..a08b0c7c2a8 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -37,6 +37,7 @@ public final class GeneratedLemma { private final RewriteTaclet taclet; private final Proof mainProof; private final GeneratedLemmaJustification justification; + private @Nullable ProofAggregate soundnessProofAggregate; private @Nullable Proof soundnessProof; GeneratedLemma(RewriteTaclet taclet, Proof mainProof, Name generatorName) { @@ -70,15 +71,39 @@ public RewriteTaclet taclet() { return soundnessProof; } + /** + * returns true iff the soundness proof obligation has been created and is not disposed + */ + public synchronized boolean isSoundnessProofPresent() { + return soundnessProof != null && !soundnessProof.isDisposed(); + } + + /** + * returns true iff the soundness proof obligation has been created and closed + */ + public synchronized boolean isProven() { + return isSoundnessProofPresent() && soundnessProof.closed(); + } + /** * returns the proof for the soundness proof obligation of the taclet, creating (and, if a * proof environment is present, registering) it on first call */ public synchronized Proof getOrCreateSoundnessProof() { + return getOrCreateSoundnessProofAggregate().getFirstProof(); + } + + /** + * returns the proof aggregate for the soundness proof obligation of the taclet, creating + * (and, if a proof environment is present, registering in it) it on first call. The + * aggregate is what a user interface registers to display and drive the proof. + */ + public synchronized ProofAggregate getOrCreateSoundnessProofAggregate() { if (soundnessProof == null || soundnessProof.isDisposed()) { - soundnessProof = createSoundnessProof(); + soundnessProofAggregate = createSoundnessProof(); + soundnessProof = soundnessProofAggregate.getFirstProof(); } - return soundnessProof; + return soundnessProofAggregate; } /** @@ -87,7 +112,7 @@ public synchronized Proof getOrCreateSoundnessProof() { * generated taclet itself is not part of that configuration, so it cannot be used to prove * itself. */ - private Proof createSoundnessProof() { + private ProofAggregate createSoundnessProof() { final InitConfig poConfig = mainProof.getInitConfig().deepCopy(); final ImmutableSet tacletsToProve = DefaultImmutableSet.nil().add(taclet); @@ -98,6 +123,6 @@ private Proof createSoundnessProof() { if (env != null) { env.registerProof(new GeneratedLemmaPO(taclet.name(), po), po); } - return po.getFirstProof(); + return po; } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java index a55a547befd..64912acc333 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java @@ -60,8 +60,7 @@ public GeneratedLemma getLemma() { * {@link de.uka.ilkd.key.proof.mgt.ProofCorrectnessMgt}) */ public boolean isProven() { - final Proof soundnessProof = lemma.getSoundnessProofIfPresent(); - return soundnessProof != null && !soundnessProof.isDisposed() && soundnessProof.closed(); + return lemma.isProven(); } @Override diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java index 215a3e2dd14..0937e427200 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java @@ -3,9 +3,11 @@ * SPDX-License-Identifier: GPL-2.0-only */ package de.uka.ilkd.key.rule.lemma; +import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import de.uka.ilkd.key.proof.Goal; @@ -96,4 +98,34 @@ public synchronized GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, public synchronized Collection getLemmas() { return Collections.unmodifiableCollection(lemmas.values()); } + + /** + * returns the lemmas generated for this proof whose soundness proof obligation has not yet + * been discharged — either it has not been created, or it has been created but is not closed. + * These are the lemmas that the proof is still closed only modulo (see + * {@link de.uka.ilkd.key.proof.mgt.ProofCorrectnessMgt}). + * + * @return the missing lemmas, in generation order + */ + public synchronized List getMissingLemmas() { + final List missing = new ArrayList<>(); + for (final GeneratedLemma lemma : lemmas.values()) { + if (!lemma.isProven()) { + missing.add(lemma); + } + } + return missing; + } + + /** + * returns the registry of the given proof if one exists, without creating it; used by clients + * that only inspect (e.g. the user interface) and must not attach a registry to proofs that + * never generated a lemma + * + * @param proof the proof + * @return the registry, or {@code null} if the proof has no generated lemmas + */ + public static @Nullable GeneratedLemmaRegistry getIfPresent(Proof proof) { + return proof.lookup(GeneratedLemmaRegistry.class); + } } diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java new file mode 100644 index 00000000000..ae73013ce12 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java @@ -0,0 +1,98 @@ +/* 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.lemma; + +import java.nio.file.Path; +import java.util.List; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests the "missing lemmas" bookkeeping that backs the Missing Lemmas tab of the proof + * management dialog: after a proof is elaborated (or found in transparent mode) it depends on + * generated lemmas whose soundness proof obligations are initially missing; creating and + * discharging those obligations removes them from the missing set and registers them in the + * proof environment. + */ +public class TestMissingLemmas { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + @Test + public void testMissingLemmasLifecycle() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + + try (KeYEnvironment env = KeYEnvironment.load(file); + KeYEnvironment targetEnv = + KeYEnvironment.load(file)) { + + final Proof original = env.getLoadedProof(); + assertNotNull(original); + final StrategyProperties sp = + original.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_ON); + original.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + OneStepSimplifier.refreshOSS(original); + new AutomaticProver().start(original, 20000, 120000); + assertTrue(original.closed()); + + final Proof target = targetEnv.getLoadedProof(); + assertNotNull(target); + OneStepSimplifier.refreshOSS(target); + final LemmaElaboratingProofReplayer replayer = + LemmaElaboratingProofReplayer.elaborate(original, target); + assertTrue(target.closed()); + assertTrue(replayer.getElaboratedCount() > 0); + + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(target); + + // initially every generated lemma is missing (no soundness proof created yet) + final List missing = registry.getMissingLemmas(); + assertEquals(registry.getLemmas().size(), missing.size(), + "all generated lemmas are initially missing"); + assertFalse(missing.isEmpty()); + + // "load as side proof": create the soundness proof aggregate for one lemma; it gets + // registered in the target's proof environment + final GeneratedLemma first = missing.get(0); + assertFalse(first.isSoundnessProofPresent()); + final ProofAggregate aggregate = first.getOrCreateSoundnessProofAggregate(); + final Proof soundnessProof = aggregate.getFirstProof(); + assertTrue(first.isSoundnessProofPresent()); + assertSame(target.getEnv(), soundnessProof.getEnv(), + "soundness proof must live in the same proof environment as the main proof"); + assertTrue(target.getEnv().getProofs().stream() + .anyMatch(pl -> pl.getFirstProof() == soundnessProof), + "soundness proof must be registered in the proof environment"); + + // an open (not yet closed) soundness proof still counts as missing + assertTrue(registry.getMissingLemmas().contains(first), + "an unclosed soundness proof leaves the lemma in the missing set"); + + // discharge it: the lemma leaves the missing set + new AutomaticProver().start(soundnessProof, 10000, 60000); + assertTrue(soundnessProof.closed()); + assertTrue(first.isProven()); + assertFalse(registry.getMissingLemmas().contains(first), + "a proven lemma is no longer missing"); + assertEquals(missing.size() - 1, registry.getMissingLemmas().size()); + + // requesting the aggregate again returns the same proof (no duplicate side proof) + assertSame(soundnessProof, first.getOrCreateSoundnessProofAggregate().getFirstProof()); + } + } +} diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java new file mode 100644 index 00000000000..80041e8b24a --- /dev/null +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java @@ -0,0 +1,145 @@ +/* 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.gui; + +import java.awt.BorderLayout; +import java.awt.Component; +import java.awt.FlowLayout; +import java.util.List; +import javax.swing.*; +import javax.swing.border.TitledBorder; + +import de.uka.ilkd.key.core.KeYMediator; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.ProofAggregate; +import de.uka.ilkd.key.rule.lemma.GeneratedLemma; +import de.uka.ilkd.key.rule.lemma.GeneratedLemmaRegistry; + +import org.jspecify.annotations.Nullable; + +/** + * Panel listing the generated lemmas that a proof still depends on but whose soundness has not + * yet been established (see {@link GeneratedLemmaRegistry#getMissingLemmas()}). The user can + * select individual entries or all of them and load their soundness proof obligations as side + * proofs into the same proof environment, where they become visible and provable in the KeY GUI. + * + * @see de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule + */ +public final class MissingLemmasPanel extends JPanel { + + private static final long serialVersionUID = 1L; + + private final KeYMediator mediator; + private final DefaultListModel model = new DefaultListModel<>(); + private final JList lemmaList = new JList<>(model); + private final JButton proveButton = new JButton("Load selected as side proofs"); + private final JButton selectAllButton = new JButton("Select all"); + private @Nullable Proof proof; + + public MissingLemmasPanel(KeYMediator mediator) { + super(new BorderLayout()); + this.mediator = mediator; + + lemmaList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); + lemmaList.setCellRenderer(new LemmaCellRenderer()); + lemmaList.addListSelectionListener(e -> updateButtons()); + + final JScrollPane scrollPane = new JScrollPane(lemmaList); + scrollPane + .setBorder(new TitledBorder("Generated lemmas without a (closed) soundness proof")); + add(scrollPane, BorderLayout.CENTER); + + selectAllButton.addActionListener(e -> { + if (model.getSize() > 0) { + lemmaList.setSelectionInterval(0, model.getSize() - 1); + } + }); + proveButton.addActionListener(e -> loadSelected()); + + final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5)); + buttonPanel.add(selectAllButton); + buttonPanel.add(proveButton); + add(buttonPanel, BorderLayout.SOUTH); + + updateButtons(); + } + + /** + * Shows the missing lemmas of the given proof (or clears the list if the proof is + * {@code null} or has generated no lemmas). + * + * @param proof the proof whose missing lemmas to display + */ + public void setProof(Proof proof) { + this.proof = proof; + refresh(); + } + + private void refresh() { + model.clear(); + if (proof != null) { + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.getIfPresent(proof); + if (registry != null) { + for (final GeneratedLemma lemma : registry.getMissingLemmas()) { + model.addElement(lemma); + } + } + } + updateButtons(); + } + + private void updateButtons() { + selectAllButton.setEnabled(model.getSize() > 0); + proveButton.setEnabled(!lemmaList.getSelectedValuesList().isEmpty()); + } + + private void loadSelected() { + final List selected = lemmaList.getSelectedValuesList(); + if (selected.isEmpty()) { + return; + } + Proof firstLoaded = null; + for (final GeneratedLemma lemma : selected) { + final boolean alreadyShown = lemma.isSoundnessProofPresent(); + final ProofAggregate aggregate = lemma.getOrCreateSoundnessProofAggregate(); + // the soundness proof is registered in the proof environment when created; only add + // it to the user interface (task tree) if it is not already there + if (!alreadyShown) { + mediator.getUI().registerProofAggregate(aggregate); + } + if (firstLoaded == null) { + firstLoaded = aggregate.getFirstProof(); + } + } + if (firstLoaded != null) { + mediator.getSelectionModel().setSelectedProof(firstLoaded); + } + // the listed proofs now exist; drop the ones that are already closed, keep the rest so + // the user can continue proving them + refresh(); + } + + private static final class LemmaCellRenderer extends DefaultListCellRenderer { + private static final long serialVersionUID = 1L; + + @Override + public Component getListCellRendererComponent(JList list, Object value, int index, + boolean isSelected, boolean cellHasFocus) { + final Component result = super.getListCellRendererComponent(list, value, index, + isSelected, cellHasFocus); + if (value instanceof GeneratedLemma lemma && result instanceof JLabel label) { + final String status; + if (!lemma.isSoundnessProofPresent()) { + status = "soundness proof obligation not yet created"; + } else if (lemma.isProven()) { + status = "proven"; + } else { + status = "soundness proof open"; + } + label.setText(lemma.taclet().name() + " (" + status + ")"); + } + return result; + } + } +} diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java index 747a755fbc7..95bf6c14211 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java @@ -70,6 +70,7 @@ public final class ProofManagementDialog extends JDialog { private JList proofList; private ContractSelectionPanel contractPanelByMethod; private ContractSelectionPanel contractPanelByProof; + private MissingLemmasPanel missingLemmasPanel; private JButton startButton; private JButton cancelButton; private final KeYMediator mediator; @@ -142,7 +143,10 @@ public Component getListCellRendererComponent(JList list, Object value, int i return result; } }); - proofList.addListSelectionListener(e -> updateContractPanel()); + proofList.addListSelectionListener(e -> { + updateContractPanel(); + updateMissingLemmasPanel(); + }); // create method list panel, scroll pane JPanel listPanelByMethod = new JPanel(); @@ -190,15 +194,21 @@ public void mouseClicked(MouseEvent e) { contractPanelByProof.addListSelectionListener(e -> updateStartButton()); listPanelByProof.add(contractPanelByProof); + // create missing-lemmas panel (shows generated lemmas of the selected proof whose + // soundness has not yet been established, and lets the user load them as side proofs) + missingLemmasPanel = new MissingLemmasPanel(mediator); + // create tabbed pane tabbedPane = new JTabbedPane(); tabbedPane.addTab("By Target", listPanelByMethod); tabbedPane.addTab("By Proof", listPanelByProof); + tabbedPane.addTab("Missing Lemmas", missingLemmasPanel); tabbedPane.addChangeListener(e -> { updateStartButton(); if (proofList.getSelectedIndex() == -1 && proofList.getModel().getSize() > 0) { proofList.setSelectedIndex(0); } + updateMissingLemmasPanel(); }); getContentPane().add(tabbedPane); @@ -250,6 +260,7 @@ public void dispose() { classTree = null; contractPanelByMethod = null; contractPanelByProof = null; + missingLemmasPanel = null; startButton = null; cancelButton = null; // ============================================ @@ -561,6 +572,18 @@ private void updateContractPanel() { updateStartButton(); } + private void updateMissingLemmasPanel() { + if (missingLemmasPanel == null) { + return; + } + final ProofWrapper selected = proofList.getSelectedValue(); + Proof proof = selected != null ? selected.proof : null; + if (proof == null) { + proof = mediator.getSelectedProof(); + } + missingLemmasPanel.setProof(proof); + } + private void updateGlobalStatus() { // target icons From b1388172919339388408275c5f052dee5c941972 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 16:44:15 +0200 Subject: [PATCH 09/20] Fix transparent-mode automation on heap/SE proofs (SumAndMax runaway) Transparent automode on SumAndMax ran away: thousands of introduce_ossLemma steps, taclets that never applied, visually no-op lemmas, proof not closing (12000-node cap: 11200 introductions, 59 lemma applications, open). Root cause 1 - lemma name aliasing across branches: formulas on sibling branches can be equal in their printed form yet contain distinct proof-local symbol instances (program variables, skolem constants) sharing the same name. The purely content-derived (print-based) lemma names aliased such formulas, so the registry handed branch B a taclet built from branch A's instances; it could never match, and every automation round re-introduced it. Fix: OSS lemma names are qualified with the introduction node's tree-structural id (Node.getUniqueTacletId, the mechanism QueryExpand uses), which is replay-stable, branch-distinguishing, and prune-consistent. Within-branch reuse is unaffected (the introduced taclet is inherited and applied). The registry additionally verifies on every name hit that the cached taclet's find equals the regenerated one, turning any residual aliasing into a RuleAbortException instead of a dead taclet. Root cause 2 - semantic no-op lemmas: aggregated simplifications whose result equals the formula up to renaming and term labels produced worthless lemmas and kept the formula eligible forever. The generator now rejects them and vetoes the formula; the veto is proof-local state in the registry (a JVM-wide cache would leak vetoes into the replay of a reloaded proof). Root cause 3 - index-dependent applicability: the previous guard against re-introduction consulted the goal-local taclet index in isApplicable. Rule applications are queued, so the original run could legitimately record an introduction that the guard would reject when re-evaluated freshly - which is exactly what proof replay does, making a few recorded introductions unreplayable. Built-in applicability is now pure in the formula; occasional redundant introductions are harmless (unique names) and replay faithfully. SumAndMax with transparent automode now closes (2568 nodes, 241 introductions = 241 distinct lemmas, 229 applied, opaque OSS only on modality formulas, no no-op lemmas) and the saved proof replays to closed; kept as regression test TestTransparentModeSumAndMax. Created with AI tooling support --- .../rule/lemma/GeneratedLemmaRegistry.java | 39 ++++++ .../key/rule/lemma/LemmaIntroductionRule.java | 50 ++----- .../key/rule/lemma/LemmaTacletGenerator.java | 10 +- .../key/rule/lemma/OssLemmaGenerator.java | 32 ++++- .../lemma/TestTransparentModeSumAndMax.java | 131 ++++++++++++++++++ 5 files changed, 220 insertions(+), 42 deletions(-) create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java index 0937e427200..af9b6c2c723 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java @@ -15,7 +15,10 @@ import de.uka.ilkd.key.rule.RewriteTaclet; import org.key_project.logic.Name; +import org.key_project.prover.rules.RuleAbortException; import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.LRUCache; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -34,11 +37,37 @@ @NullMarked public final class GeneratedLemmaRegistry { + private static final int VETO_CACHE_SIZE = 1000; + private final Map lemmas = new LinkedHashMap<>(); + /** + * Formulas for which lemma generation turned out not to be worthwhile (the aggregated + * transformation is a semantic no-op). Vetoed formulas are not offered for introduction + * again: without this, automation would not terminate. The veto is proof-local state — a + * JVM-wide cache would leak vetoes into the replay of a reloaded proof, where a formula + * vetoed late in the original run (in some branch context) would suppress the replay of an + * introduction recorded earlier (in another context). + */ + private final Map vetoed = new LRUCache<>(VETO_CACHE_SIZE); + private GeneratedLemmaRegistry() { } + /** + * marks the formula as not worth a lemma (see {@link #vetoed}) + */ + public synchronized void veto(SequentFormula formula) { + vetoed.put(formula, Boolean.TRUE); + } + + /** + * returns true iff lemma generation has been vetoed for the given formula + */ + public synchronized boolean isVetoed(SequentFormula formula) { + return vetoed.get(formula) != null; + } + /** * returns the registry of the given proof, creating it on first access */ @@ -70,6 +99,16 @@ public synchronized GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, final GeneratedLemma existing = lemmas.get(taclet.name()); if (existing != null) { + // a name hit must mean identical content: formulas on different branches can be + // equal in their printed form yet contain distinct proof-local symbol instances, + // and handing out a taclet for the wrong instances would produce a rule that never + // matches. Generators avoid this by making names unique per introduction where + // necessary; this check turns any residual name aliasing into a visible abort. + if (!existing.taclet().find().equals(taclet.find())) { + throw new RuleAbortException("generated lemma name aliasing: " + taclet.name() + + " already denotes a lemma with different content" + + " (distinct proof-local symbols?)"); + } // pruning removes the justification together with the goal-locally introduced // taclet; re-register the identical justification instance in that case if (proof.getInitConfig().getJustifInfo() diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java index 307709b8388..fe804810b09 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaIntroductionRule.java @@ -9,11 +9,9 @@ import de.uka.ilkd.key.rule.DefaultBuiltInRuleApp; import de.uka.ilkd.key.rule.IBuiltInRuleApp; import de.uka.ilkd.key.rule.NoPosTacletApp; -import de.uka.ilkd.key.rule.TacletApp; import de.uka.ilkd.key.rule.inst.SVInstantiations; import org.key_project.logic.Name; -import org.key_project.prover.proof.rulefilter.TacletFilter; import org.key_project.prover.rules.RuleApp; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.util.collection.ImmutableList; @@ -63,44 +61,24 @@ public String toString() { return displayName(); } - @Override - public boolean isApplicable(Goal goal, @Nullable PosInOccurrence pio) { - return pio != null && generator.isApplicable(goal, pio) - && !lemmaAlreadyAvailable(goal, pio); - } - /** - * Checks whether a lemma taclet of this generator is already available and usable at the - * given position. In that case the introduction is not offered again: re-introduction would - * be redundant, and — since the introduction step does not change the sequent — automated - * application would not terminate without this check. + * {@inheritDoc} + * + *

+ * The result must depend only on the formula at the position (and formula-keyed state such + * as the generator's veto bookkeeping), never on the goal-local rule indices: built-in rule + * applications are queued and may be executed after further rules changed the goal, and + * proof replay re-evaluates applicability freshly at the recorded position. An + * index-dependent condition would make recorded introductions unreplayable whenever the + * queued application was executed after the condition had changed. As a consequence, an + * introduction may occasionally be redundant (an equivalent lemma is already available); + * this is harmless, since lemma names are unique per introduction. */ - private boolean lemmaAlreadyAvailable(Goal goal, PosInOccurrence pio) { - final var services = goal.proof().getServices(); - final ImmutableList candidates = goal.indexOfTaclets() - .getRewriteTaclet(pio, GENERATED_BY_THIS_GENERATOR, services); - for (final NoPosTacletApp candidate : candidates) { - if (candidate.taclet().assumesSequent().isEmpty()) { - return true; - } - // assumption-carrying lemmas count as available only if their assumptions can - // actually be instantiated in the current sequent - final TacletApp positioned = candidate.setPosInOccurrence(pio, services); - if (positioned != null && (positioned.complete() || !positioned - .findIfFormulaInstantiations(goal.sequent(), services).isEmpty())) { - return true; - } - } - return false; + @Override + public boolean isApplicable(Goal goal, @Nullable PosInOccurrence pio) { + return pio != null && generator.isApplicable(goal, pio); } - private final TacletFilter GENERATED_BY_THIS_GENERATOR = new TacletFilter() { - @Override - protected boolean filter(org.key_project.prover.rules.Taclet taclet) { - return generator.name().toString().equals(taclet.displayName()); - } - }; - @Override public boolean isApplicableOnSubTerms() { return true; diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java index 31712f8a36e..35649897cff 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java @@ -32,9 +32,13 @@ * proof obligation machinery (see * {@link de.uka.ilkd.key.taclettranslation.lemma.DefaultLemmaGenerator}): no variable conditions, * no metaconstructs, no modalities. - *

  • Determinism: the taclet (in particular its name) must be derived solely from the - * content of the term at the given position, so that replaying the introducing rule application - * regenerates an identical taclet.
  • + *
  • Determinism: replaying the introducing rule application (same node, same sequent) + * must regenerate an identical taclet, in particular the identical name. Purely content-derived + * names are only sound if the content cannot contain proof-local symbols: formulas on different + * branches can be equal in their printed form yet contain distinct program variable or skolem + * constant instances sharing the same name. Generators for such content must additionally + * qualify the name with a replay-stable introduction-point id (see + * {@link de.uka.ilkd.key.proof.Node#getUniqueTacletId()}).
  • * */ @NullMarked diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java index 68b35488533..71bd93aa7dd 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java @@ -22,6 +22,7 @@ import org.key_project.logic.Term; import org.key_project.logic.op.Modality; import org.key_project.prover.rules.ApplicationRestriction; +import org.key_project.prover.rules.RuleAbortException; import org.key_project.prover.rules.RuleSet; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.sequent.SequentFormula; @@ -29,6 +30,8 @@ import org.jspecify.annotations.NullMarked; +import static de.uka.ilkd.key.logic.equality.RenamingTermProperty.RENAMING_TERM_PROPERTY; + /** * Lemma generator capturing an aggregated one-step-simplification as a taclet: for a top level * formula {@code F} that the {@link OneStepSimplifier} would simplify to {@code F'} using the @@ -78,6 +81,11 @@ public boolean isApplicable(Goal goal, PosInOccurrence pio) { if (!pio.isTopLevel()) { return false; } + final GeneratedLemmaRegistry registry = + GeneratedLemmaRegistry.getIfPresent(goal.proof()); + if (registry != null && registry.isVetoed(pio.sequentFormula())) { + return false; + } final OneStepSimplifier simplifier = MiscTools.findOneStepSimplifier(goal.proof()); return simplifier != null && simplifier.canSimplify(goal, pio) && !containsModality(pio.sequentFormula().formula()); @@ -94,14 +102,24 @@ public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { final OneStepSimplifier.SimplificationResult result = simplifier.computeSimplification(goal, pio, new OneStepSimplifier.Protocol()); if (result == null) { - throw new IllegalStateException("one step simplification of " + GeneratedLemmaRegistry.get(goal.proof()).veto(pio.sequentFormula()); + throw new RuleAbortException("one step simplification of " + pio.sequentFormula().formula() - + " failed (is the one step simplifier active for this proof?)"); + + " produced no result (is the one step simplifier active for this proof?)"); } final JTerm find = (JTerm) pio.sequentFormula().formula(); final JTerm simplified = (JTerm) result.simplified().formula(); + if (RENAMING_TERM_PROPERTY.equalsModThisProperty(find, simplified)) { + // the aggregated simplification only shuffles term labels or bound variable names; + // a lemma \find(F) \replacewith(F') with F == F' modulo those is worthless and, + // under automation, a source of non-termination + GeneratedLemmaRegistry.get(goal.proof()).veto(pio.sequentFormula()); + throw new RuleAbortException( + "one step simplification changes " + find + " only up to renaming/term labels"); + } + ImmutableList assumesAntec = ImmutableList.nil(); ImmutableList assumesSucc = ImmutableList.nil(); for (final PosInOccurrence context : result.usedContextFormulas()) { @@ -113,8 +131,16 @@ public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { } final RewriteTacletBuilder tb = new RewriteTacletBuilder<>(); + // the content hash is a human-recognizable prefix only; uniqueness comes from the + // introduction node's tree-structural id. Formulas on sibling branches can be equal in + // their printed form yet contain distinct proof-local symbol instances (program + // variables, skolem constants) sharing the same name, so a purely content-derived name + // would alias semantically different lemmas across branches. The node id is + // replay-stable (path and introduced-rule count), so replaying the introduction + // regenerates the same name. tb.setName(MiscTools.toValidTacletName( - contentHashName(services, find, assumesAntec, assumesSucc, simplified))); + contentHashName(services, find, assumesAntec, assumesSucc, simplified) + "_" + + goal.node().getUniqueTacletId())); tb.setDisplayName(NAME.toString()); tb.setFind(find); tb.addGoalTerm(simplified); diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java new file mode 100644 index 00000000000..7c00b4dba63 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java @@ -0,0 +1,131 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +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.proof.io.ProofSaver; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.rule.RewriteTaclet; +import de.uka.ilkd.key.rule.tacletbuilder.RewriteTacletGoalTemplate; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static de.uka.ilkd.key.logic.equality.RenamingTermProperty.RENAMING_TERM_PROPERTY; +import static org.junit.jupiter.api.Assertions.*; + +/** + * Regression test for transparent mode on a heap/symbolic-execution scale proof (SumAndMax). + * Guards against two automation defects observed on this example: + *
      + *
    • dead re-introductions: formulas on sibling branches that are equal in their printed form + * but contain distinct proof-local symbol instances aliased to the same lemma, producing taclets + * that never match and an endless stream of introduction steps (fixed by qualifying lemma names + * with the replay-stable introduction-node id);
    • + *
    • no-op lemmas whose find and replacewith differ only in term labels or bound variable + * names (rejected by the generator).
    • + *
    + */ +public class TestTransparentModeSumAndMax { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + @Test + public void testTransparentAutomodeClosesSumAndMax() throws Exception { + final Path file = TEST_CASE_DIRECTORY.resolve( + "../../../../../key.ui/examples/heap/vstte10_01_SumAndMax/SumAndMax_sumAndMax.key"); + assertTrue(Files.exists(file)); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, + StrategyProperties.OSS_TRANSPARENT); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + proof.setActiveStrategy(proof.getServices().getProfile() + .getDefaultStrategyFactory().create(proof, sp)); + OneStepSimplifier.refreshOSS(proof); + + new AutomaticProver().start(proof, 12000, 300000); + assertTrue(proof.closed(), "transparent automode did not close SumAndMax"); + + int intro = 0; + int lemmaApps = 0; + final var nodes = proof.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node node = nodes.next(); + final var app = node.getAppliedRuleApp(); + if (app == null) { + continue; + } + if (app.rule() == OssLemmaIntroductionRule.INSTANCE) { + intro++; + } else if (app instanceof OneStepSimplifierRuleApp ossApp) { + assertTrue( + OssLemmaGenerator.containsModality( + ossApp.posInOccurrence().sequentFormula().formula()), + "opaque simplifier application on a lemma-eligible formula"); + } else if (app.rule().name().toString().startsWith("ossLemma_")) { + lemmaApps++; + final RewriteTaclet taclet = (RewriteTaclet) app.rule(); + final JTerm find = (JTerm) taclet.find(); + final JTerm replaceWith = (JTerm) ((RewriteTacletGoalTemplate) taclet + .goalTemplates().head()).replaceWith(); + assertFalse( + RENAMING_TERM_PROPERTY.equalsModThisProperty(find, replaceWith), + "no-op lemma applied: " + taclet.name()); + } + } + + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + assertTrue(intro > 0); + assertEquals(intro, registry.getLemmas().size(), + "every introduction must mint a fresh lemma - a mismatch means dead" + + " re-introductions of aliased lemmas"); + assertTrue(intro - lemmaApps <= intro / 4, + "most introduced lemmas should actually be applied (intro=" + intro + + ", applied=" + lemmaApps + ")"); + + // the introduction-node-qualified names must be replay-stable. The proof is saved + // inside the build tree: the problem has a boot classpath below the checkout, and + // the saver relativizes paths against the save location. + final Path tmpDir = Files.createDirectories(Path.of("build", "tmp", "lemmaTests")); + final Path proofFile = + Files.createTempFile(tmpDir, "sumAndMaxTransparent", ".proof"); + try { + assertNull(new ProofSaver(proof, proofFile).save()); + try (KeYEnvironment reloadedEnv = + KeYEnvironment.load(proofFile)) { + final Proof reloaded = reloadedEnv.getLoadedProof(); + assertNotNull(reloaded); + if (!reloaded.closed() && reloadedEnv.getReplayResult() != null) { + // diagnostics on failure only + reloadedEnv.getReplayResult().getErrorList().stream().limit(3) + .forEach(t -> System.err.println("replay error: " + t + + (t.getCause() != null ? " caused by " + t.getCause() + : ""))); + } + assertTrue(reloaded.closed(), + "reloaded transparent SumAndMax proof did not replay to closed"); + assertEquals(proof.countNodes(), reloaded.countNodes()); + } + } finally { + Files.deleteIfExists(proofFile); + } + } + } +} From b3df7bbfd6272aca303f2876ded0762111e721c0 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 17:00:08 +0200 Subject: [PATCH 10/20] Fix circular lemma soundness proof + duplicate GUI registration Circularity (soundness bug): a generated lemma's soundness proof obligation inherited the main proof's strategy settings. In transparent mode that meant the obligation was discharged by generating and applying further lemmas - re-deriving the very simplification it must justify from first principles, a circular and non-terminating justification (probe: 2 introductions + 1 lemma application inside the obligation proof). GeneratedLemma now forces the opaque one step simplifier on the proof-obligation configuration, so soundness is established in the base calculus. Regression test TestLemmaSoundnessNonCircular asserts the obligation runs OSS_ON and contains no introduce_ossLemma / ossLemma_ steps. Duplicate task-tree entries: creating a lemma soundness proof registers it in the proof environment, and the user interface already listens on the environment (proofRegistered -> registerProofAggregate -> task tree). The Missing Lemmas panel registered it a second time explicitly; that call is removed. The panel now also runs an onLoaded action so the proof management dialog closes after loading (previously it stayed open). Created with AI tooling support --- .../ilkd/key/rule/lemma/GeneratedLemma.java | 26 ++++++ .../lemma/TestLemmaSoundnessNonCircular.java | 88 +++++++++++++++++++ .../uka/ilkd/key/gui/MissingLemmasPanel.java | 26 ++++-- .../ilkd/key/gui/ProofManagementDialog.java | 1 + 4 files changed, 132 insertions(+), 9 deletions(-) create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java index a08b0c7c2a8..7d8255ee666 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -11,6 +11,8 @@ import de.uka.ilkd.key.proof.mgt.ProofEnvironment; import de.uka.ilkd.key.rule.RewriteTaclet; import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.settings.ProofSettings; +import de.uka.ilkd.key.strategy.StrategyProperties; import de.uka.ilkd.key.taclettranslation.lemma.ProofObligationCreator; import org.key_project.logic.Name; @@ -114,6 +116,12 @@ public synchronized ProofAggregate getOrCreateSoundnessProofAggregate() { */ private ProofAggregate createSoundnessProof() { final InitConfig poConfig = mainProof.getInitConfig().deepCopy(); + // The soundness of a generated lemma must be established in the base calculus. If the + // main proof runs the one step simplifier in transparent mode, the copied configuration + // would too, and the lemma's proof obligation would be discharged by generating and + // applying further lemmas — re-doing the very simplification it is meant to justify + // (a circular, non-terminating justification). Force the opaque simplifier here. + forceOpaqueOneStepSimplification(poConfig); final ImmutableSet tacletsToProve = DefaultImmutableSet.nil().add(taclet); final ProofAggregate po = new ProofObligationCreator().create(tacletsToProve, @@ -125,4 +133,22 @@ private ProofAggregate createSoundnessProof() { } return po; } + + /** + * Switches the one step simplifier of the given configuration to its opaque mode, so that a + * proof run in it does not itself generate lemmas (see {@link #createSoundnessProof()}). + */ + private static void forceOpaqueOneStepSimplification(InitConfig config) { + final ProofSettings settings = config.getSettings(); + if (settings == null) { + return; + } + final StrategyProperties sp = + settings.getStrategySettings().getActiveStrategyProperties(); + if (StrategyProperties.OSS_TRANSPARENT + .equals(sp.getProperty(StrategyProperties.OSS_OPTIONS_KEY))) { + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_ON); + settings.getStrategySettings().setActiveStrategyProperties(sp); + } + } } diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java new file mode 100644 index 00000000000..9af49a14db8 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java @@ -0,0 +1,88 @@ +/* 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.lemma; + +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Guards the soundness of generated lemmas against circular justification: even when the main + * proof runs the one step simplifier in transparent mode, a lemma's soundness proof obligation + * must be discharged in the base calculus (opaque simplifier), never by generating and applying + * further lemmas — which would re-derive the very simplification the obligation is meant to + * justify, and never terminate. + */ +public class TestLemmaSoundnessNonCircular { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + @Test + public void testSoundnessProofUsesBaseCalculus() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, + StrategyProperties.OSS_TRANSPARENT); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + proof.setActiveStrategy(proof.getServices().getProfile() + .getDefaultStrategyFactory().create(proof, sp)); + OneStepSimplifier.refreshOSS(proof); + new AutomaticProver().start(proof, 20000, 120000); + assertTrue(proof.closed()); + + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + assertFalse(registry.getMissingLemmas().isEmpty()); + + for (final GeneratedLemma lemma : registry.getMissingLemmas()) { + final Proof po = lemma.getOrCreateSoundnessProof(); + + // the soundness proof runs the opaque simplifier, not the transparent one + assertEquals(StrategyProperties.OSS_ON, + po.getSettings().getStrategySettings().getActiveStrategyProperties() + .getProperty(StrategyProperties.OSS_OPTIONS_KEY), + "the soundness proof of " + lemma.taclet().name() + + " must run in the base calculus (opaque OSS)"); + + new AutomaticProver().start(po, 10000, 60000); + assertTrue(po.closed(), + "soundness proof of " + lemma.taclet().name() + " did not close"); + + // it must not contain any lemma introduction or application (no circularity) + final var nodes = po.root().subtreeIterator(); + while (nodes.hasNext()) { + final Node node = nodes.next(); + final var app = node.getAppliedRuleApp(); + if (app == null) { + continue; + } + final String ruleName = app.rule().name().toString(); + assertFalse(ruleName.startsWith("introduce_ossLemma"), + "soundness proof of " + lemma.taclet().name() + + " introduces a lemma (circular)"); + assertFalse(ruleName.startsWith("ossLemma_"), + "soundness proof of " + lemma.taclet().name() + + " applies a generated lemma (circular): " + ruleName); + } + } + } + } +} diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java index 80041e8b24a..c029fa8fb89 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java @@ -36,6 +36,7 @@ public final class MissingLemmasPanel extends JPanel { private final JButton proveButton = new JButton("Load selected as side proofs"); private final JButton selectAllButton = new JButton("Select all"); private @Nullable Proof proof; + private @Nullable Runnable onLoaded; public MissingLemmasPanel(KeYMediator mediator) { super(new BorderLayout()); @@ -76,6 +77,16 @@ public void setProof(Proof proof) { refresh(); } + /** + * Sets an action to run after lemmas have been loaded as side proofs (e.g. to close the + * enclosing dialog). + * + * @param onLoaded the action, or {@code null} for none + */ + public void setOnLoaded(Runnable onLoaded) { + this.onLoaded = onLoaded; + } + private void refresh() { model.clear(); if (proof != null) { @@ -101,13 +112,10 @@ private void loadSelected() { } Proof firstLoaded = null; for (final GeneratedLemma lemma : selected) { - final boolean alreadyShown = lemma.isSoundnessProofPresent(); + // creating the soundness proof registers it in the proof environment; the user + // interface listens on the environment and adds it to the task tree, so the panel + // must not register it a second time (that produced duplicate task-tree entries) final ProofAggregate aggregate = lemma.getOrCreateSoundnessProofAggregate(); - // the soundness proof is registered in the proof environment when created; only add - // it to the user interface (task tree) if it is not already there - if (!alreadyShown) { - mediator.getUI().registerProofAggregate(aggregate); - } if (firstLoaded == null) { firstLoaded = aggregate.getFirstProof(); } @@ -115,9 +123,9 @@ private void loadSelected() { if (firstLoaded != null) { mediator.getSelectionModel().setSelectedProof(firstLoaded); } - // the listed proofs now exist; drop the ones that are already closed, keep the rest so - // the user can continue proving them - refresh(); + if (onLoaded != null) { + onLoaded.run(); + } } private static final class LemmaCellRenderer extends DefaultListCellRenderer { diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java index 95bf6c14211..76e18eca087 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java @@ -197,6 +197,7 @@ public void mouseClicked(MouseEvent e) { // create missing-lemmas panel (shows generated lemmas of the selected proof whose // soundness has not yet been established, and lets the user load them as side proofs) missingLemmasPanel = new MissingLemmasPanel(mediator); + missingLemmasPanel.setOnLoaded(() -> setVisible(false)); // create tabbed pane tabbedPane = new JTabbedPane(); From 31d75335ad75aef729b21a3345f79a9fdcd68236 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 17:11:16 +0200 Subject: [PATCH 11/20] Redesign lemma dependency view: per-proof, grouped, batch-provable Replaces the flat, dialog-global 'Missing Lemmas' tab with a per-proof dependency view living beside the used contracts, and organizes the many generated lemmas for overview. Placement: the 'By Proof' tab's right pane is now a Contracts | Lemmas tabbed pane, both scoped to the selected proof (the top-level Missing Lemmas tab is gone). This mirrors the used-contracts dependency view for lemmas. Overview: LemmaDependencyPanel shows the selected proof's lemmas as a tree grouped by generator and, within each generator, collapsed by content (GeneratedLemmaRegistry.groupByContent) - the many introduction-point-distinct instances of the same simplification appear as one row with a count and a proven/open status. On SumAndMax this turns ~241 flat rows into a handful of distinct simplification shapes. Actions: - 'Load selected as side proofs' creates the obligation proofs for the selected generator/content rows (registered in the environment, shown in the task tree). - 'Prove all lemmas...' batch-discharges via the new core LemmaProver: each obligation is proved with a user-chosen step bound (default 10000); obligations that do not close stay open for manual work; the obligation proofs can optionally be saved to a directory. Runs in the background; robust to a single obligation whose automatic search fails. Core additions (headless-tested in TestLemmaProverAndGrouping): GeneratedLemma.generatorName()/contentKey(); GeneratedLemmaRegistry.getLemmasByGenerator()/groupByContent(); LemmaProver. Created with AI tooling support --- .../ilkd/key/rule/lemma/GeneratedLemma.java | 38 +++ .../rule/lemma/GeneratedLemmaRegistry.java | 31 ++ .../uka/ilkd/key/rule/lemma/LemmaProver.java | 103 ++++++ .../lemma/TestLemmaProverAndGrouping.java | 129 ++++++++ .../ilkd/key/gui/LemmaDependencyPanel.java | 308 ++++++++++++++++++ .../uka/ilkd/key/gui/MissingLemmasPanel.java | 153 --------- .../ilkd/key/gui/ProofManagementDialog.java | 28 +- 7 files changed, 624 insertions(+), 166 deletions(-) create mode 100644 key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java create mode 100644 key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java delete mode 100644 key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java index 7d8255ee666..e1b44f1af3a 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -5,17 +5,22 @@ import java.util.List; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.ProofAggregate; import de.uka.ilkd.key.proof.init.InitConfig; +import de.uka.ilkd.key.proof.io.OutputStreamProofSaver; import de.uka.ilkd.key.proof.mgt.ProofEnvironment; import de.uka.ilkd.key.rule.RewriteTaclet; import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.tacletbuilder.RewriteTacletGoalTemplate; import de.uka.ilkd.key.settings.ProofSettings; import de.uka.ilkd.key.strategy.StrategyProperties; import de.uka.ilkd.key.taclettranslation.lemma.ProofObligationCreator; import org.key_project.logic.Name; +import org.key_project.prover.sequent.Sequent; import org.key_project.util.collection.DefaultImmutableSet; import org.key_project.util.collection.ImmutableSet; @@ -65,6 +70,39 @@ public RewriteTaclet taclet() { return taclet; } + /** + * returns the name of the generator that produced this lemma + */ + public Name generatorName() { + return justification.getGenerator(); + } + + /** + * A key identifying this lemma by its content, independent of the introduction point: + * printed find, assumptions (by polarity), and replacewith. Lemmas with the same content key + * denote the same simplification and are grouped for display (there may be many, one per + * introduction point / branch). Note that content-equal lemmas are still distinct taclets + * with their own soundness obligations, since their proof-local symbols may differ. + */ + public String contentKey() { + final Services services = mainProof.getServices(); + final StringBuilder key = new StringBuilder(); + key.append(OutputStreamProofSaver.printTerm((JTerm) taclet.find(), services)); + final Sequent assumes = taclet.assumesSequent(); + for (final var sf : assumes.antecedent()) { + key.append("\n<= ") + .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + } + for (final var sf : assumes.succedent()) { + key.append("\n=> ") + .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + } + final JTerm rw = (JTerm) ((RewriteTacletGoalTemplate) taclet.goalTemplates().head()) + .replaceWith(); + key.append("\n~> ").append(OutputStreamProofSaver.printTerm(rw, services)); + return key.toString(); + } + /** * returns the soundness proof if it has already been created, and {@code null} otherwise; the * proof obligation is not created by this method diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java index af9b6c2c723..fd97c3a5962 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java @@ -156,6 +156,37 @@ public synchronized List getMissingLemmas() { return missing; } + /** + * groups the generated lemmas by their generator, preserving generation order within each + * group + * + * @return a map from generator name to the lemmas it produced + */ + public synchronized Map> getLemmasByGenerator() { + final Map> byGenerator = new LinkedHashMap<>(); + for (final GeneratedLemma lemma : lemmas.values()) { + byGenerator.computeIfAbsent(lemma.generatorName(), k -> new ArrayList<>()).add(lemma); + } + return byGenerator; + } + + /** + * groups the given lemmas by their content key (see {@link GeneratedLemma#contentKey()}), so + * that lemmas denoting the same simplification (differing only in introduction point and + * proof-local symbol renaming) are collapsed. The order of first appearance is preserved. + * + * @param lemmas the lemmas to group + * @return a map from content key to the lemmas sharing it + */ + public static Map> groupByContent( + Collection lemmas) { + final Map> byContent = new LinkedHashMap<>(); + for (final GeneratedLemma lemma : lemmas) { + byContent.computeIfAbsent(lemma.contentKey(), k -> new ArrayList<>()).add(lemma); + } + return byContent; + } + /** * returns the registry of the given proof if one exists, without creating it; used by clients * that only inspect (e.g. the user interface) and must not attach a registry to proofs that diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java new file mode 100644 index 00000000000..2d67cd5c7a3 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java @@ -0,0 +1,103 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; +import de.uka.ilkd.key.util.MiscTools; + +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Batch-discharges the soundness proof obligations of generated lemmas: for each lemma it creates + * (if necessary) the obligation proof and runs automatic proof search with a step bound. Lemmas + * that close within the bound need no further attention; lemmas that do not are reported back so + * that they can be shown to the user for manual completion. + * + *

    + * The obligation proofs are created in the lemma's proof environment (see + * {@link GeneratedLemma#getOrCreateSoundnessProofAggregate()}), which surfaces them in an + * attached user interface. This class does not itself perform any user-interface registration. + */ +@NullMarked +public final class LemmaProver { + + private static final Logger LOGGER = LoggerFactory.getLogger(LemmaProver.class); + + /** + * Outcome of a batch proving run. + * + * @param proven lemmas whose soundness obligation closed within the step bound + * @param remaining lemmas whose soundness obligation did not close (shown for manual work) + * @param saved the number of obligation proofs written to disk, or 0 if saving was disabled + */ + public record Result(List proven, List remaining, int saved) { + } + + private LemmaProver() { + } + + /** + * Attempts to discharge the soundness obligations of the given lemmas. + * + * @param lemmas the lemmas to prove + * @param maxSteps the automatic-mode step bound per obligation + * @param saveDir if non-null, a directory into which every obligation proof is saved + * @return the outcome + * @throws Exception if saving a proof fails + */ + public static Result proveAll(Collection lemmas, int maxSteps, + @Nullable Path saveDir) throws Exception { + final List proven = new ArrayList<>(); + final List remaining = new ArrayList<>(); + int saved = 0; + + if (saveDir != null) { + Files.createDirectories(saveDir); + } + + for (final GeneratedLemma lemma : lemmas) { + if (lemma.isProven()) { + proven.add(lemma); + } else { + final Proof po = lemma.getOrCreateSoundnessProof(); + if (!po.closed()) { + try { + new AutomaticProver().start(po, maxSteps, -1); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } catch (RuntimeException e) { + // a single obligation whose automatic proof search fails must not abort + // the batch; it is reported as remaining for manual inspection + LOGGER.warn("automatic proof of lemma {} failed", + lemma.taclet().name(), e); + } + } + (po.closed() ? proven : remaining).add(lemma); + + if (saveDir != null) { + final Path file = saveDir.resolve( + MiscTools.toValidFileName(lemma.taclet().name().toString()) + ".proof"); + final String error = new ProofSaver(po, file).save(); + if (error != null) { + throw new IllegalStateException( + "saving lemma proof failed: " + error); + } + saved++; + } + } + } + return new Result(proven, remaining, saved); + } +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java new file mode 100644 index 00000000000..26fd475b98a --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java @@ -0,0 +1,129 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.logic.Name; +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Tests the batch proving and content grouping that back the redesigned lemma dependency view: + * generated lemmas are grouped by generator and by content, and their soundness obligations are + * discharged in a bounded batch, with those that do not close reported for manual completion. + */ +public class TestLemmaProverAndGrouping { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + private static Proof transparentClosedSumAndMax( + KeYEnvironment env) throws Exception { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_TRANSPARENT); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + proof.setActiveStrategy( + proof.getServices().getProfile().getDefaultStrategyFactory().create(proof, sp)); + OneStepSimplifier.refreshOSS(proof); + new AutomaticProver().start(proof, 20000, 300000); + assertTrue(proof.closed()); + return proof; + } + + private static Proof transparentClosed(KeYEnvironment env, + int maxSteps) throws Exception { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_TRANSPARENT); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + proof.setActiveStrategy( + proof.getServices().getProfile().getDefaultStrategyFactory().create(proof, sp)); + OneStepSimplifier.refreshOSS(proof); + new AutomaticProver().start(proof, maxSteps, 300000); + assertTrue(proof.closed()); + return proof; + } + + @Test + public void testContentGroupingCollapsesDuplicates() throws Exception { + // SumAndMax generates many introduction-point-distinct lemmas that denote the same + // simplifications: content grouping must collapse them. + final Path file = TEST_CASE_DIRECTORY.resolve( + "../../../../../key.ui/examples/heap/vstte10_01_SumAndMax/SumAndMax_sumAndMax.key"); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = transparentClosedSumAndMax(env); + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + + final Map> byGenerator = registry.getLemmasByGenerator(); + assertEquals(1, byGenerator.size(), "all lemmas come from the OSS generator"); + final List ossLemmas = + byGenerator.get(OssLemmaGenerator.INSTANCE.name()); + assertNotNull(ossLemmas); + assertEquals(registry.getLemmas().size(), ossLemmas.size()); + + final Map> byContent = + GeneratedLemmaRegistry.groupByContent(ossLemmas); + assertTrue(byContent.size() < ossLemmas.size(), + "content grouping should collapse duplicate simplifications (" + ossLemmas.size() + + " lemmas, " + byContent.size() + " distinct)"); + assertEquals(ossLemmas.size(), + byContent.values().stream().mapToInt(List::size).sum()); + } + } + + @Test + public void testBatchProvingDischargesAndSaves() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = transparentClosed(env, 20000); + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + final List lemmas = + registry.getMissingLemmas(); + assertFalse(lemmas.isEmpty()); + + final Path saveDir = + Files.createDirectories(Path.of("build", "tmp", "lemmaProverTest")); + final LemmaProver.Result result = LemmaProver.proveAll(lemmas, 10000, saveDir); + + assertEquals(lemmas.size(), result.proven().size() + result.remaining().size()); + assertFalse(result.proven().isEmpty(), "arith lemmas should close"); + for (final GeneratedLemma lemma : result.proven()) { + assertTrue(lemma.isProven()); + assertSame(proof.getEnv(), lemma.getSoundnessProofIfPresent().getEnv(), + "obligation proof lives in the main proof's environment"); + } + assertTrue(registry.getMissingLemmas().size() <= result.remaining().size(), + "proven lemmas leave the missing set"); + assertEquals(lemmas.size(), result.saved(), "each obligation proof was saved"); + for (final GeneratedLemma lemma : lemmas) { + final Path expected = saveDir.resolve( + de.uka.ilkd.key.util.MiscTools.toValidFileName( + lemma.taclet().name().toString()) + ".proof"); + assertTrue(Files.exists(expected), "missing saved proof: " + expected); + Files.deleteIfExists(expected); + } + } + } +} diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java new file mode 100644 index 00000000000..de597672383 --- /dev/null +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java @@ -0,0 +1,308 @@ +/* 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.gui; + +import java.awt.BorderLayout; +import java.awt.FlowLayout; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import javax.swing.*; +import javax.swing.border.TitledBorder; +import javax.swing.tree.DefaultMutableTreeNode; +import javax.swing.tree.DefaultTreeModel; +import javax.swing.tree.TreePath; +import javax.swing.tree.TreeSelectionModel; + +import de.uka.ilkd.key.core.KeYMediator; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.lemma.GeneratedLemma; +import de.uka.ilkd.key.rule.lemma.GeneratedLemmaRegistry; +import de.uka.ilkd.key.rule.lemma.LemmaProver; + +import org.key_project.logic.Name; + +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Shows, for a single proof, the lemmas it depends on that were introduced by + * {@link de.uka.ilkd.key.rule.lemma.LemmaTacletGenerator lemma generators} (see the transparent + * mode of the One Step Simplifier). The lemmas are grouped by generator and, within a generator, + * collapsed by content, so that the many introduction-point-distinct instances of the same + * simplification appear as one row with a count. + * + *

    + * From here the user can load selected lemmas as side proofs (into the same proof environment, + * where they become visible and provable) or batch-discharge all lemmas of a generator: each + * obligation is proved with a step bound, those that do not close are kept for manual work, and + * the obligation proofs can optionally be saved. + */ +public final class LemmaDependencyPanel extends JPanel { + + private static final long serialVersionUID = 1L; + private static final Logger LOGGER = LoggerFactory.getLogger(LemmaDependencyPanel.class); + + private static final int DEFAULT_MAX_STEPS = 10000; + private static final int MAX_LABEL_LENGTH = 90; + + private final KeYMediator mediator; + private final DefaultMutableTreeNode root = new DefaultMutableTreeNode(); + private final DefaultTreeModel treeModel = new DefaultTreeModel(root); + private final JTree tree = new JTree(treeModel); + private final JButton loadButton = new JButton("Load selected as side proofs"); + private final JButton proveAllButton = new JButton("Prove all lemmas..."); + private @Nullable Proof proof; + private @Nullable Runnable onLoaded; + + public LemmaDependencyPanel(KeYMediator mediator) { + super(new BorderLayout()); + this.mediator = mediator; + + tree.setRootVisible(false); + tree.setShowsRootHandles(true); + tree.getSelectionModel() + .setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); + tree.addTreeSelectionListener(e -> updateButtons()); + + final JScrollPane scrollPane = new JScrollPane(tree); + scrollPane.setBorder(new TitledBorder("Lemmas this proof depends on")); + add(scrollPane, BorderLayout.CENTER); + + loadButton.addActionListener(e -> loadSelected()); + proveAllButton.addActionListener(e -> proveAll()); + final JPanel buttons = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5)); + buttons.add(proveAllButton); + buttons.add(loadButton); + add(buttons, BorderLayout.SOUTH); + + updateButtons(); + } + + /** + * Sets an action to run after lemmas have been loaded as side proofs (e.g. to close the + * enclosing dialog). + */ + public void setOnLoaded(Runnable onLoaded) { + this.onLoaded = onLoaded; + } + + /** + * Shows the lemma dependencies of the given proof (clears the view for {@code null} or a + * proof without generated lemmas). + */ + public void setProof(@Nullable Proof proof) { + this.proof = proof; + refresh(); + } + + private void refresh() { + root.removeAllChildren(); + if (proof != null) { + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.getIfPresent(proof); + if (registry != null) { + for (final Map.Entry> gen : registry + .getLemmasByGenerator().entrySet()) { + final DefaultMutableTreeNode genNode = + new DefaultMutableTreeNode(new GeneratorRow(gen.getKey(), gen.getValue())); + for (final List group : GeneratedLemmaRegistry + .groupByContent(gen.getValue()).values()) { + genNode.add(new DefaultMutableTreeNode(new ContentRow(group))); + } + root.add(genNode); + } + } + } + treeModel.reload(); + for (int i = 0; i < tree.getRowCount(); i++) { + tree.expandRow(i); + } + updateButtons(); + } + + private void updateButtons() { + final boolean hasLemmas = root.getChildCount() > 0; + proveAllButton.setEnabled(hasLemmas); + loadButton.setEnabled(!selectedLemmas().isEmpty()); + } + + /** Collects the lemmas of the selected tree nodes (generator or content rows). */ + private List selectedLemmas() { + final Set result = new LinkedHashSet<>(); + final TreePath[] paths = tree.getSelectionPaths(); + if (paths != null) { + for (final TreePath path : paths) { + final Object node = + ((DefaultMutableTreeNode) path.getLastPathComponent()).getUserObject(); + if (node instanceof GeneratorRow row) { + result.addAll(row.lemmas); + } else if (node instanceof ContentRow row) { + result.addAll(row.lemmas); + } + } + } + return new ArrayList<>(result); + } + + private List allLemmas() { + final List all = new ArrayList<>(); + for (int i = 0; i < root.getChildCount(); i++) { + final Object row = + ((DefaultMutableTreeNode) root.getChildAt(i)).getUserObject(); + if (row instanceof GeneratorRow gen) { + all.addAll(gen.lemmas); + } + } + return all; + } + + private void loadSelected() { + final List selected = selectedLemmas(); + if (selected.isEmpty()) { + return; + } + Proof first = null; + for (final GeneratedLemma lemma : selected) { + // creating the obligation registers it in the environment; the user interface picks + // it up via the environment listener, so no explicit UI registration here + final Proof po = lemma.getOrCreateSoundnessProof(); + if (first == null) { + first = po; + } + } + if (first != null) { + mediator.getSelectionModel().setSelectedProof(first); + } + if (onLoaded != null) { + onLoaded.run(); + } + } + + private void proveAll() { + final List lemmas = allLemmas(); + if (lemmas.isEmpty()) { + return; + } + final ProveAllOptions options = ProveAllOptions.ask(this, lemmas.size()); + if (options == null) { + return; + } + + proveAllButton.setEnabled(false); + loadButton.setEnabled(false); + mediator.stopInterface(true); + new SwingWorker() { + @Override + protected LemmaProver.Result doInBackground() throws Exception { + return LemmaProver.proveAll(lemmas, options.maxSteps(), options.saveDir()); + } + + @Override + protected void done() { + mediator.startInterface(true); + try { + final LemmaProver.Result result = get(); + JOptionPane.showMessageDialog(LemmaDependencyPanel.this, + result.proven().size() + " of " + lemmas.size() + + " lemma(s) proved, " + result.remaining().size() + + " left open" + + (options.saveDir() != null + ? ", " + result.saved() + " proof(s) saved to " + + options.saveDir() + : "") + + ".", + "Prove Lemmas", JOptionPane.INFORMATION_MESSAGE); + } catch (Exception e) { + LOGGER.error("batch proving of lemmas failed", e); + JOptionPane.showMessageDialog(LemmaDependencyPanel.this, + "Batch proving failed:\n" + e.getMessage(), "Prove Lemmas", + JOptionPane.ERROR_MESSAGE); + } + refresh(); + } + }.execute(); + } + + private static String truncate(String s) { + final String oneLine = s.replace('\n', ' '); + return oneLine.length() <= MAX_LABEL_LENGTH ? oneLine + : oneLine.substring(0, MAX_LABEL_LENGTH - 1) + "…"; + } + + private static String status(List lemmas) { + int proven = 0; + int created = 0; + for (final GeneratedLemma lemma : lemmas) { + if (lemma.isProven()) { + proven++; + } else if (lemma.isSoundnessProofPresent()) { + created++; + } + } + final StringBuilder sb = new StringBuilder(); + sb.append(proven).append('/').append(lemmas.size()).append(" proven"); + if (created > 0) { + sb.append(", ").append(created).append(" open"); + } + return sb.toString(); + } + + /** A generator grouping node. */ + private record GeneratorRow(Name generator, List lemmas) { + @Override + public String toString() { + final int distinct = GeneratedLemmaRegistry.groupByContent(lemmas).size(); + return generator + " — " + lemmas.size() + " lemma(s), " + distinct + + " distinct (" + status(lemmas) + ")"; + } + } + + /** A content-collapsed group of lemmas denoting the same simplification. */ + private record ContentRow(List lemmas) { + @Override + public String toString() { + final String label = truncate(lemmas.get(0).contentKey()); + return (lemmas.size() > 1 ? "×" + lemmas.size() + " " : "") + label + + " [" + status(lemmas) + "]"; + } + } + + /** Options gathered from the user before a batch proving run. */ + private record ProveAllOptions(int maxSteps, @Nullable Path saveDir) { + static @Nullable ProveAllOptions ask(java.awt.Component parent, int count) { + final JSpinner steps = new JSpinner( + new SpinnerNumberModel(DEFAULT_MAX_STEPS, 100, 1_000_000, 1000)); + final JCheckBox save = new JCheckBox("Save obligation proofs to a directory"); + final JPanel panel = new JPanel(new java.awt.GridLayout(0, 1, 4, 4)); + panel.add(new JLabel("Prove all " + count + + " lemma(s); lemmas not closing within the step bound stay open.")); + final JPanel stepsPanel = new JPanel(new FlowLayout(FlowLayout.LEFT, 5, 0)); + stepsPanel.add(new JLabel("Max. automatic steps per lemma:")); + stepsPanel.add(steps); + panel.add(stepsPanel); + panel.add(save); + + final int choice = JOptionPane.showConfirmDialog(parent, panel, "Prove Lemmas", + JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE); + if (choice != JOptionPane.OK_OPTION) { + return null; + } + Path dir = null; + if (save.isSelected()) { + final JFileChooser chooser = new JFileChooser(); + chooser.setFileSelectionMode(JFileChooser.DIRECTORIES_ONLY); + chooser.setDialogTitle("Directory to save lemma proofs"); + if (chooser.showSaveDialog(parent) != JFileChooser.APPROVE_OPTION) { + return null; + } + dir = chooser.getSelectedFile().toPath(); + } + return new ProveAllOptions((Integer) steps.getValue(), dir); + } + } +} diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java deleted file mode 100644 index c029fa8fb89..00000000000 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/MissingLemmasPanel.java +++ /dev/null @@ -1,153 +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.gui; - -import java.awt.BorderLayout; -import java.awt.Component; -import java.awt.FlowLayout; -import java.util.List; -import javax.swing.*; -import javax.swing.border.TitledBorder; - -import de.uka.ilkd.key.core.KeYMediator; -import de.uka.ilkd.key.proof.Proof; -import de.uka.ilkd.key.proof.ProofAggregate; -import de.uka.ilkd.key.rule.lemma.GeneratedLemma; -import de.uka.ilkd.key.rule.lemma.GeneratedLemmaRegistry; - -import org.jspecify.annotations.Nullable; - -/** - * Panel listing the generated lemmas that a proof still depends on but whose soundness has not - * yet been established (see {@link GeneratedLemmaRegistry#getMissingLemmas()}). The user can - * select individual entries or all of them and load their soundness proof obligations as side - * proofs into the same proof environment, where they become visible and provable in the KeY GUI. - * - * @see de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule - */ -public final class MissingLemmasPanel extends JPanel { - - private static final long serialVersionUID = 1L; - - private final KeYMediator mediator; - private final DefaultListModel model = new DefaultListModel<>(); - private final JList lemmaList = new JList<>(model); - private final JButton proveButton = new JButton("Load selected as side proofs"); - private final JButton selectAllButton = new JButton("Select all"); - private @Nullable Proof proof; - private @Nullable Runnable onLoaded; - - public MissingLemmasPanel(KeYMediator mediator) { - super(new BorderLayout()); - this.mediator = mediator; - - lemmaList.setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION); - lemmaList.setCellRenderer(new LemmaCellRenderer()); - lemmaList.addListSelectionListener(e -> updateButtons()); - - final JScrollPane scrollPane = new JScrollPane(lemmaList); - scrollPane - .setBorder(new TitledBorder("Generated lemmas without a (closed) soundness proof")); - add(scrollPane, BorderLayout.CENTER); - - selectAllButton.addActionListener(e -> { - if (model.getSize() > 0) { - lemmaList.setSelectionInterval(0, model.getSize() - 1); - } - }); - proveButton.addActionListener(e -> loadSelected()); - - final JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 5, 5)); - buttonPanel.add(selectAllButton); - buttonPanel.add(proveButton); - add(buttonPanel, BorderLayout.SOUTH); - - updateButtons(); - } - - /** - * Shows the missing lemmas of the given proof (or clears the list if the proof is - * {@code null} or has generated no lemmas). - * - * @param proof the proof whose missing lemmas to display - */ - public void setProof(Proof proof) { - this.proof = proof; - refresh(); - } - - /** - * Sets an action to run after lemmas have been loaded as side proofs (e.g. to close the - * enclosing dialog). - * - * @param onLoaded the action, or {@code null} for none - */ - public void setOnLoaded(Runnable onLoaded) { - this.onLoaded = onLoaded; - } - - private void refresh() { - model.clear(); - if (proof != null) { - final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.getIfPresent(proof); - if (registry != null) { - for (final GeneratedLemma lemma : registry.getMissingLemmas()) { - model.addElement(lemma); - } - } - } - updateButtons(); - } - - private void updateButtons() { - selectAllButton.setEnabled(model.getSize() > 0); - proveButton.setEnabled(!lemmaList.getSelectedValuesList().isEmpty()); - } - - private void loadSelected() { - final List selected = lemmaList.getSelectedValuesList(); - if (selected.isEmpty()) { - return; - } - Proof firstLoaded = null; - for (final GeneratedLemma lemma : selected) { - // creating the soundness proof registers it in the proof environment; the user - // interface listens on the environment and adds it to the task tree, so the panel - // must not register it a second time (that produced duplicate task-tree entries) - final ProofAggregate aggregate = lemma.getOrCreateSoundnessProofAggregate(); - if (firstLoaded == null) { - firstLoaded = aggregate.getFirstProof(); - } - } - if (firstLoaded != null) { - mediator.getSelectionModel().setSelectedProof(firstLoaded); - } - if (onLoaded != null) { - onLoaded.run(); - } - } - - private static final class LemmaCellRenderer extends DefaultListCellRenderer { - private static final long serialVersionUID = 1L; - - @Override - public Component getListCellRendererComponent(JList list, Object value, int index, - boolean isSelected, boolean cellHasFocus) { - final Component result = super.getListCellRendererComponent(list, value, index, - isSelected, cellHasFocus); - if (value instanceof GeneratedLemma lemma && result instanceof JLabel label) { - final String status; - if (!lemma.isSoundnessProofPresent()) { - status = "soundness proof obligation not yet created"; - } else if (lemma.isProven()) { - status = "proven"; - } else { - status = "soundness proof open"; - } - label.setText(lemma.taclet().name() + " (" + status + ")"); - } - return result; - } - } -} diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java index 76e18eca087..ab7bd04ee87 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java @@ -70,7 +70,7 @@ public final class ProofManagementDialog extends JDialog { private JList proofList; private ContractSelectionPanel contractPanelByMethod; private ContractSelectionPanel contractPanelByProof; - private MissingLemmasPanel missingLemmasPanel; + private LemmaDependencyPanel lemmaDependencyPanel; private JButton startButton; private JButton cancelButton; private final KeYMediator mediator; @@ -145,7 +145,7 @@ public Component getListCellRendererComponent(JList list, Object value, int i }); proofList.addListSelectionListener(e -> { updateContractPanel(); - updateMissingLemmasPanel(); + updateLemmaDependencyPanel(); }); // create method list panel, scroll pane @@ -192,24 +192,26 @@ public void mouseClicked(MouseEvent e) { } }); contractPanelByProof.addListSelectionListener(e -> updateStartButton()); - listPanelByProof.add(contractPanelByProof); - // create missing-lemmas panel (shows generated lemmas of the selected proof whose - // soundness has not yet been established, and lets the user load them as side proofs) - missingLemmasPanel = new MissingLemmasPanel(mediator); - missingLemmasPanel.setOnLoaded(() -> setVisible(false)); + // the right-hand side of the "By Proof" tab shows, for the selected proof, both the + // contracts and the generated lemmas it depends on + lemmaDependencyPanel = new LemmaDependencyPanel(mediator); + lemmaDependencyPanel.setOnLoaded(() -> setVisible(false)); + JTabbedPane byProofDetails = new JTabbedPane(); + byProofDetails.addTab("Contracts", contractPanelByProof); + byProofDetails.addTab("Lemmas", lemmaDependencyPanel); + listPanelByProof.add(byProofDetails); // create tabbed pane tabbedPane = new JTabbedPane(); tabbedPane.addTab("By Target", listPanelByMethod); tabbedPane.addTab("By Proof", listPanelByProof); - tabbedPane.addTab("Missing Lemmas", missingLemmasPanel); tabbedPane.addChangeListener(e -> { updateStartButton(); if (proofList.getSelectedIndex() == -1 && proofList.getModel().getSize() > 0) { proofList.setSelectedIndex(0); } - updateMissingLemmasPanel(); + updateLemmaDependencyPanel(); }); getContentPane().add(tabbedPane); @@ -261,7 +263,7 @@ public void dispose() { classTree = null; contractPanelByMethod = null; contractPanelByProof = null; - missingLemmasPanel = null; + lemmaDependencyPanel = null; startButton = null; cancelButton = null; // ============================================ @@ -573,8 +575,8 @@ private void updateContractPanel() { updateStartButton(); } - private void updateMissingLemmasPanel() { - if (missingLemmasPanel == null) { + private void updateLemmaDependencyPanel() { + if (lemmaDependencyPanel == null) { return; } final ProofWrapper selected = proofList.getSelectedValue(); @@ -582,7 +584,7 @@ private void updateMissingLemmasPanel() { if (proof == null) { proof = mediator.getSelectedProof(); } - missingLemmasPanel.setProof(proof); + lemmaDependencyPanel.setProof(proof); } private void updateGlobalStatus() { From 63a1efda893c3cc29c5cefd5f65d15f17b2c3933 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 17:29:23 +0200 Subject: [PATCH 12/20] Fix lemma soundness PO generation for quantifiers; disable OSS in obligations Two soundness-relevant fixes to the generated-lemma proof obligations. (1) DefaultLemmaGenerator dropped concrete bound variables. Its rebuild() kept a quantifier's bound variable only when it was a schema variable (VariableSV); hand-written taclets only ever bind schema variables, so this was never exercised. Lemmas generated from concrete proof formulas, however, contain concrete quantifiers (\forall i; ... over a LogicVariable), and those bound variables were dropped, so a quantified subterm was rebuilt with an empty bound-variable list and failed the term arity check - a TermCreationException during obligation generation (not a rule soundness problem and not automation, as first suspected). rebuild() now passes concrete bound variables through unchanged. The change is inert for schema-variable taclets; ProveRulesTest (203 standard taclet proofs) still passes, and SumAndMax's quantifier lemmas now both generate and close (regression test testQuantifierLemmaObligationsGenerateAndClose). (2) The obligation is now proved with the one step simplifier switched off entirely, not merely downgraded from transparent to opaque. Opaque OSS still performs the same aggregated simplification in one hidden step, which would close a lemma's obligation by exactly the transformation under scrutiny. With OSS off, soundness is established from the individual base-calculus rewrite rules the lemma aggregates. Created with AI tooling support --- .../ilkd/key/rule/lemma/GeneratedLemma.java | 27 ++++++----- .../lemma/DefaultLemmaGenerator.java | 5 +++ .../lemma/TestLemmaProverAndGrouping.java | 45 +++++++++++++++++++ .../lemma/TestLemmaSoundnessNonCircular.java | 8 ++-- 4 files changed, 68 insertions(+), 17 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java index e1b44f1af3a..2f071fc0bd5 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -154,12 +154,14 @@ public synchronized ProofAggregate getOrCreateSoundnessProofAggregate() { */ private ProofAggregate createSoundnessProof() { final InitConfig poConfig = mainProof.getInitConfig().deepCopy(); - // The soundness of a generated lemma must be established in the base calculus. If the - // main proof runs the one step simplifier in transparent mode, the copied configuration - // would too, and the lemma's proof obligation would be discharged by generating and - // applying further lemmas — re-doing the very simplification it is meant to justify - // (a circular, non-terminating justification). Force the opaque simplifier here. - forceOpaqueOneStepSimplification(poConfig); + // The soundness of a generated lemma must be established in the base calculus, using the + // individual rewrite rules the lemma aggregates. The one step simplifier is switched off + // entirely for the obligation: not only its transparent mode (which would discharge the + // obligation by generating and applying further lemmas — re-doing the very simplification + // it must justify, a circular and non-terminating justification), but also its opaque + // mode, which performs the same aggregated simplification in one hidden step and would + // therefore close the obligation by exactly the transformation under scrutiny. + disableOneStepSimplification(poConfig); final ImmutableSet tacletsToProve = DefaultImmutableSet.nil().add(taclet); final ProofAggregate po = new ProofObligationCreator().create(tacletsToProve, @@ -173,20 +175,17 @@ private ProofAggregate createSoundnessProof() { } /** - * Switches the one step simplifier of the given configuration to its opaque mode, so that a - * proof run in it does not itself generate lemmas (see {@link #createSoundnessProof()}). + * Switches the one step simplifier off in the given configuration, so that the soundness + * proof is conducted in the base calculus (see {@link #createSoundnessProof()}). */ - private static void forceOpaqueOneStepSimplification(InitConfig config) { + private static void disableOneStepSimplification(InitConfig config) { final ProofSettings settings = config.getSettings(); if (settings == null) { return; } final StrategyProperties sp = settings.getStrategySettings().getActiveStrategyProperties(); - if (StrategyProperties.OSS_TRANSPARENT - .equals(sp.getProperty(StrategyProperties.OSS_OPTIONS_KEY))) { - sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_ON); - settings.getStrategySettings().setActiveStrategyProperties(sp); - } + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, StrategyProperties.OSS_OFF); + settings.getStrategySettings().setActiveStrategyProperties(sp); } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/taclettranslation/lemma/DefaultLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/taclettranslation/lemma/DefaultLemmaGenerator.java index f80d27928dc..1c502dcb1be 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/taclettranslation/lemma/DefaultLemmaGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/taclettranslation/lemma/DefaultLemmaGenerator.java @@ -270,6 +270,11 @@ private JTerm rebuild(Taclet taclet, JTerm term, TermServices services, qvars.add( (QuantifiableVariable) getInstantation(taclet, (VariableSV) qvar, services) .op()); + } else { + // A concrete (non-schematic) bound variable, as occurs in taclets generated from + // concrete proof formulas: keep it. Without this, a quantified subterm would be + // rebuilt with an empty bound-variable list and fail the arity check. + qvars.add(qvar); } } diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java index 26fd475b98a..8d123db7e85 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java @@ -63,6 +63,51 @@ private static Proof transparentClosed(KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = transparentClosedSumAndMax(env); + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + + int quantifierLemmas = 0; + for (final GeneratedLemma lemma : registry.getLemmas()) { + if (!containsQuantifier((de.uka.ilkd.key.logic.JTerm) lemma.taclet().find())) { + continue; + } + quantifierLemmas++; + // generation must not throw (the bug), and the obligation must close + final Proof po = lemma.getOrCreateSoundnessProof(); + new AutomaticProver().start(po, 12000, 120000); + assertTrue(po.closed(), + "quantifier lemma obligation did not close: " + lemma.taclet().name()); + if (quantifierLemmas >= 3) { + break; // a few are enough for a regression guard + } + } + assertTrue(quantifierLemmas > 0, + "SumAndMax should generate lemmas containing quantifiers"); + } + } + + private static boolean containsQuantifier(de.uka.ilkd.key.logic.JTerm t) { + if (t.op() instanceof de.uka.ilkd.key.logic.op.Quantifier) { + return true; + } + for (int i = 0; i < t.arity(); i++) { + if (containsQuantifier(t.sub(i))) { + return true; + } + } + return false; + } + @Test public void testContentGroupingCollapsesDuplicates() throws Exception { // SumAndMax generates many introduction-point-distinct lemmas that denote the same diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java index 9af49a14db8..5f3e8640af2 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaSoundnessNonCircular.java @@ -55,12 +55,14 @@ public void testSoundnessProofUsesBaseCalculus() throws Exception { for (final GeneratedLemma lemma : registry.getMissingLemmas()) { final Proof po = lemma.getOrCreateSoundnessProof(); - // the soundness proof runs the opaque simplifier, not the transparent one - assertEquals(StrategyProperties.OSS_ON, + // the soundness proof runs in the base calculus with the one step simplifier + // switched off entirely (not even its opaque mode, which would close the + // obligation by the very aggregated simplification under scrutiny) + assertEquals(StrategyProperties.OSS_OFF, po.getSettings().getStrategySettings().getActiveStrategyProperties() .getProperty(StrategyProperties.OSS_OPTIONS_KEY), "the soundness proof of " + lemma.taclet().name() - + " must run in the base calculus (opaque OSS)"); + + " must run with the one step simplifier disabled"); new AutomaticProver().start(po, 10000, 60000); assertTrue(po.closed(), From c8a20166effdd2f66ab3934cfdfc146445907e8d Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 17:44:11 +0200 Subject: [PATCH 13/20] Fix transparent-mode failures on contract PO proofs (BinarySearch) Four defects found by GUI testing on BinarySearch (contract PO, transparent mode), each with a distinct root cause: (1) BoundUniquenessChecker rejected generated lemmas whose find and assumes clauses share bound variables ('A bound SchemaVariable variables occurs both in assumes and find clauses'). The restriction exists - per the checker's own documentation - for schematic bound variables, which would have to match the identical quantified variable everywhere so that the taclet would almost never apply. Taclets generated from proof formulas legitimately share concrete LogicVariable binders between assumes and find (both stem from the same sequent). The check now applies to schema variables only. This is the third schematic-only assumption uncovered by generated concrete taclets (after DefaultLemmaGenerator's binder handling and the naming/aliasing issue). (2) The same exception, thrown out of lemma generation during automated search, aborted the strategy mid-run ('stops after 95 steps'). OssLemmaGenerator.generate now converts any taclet-construction failure into a veto of the formula plus a clean RuleAbortException; automated search can no longer be halted by a generation failure. (3) InitConfig.deepCopy copied proof-local justifications. Entries that reference nodes of the source proof (addrule taclets such as replaceKnownSelect, generated lemmas) collided with rule re-introductions when a proof is elaborated or replayed onto a copied configuration ('A rule named ... has already been registered') - a pre-existing hazard for stock addrule taclets as well. RuleJustification gains isProofLocal() (true for RuleJustificationByAddRules and GeneratedLemmaJustification), and RuleJustificationInfo.copy() skips proof-local entries. The lemma registry additionally replaces foreign same-name entries defensively. (4) TransparentProofSaver produced unloadable files for proofs of generated proof obligations: PO-created program variables are not part of the problem header, and without the \proofObligation section the saved file cannot re-create them. The elaborated proof is now registered with the original's ProofOblInput, so saving emits the obligation section and the file loads. Regression test TestTransparentModeBinarySearch covers the full workflow: transparent automode closes, the transparent form saves and replays to closed including the lemma introductions, and every lemma soundness obligation closes in the base calculus. Full key.core suite passes. Created with AI tooling support --- .../ilkd/key/proof/mgt/RuleJustification.java | 14 +++ .../mgt/RuleJustificationByAddRules.java | 6 ++ .../key/proof/mgt/RuleJustificationInfo.java | 10 +- .../ilkd/key/rule/BoundUniquenessChecker.java | 9 ++ .../lemma/GeneratedLemmaJustification.java | 7 ++ .../rule/lemma/GeneratedLemmaRegistry.java | 10 ++ .../key/rule/lemma/OssLemmaGenerator.java | 13 ++- .../key/rule/lemma/TransparentProofSaver.java | 10 ++ .../TestTransparentModeBinarySearch.java | 101 ++++++++++++++++++ 9 files changed, 178 insertions(+), 2 deletions(-) create mode 100644 key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeBinarySearch.java diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustification.java b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustification.java index aa3f25fe863..32837b6f79f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustification.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustification.java @@ -6,4 +6,18 @@ public interface RuleJustification { boolean isAxiomJustification(); + + /** + * Returns true iff this justification is local to the proof it was created in, e.g. because + * it refers to the proof node that introduced the justified rule. Proof-local justifications + * are not carried over when an initial configuration is copied for another proof: they would + * reference foreign proof nodes there, and the stale entries would collide with the rule + * (re-)introductions of the new proof (dynamically introduced rules such as {@code \addrule} + * taclets or generated lemmas register their justification when they are introduced). + * + * @return true iff this justification must not survive an initial-configuration copy + */ + default boolean isProofLocal() { + return false; + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationByAddRules.java b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationByAddRules.java index ac3c30f846b..81cfdd355f1 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationByAddRules.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationByAddRules.java @@ -21,6 +21,12 @@ public record RuleJustificationByAddRules(Node node, boolean isAxiom) implements @Override public boolean isAxiomJustification() { return isAxiom; } + @Override + public boolean isProofLocal() { + // refers to the node of its proof at which the rule was introduced + return true; + } + public RuleApp motherTaclet() { return node.getAppliedRuleApp(); } public String toString() { diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationInfo.java b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationInfo.java index 6008e87d1c3..697c93a012a 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationInfo.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/RuleJustificationInfo.java @@ -55,7 +55,15 @@ public void removeJustificationFor(Rule rule) { public RuleJustificationInfo copy() { RuleJustificationInfo info = new RuleJustificationInfo(); - info.rule2Justification.putAll(rule2Justification); + for (Map.Entry entry : rule2Justification.entrySet()) { + // proof-local justifications (dynamically introduced rules such as addrule taclets + // or generated lemmas) refer to nodes of the proof they were created in; carrying + // them into a copied configuration would leave stale entries there that collide + // with the rule (re-)introductions of the new proof + if (!entry.getValue().isProofLocal()) { + info.rule2Justification.put(entry.getKey(), entry.getValue()); + } + } return info; } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/BoundUniquenessChecker.java b/key.core/src/main/java/de/uka/ilkd/key/rule/BoundUniquenessChecker.java index 9329c7c1a17..f2242c610fc 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/BoundUniquenessChecker.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/BoundUniquenessChecker.java @@ -7,6 +7,7 @@ import java.util.LinkedHashSet; import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.VariableSV; import org.key_project.logic.SyntaxElement; import org.key_project.logic.op.QuantifiableVariable; @@ -75,6 +76,14 @@ private boolean correct(JTerm t) { for (int i = 0, ar = t.arity(); i < ar; i++) { for (int j = 0, sz = t.varsBoundHere(i).size(); j < sz; j++) { final QuantifiableVariable qv = t.varsBoundHere(i).get(j); + if (!(qv instanceof VariableSV)) { + // the uniqueness restriction is about schema variables (see class comment): + // a doubly bound schema variable would have to match the same quantified + // variable everywhere and the taclet would almost never apply. Concrete + // bound variables, as they occur in taclets generated from proof formulas, + // may legitimately be bound several times (they stem from the same sequent). + continue; + } if (boundVars.contains(qv)) { return false; } else { diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java index 64912acc333..5ea67fc06fe 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaJustification.java @@ -32,6 +32,13 @@ public boolean isAxiomJustification() { return false; } + @Override + public boolean isProofLocal() { + // tied to the introducing proof: the lemma and its soundness proof live there, and the + // justification is (re-)registered whenever the lemma is introduced + return true; + } + /** * returns the name of the generator that created the justified taclet */ diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java index fd97c3a5962..ae4b43a5544 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java @@ -119,6 +119,16 @@ public synchronized GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, } final GeneratedLemma lemma = new GeneratedLemma(taclet, proof, generator.name()); + // The registry owns the generated-lemma namespace of its proof. A justification entry + // for this name that the registry does not know stems from a copied initial + // configuration (InitConfig.deepCopy copies the justification map): e.g. when a proof + // run in transparent mode is elaborated onto a fresh proof for transparent saving, the + // fresh configuration inherits the original run's lemma justifications, and the + // elaboration deliberately regenerates the same (replay-stable) names. Such foreign + // entries are replaced by this proof's own. + if (proof.getInitConfig().getJustifInfo().getJustification(taclet) != null) { + proof.getInitConfig().getJustifInfo().removeJustificationFor(taclet); + } proof.getInitConfig().registerRule(taclet, lemma.justification()); lemmas.put(taclet.name(), lemma); return lemma; diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java index 71bd93aa7dd..8206f5b851e 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java @@ -150,7 +150,18 @@ public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { tb.setApplicationRestriction( new ApplicationRestriction(ApplicationRestriction.IN_SEQUENT_STATE)); } - return tb.getTaclet(); + try { + return tb.getTaclet(); + } catch (RuleAbortException e) { + throw e; + } catch (RuntimeException e) { + // taclet construction can be rejected for reasons rooted in taclet wellformedness + // checks; a failure must never abort automated proof search, so the formula is + // vetoed (no further introduction attempts) and the application aborted cleanly + GeneratedLemmaRegistry.get(goal.proof()).veto(pio.sequentFormula()); + throw new RuleAbortException( + "generating a lemma taclet for " + find + " failed: " + e.getMessage()); + } } /** diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java index 8c736334055..1dc8318968b 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/TransparentProofSaver.java @@ -7,6 +7,7 @@ import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.init.InitConfig; +import de.uka.ilkd.key.proof.init.ProofOblInput; import de.uka.ilkd.key.proof.io.ProofSaver; import de.uka.ilkd.key.rule.OneStepSimplifier; import de.uka.ilkd.key.strategy.StrategyProperties; @@ -53,6 +54,15 @@ public static Result save(Proof proof, Path file) throws Exception { final Proof target = new Proof(proof.name().toString(), proof.root().sequent(), proof.header(), config.createTacletIndex(), config.createBuiltInRuleIndex(), config); try { + // proofs for generated proof obligations (e.g. contract POs) reference symbols that + // are not part of the problem header but are re-created from the proof obligation on + // loading; associate the elaborated proof with the same obligation so that saving + // emits the \proofObligation section and the saved file loads + final ProofOblInput po = + proof.getServices().getSpecificationRepository().getProofOblInput(proof); + if (po != null) { + target.getServices().getSpecificationRepository().registerProof(po, target); + } // the lemma generator needs an active simplifier on the target proof final StrategyProperties sp = target.getSettings().getStrategySettings().getActiveStrategyProperties(); diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeBinarySearch.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeBinarySearch.java new file mode 100644 index 00000000000..d66b3afd2d4 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeBinarySearch.java @@ -0,0 +1,101 @@ +/* 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.lemma; + +import java.nio.file.Files; +import java.nio.file.Path; + +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.taclettranslation.lemma.AutomaticProver; + +import org.key_project.util.helper.FindResources; + +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Regression test for the BinarySearch failures in transparent mode: lemmas whose find and + * assumes clauses share concrete bound variables (both stem from the same sequent) were rejected + * by the taclet builder's bound-uniqueness check — a restriction that only makes sense for + * schematic bound variables. That failure aborted automated search mid-run and made saving the + * transparent proof impossible. Now: transparent automode closes the proof, the transparent form + * saves and replays, and the lemma soundness obligations (including those with shared binders and + * assumptions) are generated and discharged. + */ +public class TestTransparentModeBinarySearch { + + private static final Path TEST_CASE_DIRECTORY = FindResources.getTestCasesDirectory(); + + @Test + public void testTransparentBinarySearchEndToEnd() throws Exception { + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/firstTouch/06-BinarySearch/project.key"); + assertTrue(Files.exists(file)); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = env.getLoadedProof(); + assertNotNull(proof); + final StrategyProperties sp = + proof.getSettings().getStrategySettings().getActiveStrategyProperties(); + sp.setProperty(StrategyProperties.OSS_OPTIONS_KEY, + StrategyProperties.OSS_TRANSPARENT); + proof.getSettings().getStrategySettings().setActiveStrategyProperties(sp); + proof.setActiveStrategy(proof.getServices().getProfile() + .getDefaultStrategyFactory().create(proof, sp)); + OneStepSimplifier.refreshOSS(proof); + + new AutomaticProver().start(proof, 30000, 300000); + assertTrue(proof.closed(), + "transparent automode did not close BinarySearch (previously aborted by the" + + " bound-uniqueness check)"); + + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + assertFalse(registry.getLemmas().isEmpty()); + + // the transparent form saves (previously failed with a TacletBuilderException) + // and replays to closed + final Path tmpDir = Files.createDirectories(Path.of("build", "tmp", "lemmaTests")); + final Path outFile = + Files.createTempFile(tmpDir, "binarySearchTransparent", ".proof"); + try { + // the proof is already transparent, so nothing is left to elaborate; saving is + // a faithful copy whose replay re-introduces the lemmas (this used to fail with + // the taclet builder's bound-uniqueness check) + TransparentProofSaver.save(proof, outFile); + try (KeYEnvironment reloadedEnv = + KeYEnvironment.load(outFile)) { + final Proof reloaded = reloadedEnv.getLoadedProof(); + assertNotNull(reloaded); + assertTrue(reloaded.closed(), + "saved transparent BinarySearch proof did not replay to closed"); + int intros = 0; + final var nodes = reloaded.root().subtreeIterator(); + while (nodes.hasNext()) { + final var app = nodes.next().getAppliedRuleApp(); + if (app != null && app.rule() instanceof OssLemmaIntroductionRule) { + intros++; + } + } + assertTrue(intros > 0, + "reloaded transparent proof should contain lemma introductions"); + } + } finally { + Files.deleteIfExists(outFile); + } + + // all lemma soundness obligations generate; count how many close in the base + // calculus within the bound (the batch must be robust and must not throw) + final LemmaProver.Result result = + LemmaProver.proveAll(registry.getLemmas(), 10000, null); + assertTrue(result.remaining().isEmpty(), + "lemma obligations did not close: " + result.remaining().stream() + .map(l -> l.taclet().name().toString()).toList()); + } + } +} From 109802baf75bc13bb054133266b8c216d710ac3c Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 18:29:55 +0200 Subject: [PATCH 14/20] Transparent mode: no opaque OSS at all; ordinary rules handle the rest Replaces the hybrid transparent mode (opaque OSS on modality formulas) by a fully transparent one, following the observation that the opaque residue was almost exclusively update-prefix simplification in front of modalities (probe on SumAndMax: 38 of 39 opaque applications on update-application formulas; ~460 of ~480 aggregated micro steps from the update rule sets). In transparent mode the one step simplifier now - is applicable nowhere (previously it kept handling modality formulas), and - no longer takes the captured rule set taclets out of the goals' rule indices (appsTakenOver stays empty); its internal indices still serve as the computation core of the lemma generator. Formulas outside the lemma fragment are thus simplified by the ordinary strategy in individual, fully visible steps - transparent proofs contain no opaque aggregation of any kind. (For contrast: an opaque OSS step persists nothing about the rules it aggregated - only position and used context formulas are saved, and replay re-runs the whole fixpoint trusting that the current rule base reproduces the result.) Fixed in passing: refresh() short-circuited on mode flips for an unchanged proof - the loader's initial refresh (opaque, default settings) had already captured the taclets, and switching to transparent neither restored them nor rebuilt, leaving goals without simplification rules entirely (automode saturated after a few steps). refresh() now shuts down (restoring any taken-over taclets) before re-initializing on any state change. computation.key: 775 nodes with OSS off, 728 fully transparent (15 introductions, 10 lemma applications; a few lemmas are obsoleted by individual steps between introduction and application - scheduling to be tuned in a follow-up), 597 opaque. Old transparent proofs containing opaque modality-OSS steps no longer replay (pre-release format change). Created with AI tooling support --- .../uka/ilkd/key/rule/OneStepSimplifier.java | 42 ++++++++++++------- .../strategy/JavaCardDLStrategyFactory.java | 6 ++- .../key/rule/lemma/TestTransparentMode.java | 26 ++++++------ .../lemma/TestTransparentModeSumAndMax.java | 7 +--- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java index 450f9d54e46..e2977085be9 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java @@ -23,7 +23,6 @@ import de.uka.ilkd.key.proof.TacletIndexKit; import de.uka.ilkd.key.proof.calculus.JavaDLSequentKit; import de.uka.ilkd.key.rule.inst.SVInstantiations; -import de.uka.ilkd.key.rule.lemma.OssLemmaGenerator; import de.uka.ilkd.key.settings.ProofSettings; import de.uka.ilkd.key.strategy.StrategyProperties; import de.uka.ilkd.key.util.MiscTools; @@ -90,11 +89,14 @@ public static final class Protocol extends ArrayList { private Map[] notSimplifiableCaches; private boolean active; /** - * In transparent mode (strategy option {@link StrategyProperties#OSS_TRANSPARENT}), the - * simplifier machinery stays active but lemma-eligible formulas are not simplified by this - * rule; they are handled by - * {@link de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule} instead, so that the - * aggregated simplification becomes an inspectable, separately provable taclet. + * In transparent mode (strategy option {@link StrategyProperties#OSS_TRANSPARENT}), this + * rule itself is never applicable and no taclets are taken over from the goals' rule + * indices: lemma-eligible formulas are simplified via + * {@link de.uka.ilkd.key.rule.lemma.OssLemmaIntroductionRule} (an inspectable, separately + * provable taclet per aggregated simplification), and all other formulas are simplified by + * the ordinary strategy with the individual rules. The simplifier machinery stays active + * solely as the computation core of the lemma generator (see + * {@link #computeSimplification(Goal, PosInOccurrence, Protocol)}). */ private boolean transparent; @@ -158,7 +160,9 @@ private ImmutableList tacletsForRuleSet(Proof proof, String ruleSetName, } if (accept) { - appsTakenOver = appsTakenOver.prepend(app); + if (!transparent) { + appsTakenOver = appsTakenOver.prepend(app); + } result = result.prepend(tac); } } @@ -173,7 +177,10 @@ private ImmutableList tacletsForRuleSet(Proof proof, String ruleSetName, assert Immutables.isDuplicateFree(result) : "If this fails unexpectedly, add a call to Immutables.removeDuplicates."; - // remove apps in appsTakenOver from taclet indices of all goals + // Remove apps in appsTakenOver from taclet indices of all goals. In transparent mode + // nothing is taken over: the captured rule sets remain available to the ordinary + // strategy (formulas outside the lemma fragment are simplified by individual, visible + // rule applications), and the indices built here serve only the lemma generator. for (NoPosTacletApp app : appsTakenOver) { for (Goal goal : proof.allGoals()) { goal.ruleAppIndex().removeNoPosTacletApp(app); @@ -556,12 +563,15 @@ private synchronized void refresh(Proof proof) { if (active != newActive || transparent != newTransparent || lastProof != proof // The setting or proof has changed. || (isShutdown() && !proof.closed())) { // A closed proof was pruned. + // restore any taken-over taclets before re-initializing: switching between the + // opaque and the transparent mode changes whether the captured rule sets are + // removed from the goals' rule indices, and initIndices alone would not rebuild + // for an unchanged proof + shutdownIndices(); active = newActive; transparent = newTransparent; if (active && proof != null && !proof.closed()) { initIndices(proof); - } else { - shutdownIndices(); } } } @@ -615,13 +625,15 @@ public boolean canSimplify(Goal goal, PosInOccurrence pio) { @Override public boolean isApplicable(Goal goal, PosInOccurrence pio) { - if (!canSimplify(goal, pio)) { + // In transparent mode this rule performs no simplification at all: lemma-eligible + // formulas are handled by the lemma introduction rule, and all other formulas are + // simplified by the ordinary strategy with the individual rules (which stay in the + // goals' rule indices, see tacletsForRuleSet). The simplifier machinery remains active + // solely as the computation core of the lemma generator. + if (transparent) { return false; } - // in transparent mode, lemma-eligible formulas are handled by the lemma introduction - // rule; this rule keeps handling only the formulas outside the lemma fragment - return !transparent - || OssLemmaGenerator.containsModality(pio.sequentFormula().formula()); + return canSimplify(goal, pio); } @Override diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java index 9cb0d296059..bc5e45af693 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/JavaCardDLStrategyFactory.java @@ -39,8 +39,10 @@ public class JavaCardDLStrategyFactory implements StrategyFactory { + "operators is captured as a taclet that is introduced and
    " + "then applied, so the performed transformation is inspectable
    " + "and can be certified by a separate soundness proof.
    " - + "Formulas containing modal operators are still simplified
    " - + "by the ordinary OSS rule." + ""; + + "Formulas containing modal operators are simplified by the
    " + + "ordinary calculus rules in individual, visible steps; the
    " + + "opaque OSS rule is never applied. Proofs are fully
    " + + "transparent but larger and search is slower." + ""; public static final String TOOL_TIP_PROOF_SPLITTING_FREE = "" + "Split formulas (if-then-else expressions,
    " + "disjunctions in the antecedent, conjunctions in
    " diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java index 1c95b14b345..4a29b378942 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentMode.java @@ -86,20 +86,18 @@ public void testTransparentAutomode() throws Exception { final Counts counts = count(proof); assertTrue(counts.intro() > 0, "transparent mode should introduce lemma taclets during search"); - assertEquals(counts.intro(), counts.lemmaApps(), - "each introduced lemma is applied exactly once per introduction"); - // the opaque rule keeps handling only formulas with modal operators - final var nodes = proof.root().subtreeIterator(); - while (nodes.hasNext()) { - final Node node = nodes.next(); - if (node.getAppliedRuleApp() instanceof OneStepSimplifierRuleApp app) { - assertTrue( - OssLemmaGenerator.containsModality( - app.posInOccurrence().sequentFormula().formula()), - "opaque simplifier application on a lemma-eligible formula in " - + "transparent mode"); - } - } + // most introductions lead to an application; a few lemmas may be obsoleted by an + // individual rule step rewriting the formula between introduction and application + // (the individual rules compete with the lemma path since they remain available) + assertTrue(counts.lemmaApps() > 0); + assertTrue(counts.lemmaApps() <= counts.intro()); + assertTrue(counts.intro() - counts.lemmaApps() <= counts.intro() / 2, + "too many introduced lemmas were never applied (intro=" + counts.intro() + + ", applied=" + counts.lemmaApps() + ")"); + // the opaque rule is never applied in transparent mode: formulas outside the lemma + // fragment are simplified by the ordinary strategy in individual, visible steps + assertEquals(0, counts.oss(), + "no opaque simplifier application may occur in transparent mode"); // the transparent proof saves and replays final Path proofFile = Files.createTempFile("transparentMode", ".proof"); diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java index 7c00b4dba63..30a580e96e8 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestTransparentModeSumAndMax.java @@ -74,11 +74,8 @@ public void testTransparentAutomodeClosesSumAndMax() throws Exception { } if (app.rule() == OssLemmaIntroductionRule.INSTANCE) { intro++; - } else if (app instanceof OneStepSimplifierRuleApp ossApp) { - assertTrue( - OssLemmaGenerator.containsModality( - ossApp.posInOccurrence().sequentFormula().formula()), - "opaque simplifier application on a lemma-eligible formula"); + } else if (app instanceof OneStepSimplifierRuleApp) { + fail("no opaque simplifier application may occur in transparent mode"); } else if (app.rule().name().toString().startsWith("ossLemma_")) { lemmaApps++; final RewriteTaclet taclet = (RewriteTaclet) app.rule(); From ffc739e84e99eefe86f7788a2813afac3666f8c6 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 19:09:56 +0200 Subject: [PATCH 15/20] Transparent scheduling: lemma path wins the race; two determinism fixes Tunes the fully transparent mode so that aggregated lemmas, not individual rule applications, perform the simplification wherever the lemma path applies. Baseline measurements (computation/SumAndMax/BinarySearch) showed 23-35% of introductions wasted and 130-145 captured-rule-set singles firing on modality-free formulas. Scheduling: - Generated lemma taclets carry the new rule set 'generatedLemma' (declared in ruleSetsDeclarations.key), bound like 'concrete' (-11000 plus depth scaling), so a blanket treatment of the captured rule sets cannot penalize the lemma applications themselves. - In transparent mode, applications of the seven OSS-captured rule sets are penalized on formulas without executable code (focus-conditional feature) - exactly where the lemma path competes; symbolic-execution scheduling is untouched, and the rules remain applicable everywhere (completeness). - The penalty is deliberately small (+2000): it only breaks the cost tie with the lemma path. A large penalty inverts cost orderings that encode termination invariants of the strategy - observed as an unbounded bounded-sum descent, where the unrolling rule (enlarging, about -2000) outranked its penalized empty-range counterpart (concrete) forever. Determinism (found by elaborating a transparent proof onto a fresh copy): - The simplifier's captured-taclet indices were built in set-iteration order; when several rules match at one position, proof instances of the same problem could simplify differently. The taclets are now sorted by name before index construction. - The sub-origins of merged origin term labels are collected in object identity order, so the printed form of otherwise identical terms differs between proof instances. Lemma names and content-grouping keys are now computed from label-stripped terms (LemmaTacletGenerator.removeTermLabels); labels are proof metadata, and lemma identity is secured by the introduction-node id. Also: GeneratedLemma.aggregatedSteps() records how many base calculus steps a lemma aggregates (measurement and display metadata; the generator returns a GeneratedTaclet record). Results: wasted introductions 0 on all three examples (from 5/55/66), captured singles on modality-free formulas roughly halved (rest are legitimate last-resort applications), total aggregated coverage increased, node counts and closure unchanged. Full key.core suite passes. Created with AI tooling support --- .../uka/ilkd/key/rule/OneStepSimplifier.java | 10 ++- .../rule/lemma/AddLiteralsLemmaGenerator.java | 9 ++- .../ilkd/key/rule/lemma/GeneratedLemma.java | 29 +++++++-- .../rule/lemma/GeneratedLemmaRegistry.java | 6 +- .../key/rule/lemma/LemmaTacletGenerator.java | 38 ++++++++++- .../key/rule/lemma/OssLemmaGenerator.java | 28 +++++--- .../de/uka/ilkd/key/strategy/FOLStrategy.java | 64 +++++++++++++++++-- .../key/proof/rules/ruleSetsDeclarations.key | 3 + 8 files changed, 156 insertions(+), 31 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java index e2977085be9..f7481064dee 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java @@ -207,7 +207,15 @@ private void initIndices(Proof proof) { ImmutableList done = ImmutableList.nil(); for (String ruleSet : ruleSets) { ImmutableList taclets = tacletsForRuleSet(proof, ruleSet, done); - indices[i] = TacletIndexKit.getKit().createTacletIndex(taclets); + // Sort by name for a deterministic index: the taclets are collected in set + // iteration order, and when several rules match at the same position the + // simplifier applies the first one found. Without a canonical order, the + // aggregated simplification result may differ between proof instances of the + // same problem (e.g. between a proof and its replayed or elaborated copy). + final ArrayList sorted = new ArrayList<>(); + taclets.forEach(sorted::add); + sorted.sort(Comparator.comparing(t -> t.name().toString())); + indices[i] = TacletIndexKit.getKit().createTacletIndex(sorted); notSimplifiableCaches[i] = new LRUCache<>(DEFAULT_CACHE_SIZE); i++; done = done.prepend(ruleSet); diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java index 857700327d6..cfdcb9ec8eb 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/AddLiteralsLemmaGenerator.java @@ -60,7 +60,7 @@ public boolean isApplicable(Goal goal, PosInOccurrence pio) { } @Override - public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { + public GeneratedTaclet generate(Goal goal, PosInOccurrence pio) { final Services services = goal.proof().getServices(); final JTerm find = (JTerm) pio.subTerm(); @@ -77,7 +77,10 @@ public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { tb.setDisplayName(NAME.toString()); tb.setFind(find); tb.addGoalTerm(sum); - tb.addRuleSet(new RuleSet(new Name("concrete"))); - return tb.getTaclet(); + final RuleSet generatedLemmaRS = + services.getNamespaces().ruleSets().lookup(new Name("generatedLemma")); + tb.addRuleSet(generatedLemmaRS != null ? generatedLemmaRS + : new RuleSet(new Name("generatedLemma"))); + return new GeneratedTaclet(tb.getTaclet(), 1); } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java index 2f071fc0bd5..5b57b8f32a2 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -44,13 +44,24 @@ public final class GeneratedLemma { private final RewriteTaclet taclet; private final Proof mainProof; private final GeneratedLemmaJustification justification; + private final int aggregatedSteps; private @Nullable ProofAggregate soundnessProofAggregate; private @Nullable Proof soundnessProof; - GeneratedLemma(RewriteTaclet taclet, Proof mainProof, Name generatorName) { + GeneratedLemma(RewriteTaclet taclet, Proof mainProof, Name generatorName, + int aggregatedSteps) { this.taclet = taclet; this.mainProof = mainProof; this.justification = new GeneratedLemmaJustification(generatorName, this); + this.aggregatedSteps = aggregatedSteps; + } + + /** + * returns the number of base calculus steps the lemma aggregates (display and measurement + * metadata) + */ + public int aggregatedSteps() { + return aggregatedSteps; } /** @@ -87,19 +98,23 @@ public Name generatorName() { public String contentKey() { final Services services = mainProof.getServices(); final StringBuilder key = new StringBuilder(); - key.append(OutputStreamProofSaver.printTerm((JTerm) taclet.find(), services)); + key.append(OutputStreamProofSaver.printTerm( + LemmaTacletGenerator.removeTermLabels((JTerm) taclet.find(), services), services)); final Sequent assumes = taclet.assumesSequent(); for (final var sf : assumes.antecedent()) { - key.append("\n<= ") - .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + key.append("\n<= ").append(OutputStreamProofSaver.printTerm( + LemmaTacletGenerator.removeTermLabels((JTerm) sf.formula(), services), + services)); } for (final var sf : assumes.succedent()) { - key.append("\n=> ") - .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + key.append("\n=> ").append(OutputStreamProofSaver.printTerm( + LemmaTacletGenerator.removeTermLabels((JTerm) sf.formula(), services), + services)); } final JTerm rw = (JTerm) ((RewriteTacletGoalTemplate) taclet.goalTemplates().head()) .replaceWith(); - key.append("\n~> ").append(OutputStreamProofSaver.printTerm(rw, services)); + key.append("\n~> ").append(OutputStreamProofSaver.printTerm( + LemmaTacletGenerator.removeTermLabels(rw, services), services)); return key.toString(); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java index ae4b43a5544..6d2974d283d 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemmaRegistry.java @@ -95,7 +95,8 @@ public static GeneratedLemmaRegistry get(Proof proof) { public synchronized GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, LemmaTacletGenerator generator) { final Proof proof = goal.proof(); - final RewriteTaclet taclet = generator.generate(goal, pio); + final LemmaTacletGenerator.GeneratedTaclet generated = generator.generate(goal, pio); + final RewriteTaclet taclet = generated.taclet(); final GeneratedLemma existing = lemmas.get(taclet.name()); if (existing != null) { @@ -118,7 +119,8 @@ public synchronized GeneratedLemma getOrCreate(Goal goal, PosInOccurrence pio, return existing; } - final GeneratedLemma lemma = new GeneratedLemma(taclet, proof, generator.name()); + final GeneratedLemma lemma = + new GeneratedLemma(taclet, proof, generator.name(), generated.aggregatedSteps()); // The registry owns the generated-lemma namespace of its proof. A justification entry // for this name that the registry does not know stems from a copied initial // configuration (InitConfig.deepCopy copies the justification map): e.g. when a proof diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java index 35649897cff..5e7bf56de57 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaTacletGenerator.java @@ -58,13 +58,47 @@ public interface LemmaTacletGenerator { */ boolean isApplicable(Goal goal, PosInOccurrence pio); + /** + * A generated taclet together with the number of base calculus steps it aggregates (display + * and measurement metadata; 1 if the transformation corresponds to a single step). + */ + record GeneratedTaclet(RewriteTaclet taclet, int aggregatedSteps) { + } + + /** + * Returns the term with all term labels removed (recursively). Name and content-grouping + * computations must work on label-free terms: labels are proof metadata whose serialization + * is not canonical (e.g. the sub-origins of merged origin labels are collected in identity + * order), so label-sensitive names would differ between a proof and its replayed or + * elaborated copy. + * + * @param term the term + * @param services services for term construction + * @return the term without any labels + */ + static de.uka.ilkd.key.logic.JTerm removeTermLabels(de.uka.ilkd.key.logic.JTerm term, + de.uka.ilkd.key.java.Services services) { + final de.uka.ilkd.key.logic.JTerm[] subs = + new de.uka.ilkd.key.logic.JTerm[term.arity()]; + boolean changed = term.hasLabels(); + for (int i = 0; i < term.arity(); i++) { + subs[i] = removeTermLabels(term.sub(i), services); + changed |= subs[i] != term.sub(i); + } + if (!changed) { + return term; + } + return services.getTermFactory().createTerm(term.op(), subs, term.boundVars(), + null); + } + /** * computes the lemma taclet for the term at the given position. The result must be * deterministic in the content of the term at the position (see the class-level contract). * * @param goal the current goal * @param pio the position of the term to be transformed - * @return the generated taclet + * @return the generated taclet with its aggregation count */ - RewriteTaclet generate(Goal goal, PosInOccurrence pio); + GeneratedTaclet generate(Goal goal, PosInOccurrence pio); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java index 8206f5b851e..84232758d0e 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/OssLemmaGenerator.java @@ -92,7 +92,7 @@ public boolean isApplicable(Goal goal, PosInOccurrence pio) { } @Override - public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { + public GeneratedTaclet generate(Goal goal, PosInOccurrence pio) { final Services services = goal.proof().getServices(); final OneStepSimplifier simplifier = MiscTools.findOneStepSimplifier(goal.proof()); if (simplifier == null) { @@ -144,14 +144,17 @@ public RewriteTaclet generate(Goal goal, PosInOccurrence pio) { tb.setDisplayName(NAME.toString()); tb.setFind(find); tb.addGoalTerm(simplified); - tb.addRuleSet(new RuleSet(new Name("concrete"))); + final RuleSet generatedLemmaRS = + services.getNamespaces().ruleSets().lookup(new Name("generatedLemma")); + tb.addRuleSet(generatedLemmaRS != null ? generatedLemmaRS + : new RuleSet(new Name("generatedLemma"))); if (!assumesAntec.isEmpty() || !assumesSucc.isEmpty()) { tb.setAssumesSequent(JavaDLSequentKit.createSequent(assumesAntec, assumesSucc)); tb.setApplicationRestriction( new ApplicationRestriction(ApplicationRestriction.IN_SEQUENT_STATE)); } try { - return tb.getTaclet(); + return new GeneratedTaclet(tb.getTaclet(), result.numAppliedRules()); } catch (RuleAbortException e) { throw e; } catch (RuntimeException e) { @@ -189,18 +192,23 @@ public static boolean containsModality(Term term) { private static String contentHashName(Services services, JTerm find, ImmutableList assumesAntec, ImmutableList assumesSucc, JTerm replacewith) { + // hashed label-free: label serialization is not canonical (see + // LemmaTacletGenerator.removeTermLabels), and names must be reproducible on replay final StringBuilder content = new StringBuilder(); - content.append("find:").append(OutputStreamProofSaver.printTerm(find, services)); + content.append("find:").append(OutputStreamProofSaver + .printTerm(LemmaTacletGenerator.removeTermLabels(find, services), services)); for (final SequentFormula sf : assumesAntec) { - content.append("\nassumeA:") - .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + content.append("\nassumeA:").append(OutputStreamProofSaver.printTerm( + LemmaTacletGenerator.removeTermLabels((JTerm) sf.formula(), services), + services)); } for (final SequentFormula sf : assumesSucc) { - content.append("\nassumeS:") - .append(OutputStreamProofSaver.printTerm((JTerm) sf.formula(), services)); + content.append("\nassumeS:").append(OutputStreamProofSaver.printTerm( + LemmaTacletGenerator.removeTermLabels((JTerm) sf.formula(), services), + services)); } - content.append("\nreplacewith:") - .append(OutputStreamProofSaver.printTerm(replacewith, services)); + content.append("\nreplacewith:").append(OutputStreamProofSaver.printTerm( + LemmaTacletGenerator.removeTermLabels(replacewith, services), services)); try { final MessageDigest digest = MessageDigest.getInstance("SHA-256"); diff --git a/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java b/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java index 4624db08df6..86f20211379 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java +++ b/key.core/src/main/java/de/uka/ilkd/key/strategy/FOLStrategy.java @@ -28,6 +28,7 @@ import org.key_project.logic.Name; import org.key_project.prover.proof.ProofGoal; +import org.key_project.prover.proof.rulefilter.RuleFilter; import org.key_project.prover.proof.rulefilter.SetRuleFilter; import org.key_project.prover.rules.RuleApp; import org.key_project.prover.rules.RuleSet; @@ -81,10 +82,33 @@ public FOLStrategy(Proof proof, StrategyProperties strategyProperties) { approvalF = approvalDispatcher; } + /** + * The rule sets captured by the one step simplifier. In transparent mode these rules remain + * available to the strategy, but on formulas within the lemma fragment (no executable code) + * their applications are penalized so that the lemma path — which covers exactly those + * formulas with aggregated, separately provable steps — wins the scheduling race. The rules + * stay applicable (completeness): they still fire, as a last resort, on formulas the lemma + * path does not serve (e.g. vetoed ones). + */ + private static final Set OSS_CAPTURED_RULE_SETS = + Set.of("concrete", "concrete_java", "update_elim", "update_apply_on_update", + "update_apply", "update_join", "elimQuantifier"); + + /** + * Deliberately small: the penalty only needs to break the cost tie between the lemma path + * (introduction and lemma application at -11000) and the captured rules (concrete at + * -11000 plus depth scaling), so that aggregated lemmas win wherever they apply. It must + * NOT lift the captured rules above the enlarging/expanding rule band (around -2000): + * cost orderings such as "the empty-range rule of a bounded sum is cheaper than its + * unrolling rule" encode termination invariants of the strategy, and a large penalty + * inverts them (observed as an unbounded bounded-sum descent). + */ + private static final long TRANSPARENT_CAPTURED_PENALTY = 2000; + private Feature setUpGlobalF(RuleSetDispatchFeature d) { final Feature oneStepSimplificationF = oneStepSimplificationFeature(longConst(-11000)); - return add(d, oneStepSimplificationF); + return add(d, oneStepSimplificationF, transparentCapturedRuleSetPenalty()); } private Feature oneStepSimplificationFeature(Feature cost) { @@ -96,6 +120,31 @@ private Feature oneStepSimplificationFeature(Feature cost) { return ConditionalFeature.createConditional(filter, cost); } + private Feature transparentCapturedRuleSetPenalty() { + if (!transparentOss()) { + return longConst(0); + } + final RuleFilter capturedRuleSets = rule -> { + if (!(rule instanceof de.uka.ilkd.key.rule.Taclet taclet)) { + return false; + } + for (final RuleSet rs : taclet.getRuleSets()) { + if (OSS_CAPTURED_RULE_SETS.contains(rs.name().toString())) { + return true; + } + } + return false; + }; + return ConditionalFeature.createConditional(capturedRuleSets, + ifZero(applyTF(FocusFormulaProjection.INSTANCE, ff.notContainsExecutable), + longConst(TRANSPARENT_CAPTURED_PENALTY), longConst(0))); + } + + private boolean transparentOss() { + return StrategyProperties.OSS_TRANSPARENT + .equals(strategyProperties.getProperty(StrategyProperties.OSS_OPTIONS_KEY)); + } + private RuleSetDispatchFeature setupCostComputationF() { final RuleSetDispatchFeature d = new RuleSetDispatchFeature(); @@ -110,6 +159,11 @@ private RuleSetDispatchFeature setupCostComputationF() { bindRuleSet(d, "concrete", add(longConst(-11000), ScaleFeature.createScaled(findDepthFeature, 10.0))); + // generated lemma taclets (transparent one step simplification) are scheduled exactly + // like the simplifications they aggregate + bindRuleSet(d, "generatedLemma", + add(longConst(-11000), + ScaleFeature.createScaled(findDepthFeature, 10.0))); bindRuleSet(d, "simplify", -4500); bindRuleSet(d, "simplify_enlarging", -2000); bindRuleSet(d, "simplify_ENLARGING", -1900); @@ -631,10 +685,8 @@ public boolean isResponsibleFor(BuiltInRule rule) { } // in transparent mode, aggregated simplifications of lemma-eligible formulas are // performed via generated lemma taclets: the strategy applies the introduction rule - // (the simplifier itself yields those formulas, see OneStepSimplifier#isApplicable), - // and the introduced taclet is then applied through its "concrete" rule set - return rule instanceof OssLemmaIntroductionRule - && StrategyProperties.OSS_TRANSPARENT.equals( - strategyProperties.getProperty(StrategyProperties.OSS_OPTIONS_KEY)); + // (the simplifier itself is never applicable then), and the introduced taclet is then + // applied through its "generatedLemma" rule set + return rule instanceof OssLemmaIntroductionRule && transparentOss(); } } diff --git a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/ruleSetsDeclarations.key b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/ruleSetsDeclarations.key index b03574d0128..83aaf4d73c7 100644 --- a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/ruleSetsDeclarations.key +++ b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/ruleSetsDeclarations.key @@ -96,6 +96,9 @@ boolean_cases; alpha; concrete; + // taclets generated at proof time by lemma generators (transparent one step + // simplification); scheduled like the simplifications they aggregate + generatedLemma; try_apply_subst; type_hierarchy_def; From 0074c47a6dba9922f4a1d0bd22936b130ad90c0a Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 19:56:25 +0200 Subject: [PATCH 16/20] Batch prove: keep closed obligations out of the task tree; refresh status Two GUI defects reported on BinarySearch: after 'Prove all lemmas' the depending proof still showed 'closed but lemmas left' even though all lemmas were proven, and the batch registered all obligations, flooding the task tree with closed side proofs. The proof status was in fact correct at the model level (proving a lemma obligation triggers the depending proof's status recomputation, which reads the obligation proof object, not its environment registration). The dialog simply never refreshed: updateGlobalStatus runs once when the dialog opens. Changes: - GeneratedLemma separates obligation creation from environment registration. getOrCreateSoundnessProofAggregate now only creates; registerInEnvironment (idempotent) registers so a user interface surfaces the obligation. - LemmaProver registers only obligations that remain open (for manual work); closed obligations are certificates and stay unregistered, so a batch does not flood the environment. The depending proof's status still tracks them. - LemmaDependencyPanel: 'Load selected as side proofs' registers explicitly; after 'Prove all lemmas' it recomputes the depending proof's status (robust against proof-tree listener ordering) and refreshes the dialog's status display via a callback. Regression testBatchProvingUpdatesStatusWithoutFloodingEnvironment: proving all lemmas turns the status to CLOSED automatically and leaves the environment's proof count unchanged. Created with AI tooling support --- .../ilkd/key/rule/lemma/GeneratedLemma.java | 37 ++++++++++++++----- .../uka/ilkd/key/rule/lemma/LemmaProver.java | 21 +++++++++-- .../lemma/TestLemmaProverAndGrouping.java | 36 +++++++++++++++++- .../key/rule/lemma/TestMissingLemmas.java | 22 ++++++++--- .../ilkd/key/gui/LemmaDependencyPanel.java | 25 +++++++++++-- .../ilkd/key/gui/ProofManagementDialog.java | 1 + 6 files changed, 117 insertions(+), 25 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java index 5b57b8f32a2..e60a750e458 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/GeneratedLemma.java @@ -47,6 +47,7 @@ public final class GeneratedLemma { private final int aggregatedSteps; private @Nullable ProofAggregate soundnessProofAggregate; private @Nullable Proof soundnessProof; + private boolean registeredInEnvironment; GeneratedLemma(RewriteTaclet taclet, Proof mainProof, Name generatorName, int aggregatedSteps) { @@ -141,26 +142,47 @@ public synchronized boolean isProven() { } /** - * returns the proof for the soundness proof obligation of the taclet, creating (and, if a - * proof environment is present, registering) it on first call + * returns the proof for the soundness proof obligation of the taclet, creating it (but + * not registering it in the proof environment) on first call */ public synchronized Proof getOrCreateSoundnessProof() { return getOrCreateSoundnessProofAggregate().getFirstProof(); } /** - * returns the proof aggregate for the soundness proof obligation of the taclet, creating - * (and, if a proof environment is present, registering in it) it on first call. The - * aggregate is what a user interface registers to display and drive the proof. + * returns the proof aggregate for the soundness proof obligation of the taclet, creating it + * on first call. Creation does not register the obligation in the proof environment + * — a batch that proves and closes many obligations should not flood the environment (and a + * user interface) with closed side proofs. Registration is an explicit, separate step (see + * {@link #registerInEnvironment()}); the main proof's status tracks the obligation through + * this lemma regardless of registration. */ public synchronized ProofAggregate getOrCreateSoundnessProofAggregate() { if (soundnessProof == null || soundnessProof.isDisposed()) { soundnessProofAggregate = createSoundnessProof(); soundnessProof = soundnessProofAggregate.getFirstProof(); + registeredInEnvironment = false; } return soundnessProofAggregate; } + /** + * Registers the soundness proof obligation in the main proof's environment (creating it + * first if necessary), so that a user interface listening on the environment displays and + * can drive it. Idempotent; does nothing if there is no environment. + * + * @return the registered aggregate + */ + public synchronized ProofAggregate registerInEnvironment() { + final ProofAggregate aggregate = getOrCreateSoundnessProofAggregate(); + final ProofEnvironment env = mainProof.getEnv(); + if (env != null && !registeredInEnvironment) { + env.registerProof(new GeneratedLemmaPO(taclet.name(), aggregate), aggregate); + registeredInEnvironment = true; + } + return aggregate; + } + /** * Creates the soundness proof obligation for the taclet, reusing the standard taclet lemma * machinery. The proof runs in a copy of the main proof's initial configuration; the @@ -181,11 +203,6 @@ private ProofAggregate createSoundnessProof() { final ProofAggregate po = new ProofObligationCreator().create(tacletsToProve, new InitConfig[] { poConfig }, DefaultImmutableSet.nil(), List.of()); - - final ProofEnvironment env = mainProof.getEnv(); - if (env != null) { - env.registerProof(new GeneratedLemmaPO(taclet.name(), po), po); - } return po; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java index 2d67cd5c7a3..f7bd392e259 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java @@ -26,9 +26,12 @@ * that they can be shown to the user for manual completion. * *

    - * The obligation proofs are created in the lemma's proof environment (see - * {@link GeneratedLemma#getOrCreateSoundnessProofAggregate()}), which surfaces them in an - * attached user interface. This class does not itself perform any user-interface registration. + * Closed obligations are certificates and are deliberately not registered in the proof + * environment, so a batch over many obligations does not flood the environment (and an attached + * user interface) with closed side proofs; the main proof's status still tracks them through the + * lemmas. Obligations that remain open are registered (see + * {@link GeneratedLemma#registerInEnvironment()}) so that an attached user interface surfaces + * them for manual completion. */ @NullMarked public final class LemmaProver { @@ -84,7 +87,17 @@ public static Result proveAll(Collection lemmas, int maxSteps, lemma.taclet().name(), e); } } - (po.closed() ? proven : remaining).add(lemma); + if (po.closed()) { + // a closed obligation is a certificate; it needs no further attention and is + // deliberately not registered in the environment, so a batch that proves many + // obligations does not flood the environment (and a user interface) with + // closed side proofs. The main proof's status still tracks it through the + // lemma. Only obligations that remain open are surfaced for manual work. + proven.add(lemma); + } else { + remaining.add(lemma); + lemma.registerInEnvironment(); + } if (saveDir != null) { final Path file = saveDir.resolve( diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java index 8d123db7e85..da060e217ff 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestLemmaProverAndGrouping.java @@ -136,6 +136,35 @@ public void testContentGroupingCollapsesDuplicates() throws Exception { } } + @Test + public void testBatchProvingUpdatesStatusWithoutFloodingEnvironment() throws Exception { + // Regression for the reported GUI issue: after batch-proving all lemmas, the depending + // proof must become CLOSED (not "closed but lemmas left") automatically, and the closed + // obligations must not be registered in the environment (no flooding of the task tree). + final Path file = TEST_CASE_DIRECTORY + .resolve("../../../../../key.ui/examples/standard_key/arith/computation.key"); + + try (KeYEnvironment env = KeYEnvironment.load(file)) { + final Proof proof = transparentClosed(env, 20000); + assertEquals(de.uka.ilkd.key.proof.mgt.ProofStatus.CLOSED_BUT_LEMMAS_LEFT, + proof.mgt().getStatus(), + "before proving the lemmas the proof is closed only modulo them"); + final int proofsBefore = proof.getEnv().getProofs().size(); + + final GeneratedLemmaRegistry registry = GeneratedLemmaRegistry.get(proof); + final LemmaProver.Result result = + LemmaProver.proveAll(registry.getLemmas(), 10000, null); + assertTrue(result.remaining().isEmpty(), "all arith lemmas should close"); + + // status became CLOSED automatically (no explicit updateProofStatus call here) + assertEquals(de.uka.ilkd.key.proof.mgt.ProofStatus.CLOSED, proof.mgt().getStatus(), + "proving all lemmas must turn the proof status to CLOSED"); + // no closed obligation was registered in the environment + assertEquals(proofsBefore, proof.getEnv().getProofs().size(), + "closed obligations must not be registered in the environment"); + } + } + @Test public void testBatchProvingDischargesAndSaves() throws Exception { final Path file = TEST_CASE_DIRECTORY @@ -156,8 +185,11 @@ public void testBatchProvingDischargesAndSaves() throws Exception { assertFalse(result.proven().isEmpty(), "arith lemmas should close"); for (final GeneratedLemma lemma : result.proven()) { assertTrue(lemma.isProven()); - assertSame(proof.getEnv(), lemma.getSoundnessProofIfPresent().getEnv(), - "obligation proof lives in the main proof's environment"); + // a closed obligation is a certificate and is deliberately NOT registered in the + // environment (no flooding of the task tree with closed side proofs) + assertFalse(proof.getEnv().getProofs().stream() + .anyMatch(pl -> pl.getFirstProof() == lemma.getSoundnessProofIfPresent()), + "a closed obligation must not be registered in the environment"); } assertTrue(registry.getMissingLemmas().size() <= result.remaining().size(), "proven lemmas leave the missing set"); diff --git a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java index ae73013ce12..e1ba6da9ed4 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java +++ b/key.core/src/test/java/de/uka/ilkd/key/rule/lemma/TestMissingLemmas.java @@ -66,18 +66,28 @@ public void testMissingLemmasLifecycle() throws Exception { "all generated lemmas are initially missing"); assertFalse(missing.isEmpty()); - // "load as side proof": create the soundness proof aggregate for one lemma; it gets - // registered in the target's proof environment + // creating the obligation alone does not register it (a batch of closed obligations + // must not flood the environment) final GeneratedLemma first = missing.get(0); assertFalse(first.isSoundnessProofPresent()); - final ProofAggregate aggregate = first.getOrCreateSoundnessProofAggregate(); - final Proof soundnessProof = aggregate.getFirstProof(); + first.getOrCreateSoundnessProofAggregate(); assertTrue(first.isSoundnessProofPresent()); + assertFalse(target.getEnv().getProofs().stream() + .anyMatch(pl -> pl.getFirstProof() == first.getSoundnessProofIfPresent()), + "creating the obligation must not register it in the environment"); + + // "load as side proof": registering surfaces it in the environment + final ProofAggregate aggregate = first.registerInEnvironment(); + final Proof soundnessProof = aggregate.getFirstProof(); assertSame(target.getEnv(), soundnessProof.getEnv(), - "soundness proof must live in the same proof environment as the main proof"); + "registered soundness proof lives in the main proof's environment"); assertTrue(target.getEnv().getProofs().stream() .anyMatch(pl -> pl.getFirstProof() == soundnessProof), - "soundness proof must be registered in the proof environment"); + "registered soundness proof must be in the proof environment"); + // registering again does not duplicate it + first.registerInEnvironment(); + assertEquals(1, target.getEnv().getProofs().stream() + .filter(pl -> pl.getFirstProof() == soundnessProof).count()); // an open (not yet closed) soundness proof still counts as missing assertTrue(registry.getMissingLemmas().contains(first), diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java index de597672383..c624589cf5f 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java @@ -59,6 +59,7 @@ public final class LemmaDependencyPanel extends JPanel { private final JButton proveAllButton = new JButton("Prove all lemmas..."); private @Nullable Proof proof; private @Nullable Runnable onLoaded; + private @Nullable Runnable onStatusChanged; public LemmaDependencyPanel(KeYMediator mediator) { super(new BorderLayout()); @@ -92,6 +93,14 @@ public void setOnLoaded(Runnable onLoaded) { this.onLoaded = onLoaded; } + /** + * Sets an action to run after the proven status of lemmas may have changed (e.g. to refresh + * the enclosing dialog's proof-status display). + */ + public void setOnStatusChanged(Runnable onStatusChanged) { + this.onStatusChanged = onStatusChanged; + } + /** * Shows the lemma dependencies of the given proof (clears the view for {@code null} or a * proof without generated lemmas). @@ -168,9 +177,9 @@ private void loadSelected() { } Proof first = null; for (final GeneratedLemma lemma : selected) { - // creating the obligation registers it in the environment; the user interface picks - // it up via the environment listener, so no explicit UI registration here - final Proof po = lemma.getOrCreateSoundnessProof(); + // register the obligation in the environment so the user can work on it; the user + // interface picks it up via the environment listener and adds it to the task tree + final Proof po = lemma.registerInEnvironment().getFirstProof(); if (first == null) { first = po; } @@ -224,6 +233,16 @@ protected void done() { JOptionPane.ERROR_MESSAGE); } refresh(); + // proving lemmas may have turned the depending proof from "closed but lemmas + // left" into "closed". Recompute the status explicitly (robust against the + // order in which proof-tree listeners handled the obligation-closed events) and + // let the enclosing dialog refresh its status display. + if (proof != null) { + proof.mgt().updateProofStatus(); + } + if (onStatusChanged != null) { + onStatusChanged.run(); + } } }.execute(); } diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java index ab7bd04ee87..b51c71d1769 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofManagementDialog.java @@ -197,6 +197,7 @@ public void mouseClicked(MouseEvent e) { // contracts and the generated lemmas it depends on lemmaDependencyPanel = new LemmaDependencyPanel(mediator); lemmaDependencyPanel.setOnLoaded(() -> setVisible(false)); + lemmaDependencyPanel.setOnStatusChanged(this::updateGlobalStatus); JTabbedPane byProofDetails = new JTabbedPane(); byProofDetails.addTab("Contracts", contractPanelByProof); byProofDetails.addTab("Lemmas", lemmaDependencyPanel); From c955aee886ce1cfd8baf7e221f453f0d6cae2946 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 20:03:26 +0200 Subject: [PATCH 17/20] Task tree: abandon all proofs of an environment at once Right-clicking an environment node in the Loaded Proofs tree now offers 'Abandon All Proofs of Environment', which disposes every proof of that environment after a single confirmation. Previously an environment that had accumulated many proofs (e.g. lemma side proofs loaded in bulk from the proof management dialog) could only be cleared by abandoning each proof individually or restarting KeY. The proofs are collected before disposing, since disposing a proof removes its node from the tree. Created with AI tooling support --- .../java/de/uka/ilkd/key/gui/TaskTree.java | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/TaskTree.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/TaskTree.java index 7faae53802b..741b3ea20d5 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/TaskTree.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/TaskTree.java @@ -305,11 +305,56 @@ private void checkPopup(MouseEvent e) { JPopupMenu menu = KeYGuiExtensionFacade.createContextMenu( ContextMenuKind.PROOF_LIST, p, mediator); menu.show(e.getComponent(), e.getX(), e.getY()); + } else if (selPath != null + && selPath.getLastPathComponent() instanceof EnvNode envNode) { + delegateView.setSelectionPath(selPath); + final JPopupMenu menu = new JPopupMenu(); + final JMenuItem abandonAll = new JMenuItem("Abandon All Proofs of Environment"); + abandonAll.setToolTipText( + "Drop every proof loaded in this environment at once."); + abandonAll.addActionListener(ev -> abandonEnvironment(envNode)); + menu.add(abandonAll); + menu.show(e.getComponent(), e.getX(), e.getY()); } } } } + /** + * Abandons (disposes) all proofs of the given environment node after confirmation. Useful + * to clean up an environment that accumulated many proofs at once (e.g. lemma side proofs + * loaded in bulk) without abandoning them one by one. + * + * @param envNode the environment node whose proofs to abandon + */ + private void abandonEnvironment(EnvNode envNode) { + // collect the proofs first: disposing a proof removes its node from the tree, which + // would otherwise invalidate an in-progress traversal of the environment's children + final List proofs = new LinkedList<>(); + for (int i = 0, n = envNode.getChildCount(); i < n; i++) { + if (envNode.getChildAt(i) instanceof TaskTreeNode taskNode) { + for (final Proof p : taskNode.allProofs()) { + proofs.add(p); + } + } + } + if (proofs.isEmpty()) { + return; + } + if (!mediator.getUI().confirmTaskRemoval( + "Abandon all " + proofs.size() + " proof(s) of this environment?")) { + return; + } + if (mediator.isInAutoMode()) { + mediator.getUI().getProofControl().stopAutoMode(); + } + for (final Proof p : proofs) { + if (!p.isDisposed()) { + p.dispose(); + } + } + } + /** * a prooftree listener, so that it is known when the proof has closed From 07749b155ad9eb9a08875fb10a9da985e751078c Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 20:13:02 +0200 Subject: [PATCH 18/20] Prove all lemmas: show a progress dialog with a cancel button Batch proving many lemma obligations gave no feedback while it ran. It now shows a small modeless progress dialog with a progress bar (done / total) and a Cancel button. LemmaProver.proveAll gained a progress listener that reports after each obligation and can request cancellation (the remaining, unprocessed lemmas are then simply not attempted); the existing three-argument overload delegates to it with no listener. The panel drives the bar via the SwingWorker's publish/process and disposes the dialog when done. Created with AI tooling support --- .../uka/ilkd/key/rule/lemma/LemmaProver.java | 37 +++++ .../ilkd/key/gui/LemmaDependencyPanel.java | 138 +++++++++++++----- 2 files changed, 139 insertions(+), 36 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java index f7bd392e259..834401f540f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/lemma/LemmaProver.java @@ -48,6 +48,22 @@ public final class LemmaProver { public record Result(List proven, List remaining, int saved) { } + /** + * Notified of the progress of a batch proving run, and asked whether to continue. + */ + @FunctionalInterface + public interface ProgressListener { + /** + * Called after each obligation has been processed. + * + * @param done the number of obligations processed so far + * @param total the total number of obligations + * @return {@code true} to continue, {@code false} to stop the batch (the remaining, + * unprocessed lemmas are not counted as {@code remaining} in the result) + */ + boolean progressed(int done, int total); + } + private LemmaProver() { } @@ -62,9 +78,26 @@ private LemmaProver() { */ public static Result proveAll(Collection lemmas, int maxSteps, @Nullable Path saveDir) throws Exception { + return proveAll(lemmas, maxSteps, saveDir, null); + } + + /** + * Attempts to discharge the soundness obligations of the given lemmas, reporting progress. + * + * @param lemmas the lemmas to prove + * @param maxSteps the automatic-mode step bound per obligation + * @param saveDir if non-null, a directory into which every obligation proof is saved + * @param listener if non-null, notified after each obligation and asked whether to continue + * @return the outcome + * @throws Exception if saving a proof fails + */ + public static Result proveAll(Collection lemmas, int maxSteps, + @Nullable Path saveDir, @Nullable ProgressListener listener) throws Exception { final List proven = new ArrayList<>(); final List remaining = new ArrayList<>(); int saved = 0; + int done = 0; + final int total = lemmas.size(); if (saveDir != null) { Files.createDirectories(saveDir); @@ -110,6 +143,10 @@ public static Result proveAll(Collection lemmas, int maxSteps, saved++; } } + done++; + if (listener != null && !listener.progressed(done, total)) { + break; + } } return new Result(proven, remaining, saved); } diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java index c624589cf5f..2eb4080a060 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/LemmaDependencyPanel.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import javax.swing.*; import javax.swing.border.TitledBorder; import javax.swing.tree.DefaultMutableTreeNode; @@ -205,46 +206,111 @@ private void proveAll() { proveAllButton.setEnabled(false); loadButton.setEnabled(false); mediator.stopInterface(true); - new SwingWorker() { - @Override - protected LemmaProver.Result doInBackground() throws Exception { - return LemmaProver.proveAll(lemmas, options.maxSteps(), options.saveDir()); - } - @Override - protected void done() { - mediator.startInterface(true); - try { - final LemmaProver.Result result = get(); - JOptionPane.showMessageDialog(LemmaDependencyPanel.this, - result.proven().size() + " of " + lemmas.size() - + " lemma(s) proved, " + result.remaining().size() - + " left open" - + (options.saveDir() != null - ? ", " + result.saved() + " proof(s) saved to " - + options.saveDir() - : "") - + ".", - "Prove Lemmas", JOptionPane.INFORMATION_MESSAGE); - } catch (Exception e) { - LOGGER.error("batch proving of lemmas failed", e); - JOptionPane.showMessageDialog(LemmaDependencyPanel.this, - "Batch proving failed:\n" + e.getMessage(), "Prove Lemmas", - JOptionPane.ERROR_MESSAGE); + final ProgressDialog progressDialog = new ProgressDialog(lemmas.size()); + final AtomicBoolean cancelled = new AtomicBoolean(false); + progressDialog.onCancel(() -> cancelled.set(true)); + + final SwingWorker worker = + new SwingWorker<>() { + @Override + protected LemmaProver.Result doInBackground() throws Exception { + return LemmaProver.proveAll(lemmas, options.maxSteps(), options.saveDir(), + (done, total) -> { + publish(done); + return !cancelled.get(); + }); } - refresh(); - // proving lemmas may have turned the depending proof from "closed but lemmas - // left" into "closed". Recompute the status explicitly (robust against the - // order in which proof-tree listeners handled the obligation-closed events) and - // let the enclosing dialog refresh its status display. - if (proof != null) { - proof.mgt().updateProofStatus(); + + @Override + protected void process(List chunks) { + progressDialog.setProgress(chunks.get(chunks.size() - 1)); } - if (onStatusChanged != null) { - onStatusChanged.run(); + + @Override + protected void done() { + progressDialog.dispose(); + mediator.startInterface(true); + try { + final LemmaProver.Result result = get(); + JOptionPane.showMessageDialog(LemmaDependencyPanel.this, + result.proven().size() + " of " + lemmas.size() + + " lemma(s) proved, " + result.remaining().size() + + " left open" + + (options.saveDir() != null + ? ", " + result.saved() + " proof(s) saved to " + + options.saveDir() + : "") + + ".", + "Prove Lemmas", JOptionPane.INFORMATION_MESSAGE); + } catch (Exception e) { + LOGGER.error("batch proving of lemmas failed", e); + JOptionPane.showMessageDialog(LemmaDependencyPanel.this, + "Batch proving failed:\n" + e.getMessage(), "Prove Lemmas", + JOptionPane.ERROR_MESSAGE); + } + refresh(); + // proving lemmas may have turned the depending proof from "closed but lemmas + // left" into "closed". Recompute the status explicitly (robust against the + // order in which proof-tree listeners handled the obligation-closed events) + // and let the enclosing dialog refresh its status display. + if (proof != null) { + proof.mgt().updateProofStatus(); + } + if (onStatusChanged != null) { + onStatusChanged.run(); + } } - } - }.execute(); + }; + worker.execute(); + progressDialog.setVisible(true); + } + + /** A small modeless progress dialog for the batch proving of lemmas. */ + private final class ProgressDialog extends JDialog { + private static final long serialVersionUID = 1L; + + private final JProgressBar bar; + private @Nullable Runnable cancelAction; + + ProgressDialog(int total) { + super(SwingUtilities.getWindowAncestor(LemmaDependencyPanel.this), "Proving Lemmas", + ModalityType.MODELESS); + bar = new JProgressBar(0, total); + bar.setStringPainted(true); + bar.setString("0 / " + total); + + final JButton cancel = new JButton("Cancel"); + cancel.addActionListener(e -> { + cancel.setEnabled(false); + bar.setString("cancelling after current lemma..."); + if (cancelAction != null) { + cancelAction.run(); + } + }); + + final JPanel content = new JPanel(new BorderLayout(8, 8)); + content.setBorder(BorderFactory.createEmptyBorder(12, 12, 12, 12)); + content.add(new JLabel("Discharging lemma soundness obligations..."), + BorderLayout.NORTH); + content.add(bar, BorderLayout.CENTER); + final JPanel south = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0)); + south.add(cancel); + content.add(south, BorderLayout.SOUTH); + setContentPane(content); + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + pack(); + setLocationRelativeTo(getOwner()); + } + + void onCancel(Runnable action) { + this.cancelAction = action; + } + + void setProgress(int done) { + bar.setValue(done); + bar.setString(done + " / " + bar.getMaximum()); + } } private static String truncate(String s) { From 672fbd702afbbcff830dda65e6f55cb6942454dc Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 21:28:21 +0200 Subject: [PATCH 19/20] Remove stray auto-generated files accidentally committed Removes two batch-mode proof outputs (lemmagen-demo.auto*.proof) and a statistics CSV that were swept in by a broad 'git add' while wiring up the OSS lemma generator. Only the intentional demo problem lemmagen-demo.key remains. Created with AI tooling support --- key.ui/lemmagen-demo.key.csv | 17 -------- lemmagen-demo.auto.0.proof | 82 ------------------------------------ lemmagen-demo.auto.proof | 82 ------------------------------------ 3 files changed, 181 deletions(-) delete mode 100644 key.ui/lemmagen-demo.key.csv delete mode 100644 lemmagen-demo.auto.0.proof delete mode 100644 lemmagen-demo.auto.proof diff --git a/key.ui/lemmagen-demo.key.csv b/key.ui/lemmagen-demo.key.csv deleted file mode 100644 index 4b51c9a0a09..00000000000 --- a/key.ui/lemmagen-demo.key.csv +++ /dev/null @@ -1,17 +0,0 @@ -open goals;0 -Nodes;10 -Branches;1 -Interactive steps;0 -Symbolic execution steps;0 -Automode time;32ms -Avg. time per step;3.555ms -Rule applications -Quantifier instantiations;0 -One-step Simplifier apps;3 -SMT solver apps;0 -Dependency Contract apps;0 -Operation Contract apps;0 -Block/Loop Contract apps;0 -Loop invariant apps;0 -Merge Rule apps;0 -Total rule apps;11 diff --git a/lemmagen-demo.auto.0.proof b/lemmagen-demo.auto.0.proof deleted file mode 100644 index 7f06277b4d1..00000000000 --- a/lemmagen-demo.auto.0.proof +++ /dev/null @@ -1,82 +0,0 @@ -\profile "Java Profile"; - -\settings { - "Choice" : { - "JavaCard" : "JavaCard:off", - "Strings" : "Strings:on", - "assertions" : "assertions:safe", - "bigint" : "bigint:on", - "finalFields" : "finalFields:immutable", - "floatRules" : "floatRules:strictfpOnly", - "initialisation" : "initialisation:disableStaticInitialisation", - "intRules" : "intRules:arithmeticSemanticsIgnoringOF", - "integerSimplificationRules" : "integerSimplificationRules:full", - "javaLoopTreatment" : "javaLoopTreatment:efficient", - "mergeGenerateIsWeakeningGoal" : "mergeGenerateIsWeakeningGoal:off", - "methodExpansion" : "methodExpansion:modularOnly", - "modelFields" : "modelFields:treatAsAxiom", - "moreSeqRules" : "moreSeqRules:off", - "permissions" : "permissions:on", - "programRules" : "programRules:Java", - "reach" : "reach:on", - "runtimeExceptions" : "runtimeExceptions:ban", - "sequences" : "sequences:on", - "soundDefaultContracts" : "soundDefaultContracts:on" - }, - "Labels" : { - "UseOriginLabels" : true - }, - "NewSMT" : { - - }, - "SMTSettings" : { - "SelectedTaclets" : [ - - ], - "UseBuiltUniqueness" : false, - "explicitTypeHierarchy" : false, - "instantiateHierarchyAssumptions" : true, - "integersMaximum" : 2147483645, - "integersMinimum" : -2147483645, - "invariantForall" : false, - "maxGenericSorts" : 2, - "useConstantsForBigOrSmallIntegers" : true, - "useUninterpretedMultiplication" : true - }, - "Strategy" : { - "ActiveStrategy" : "Modular JavaDL Strategy", - "MaximumNumberOfAutomaticApplications" : 10000, - "Timeout" : -1, - "options" : { - "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", - "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", - "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_FREE", - "DEP_OPTIONS_KEY" : "DEP_ON", - "LOOP_OPTIONS_KEY" : "LOOP_SCOPE_INV_TACLET", - "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", - "MPS_OPTIONS_KEY" : "MPS_MERGE", - "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_NONE", - "OSS_OPTIONS_KEY" : "OSS_ON", - "QUANTIFIERS_OPTIONS_KEY" : "QUANTIFIERS_NON_SPLITTING_WITH_PROGS", - "QUERYAXIOM_OPTIONS_KEY" : "QUERYAXIOM_ON", - "QUERY_NEW_OPTIONS_KEY" : "QUERY_OFF", - "SPLITTING_OPTIONS_KEY" : "SPLITTING_DELAYED", - "STOPMODE_OPTIONS_KEY" : "STOPMODE_DEFAULT", - "SYMBOLIC_EXECUTION_ALIAS_CHECK_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_ALIAS_CHECK_NEVER", - "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OFF", - "USER_TACLETS_OPTIONS_KEY1" : "USER_TACLETS_OFF", - "USER_TACLETS_OPTIONS_KEY2" : "USER_TACLETS_OFF", - "USER_TACLETS_OPTIONS_KEY3" : "USER_TACLETS_OFF", - "VBT_PHASE" : "VBT_SYM_EX" - } - } -} - - - -\problem { - add(Z(5(#)), Z(7(#))) = Z(2(1(#))) -& mul(Z(2(#)), add(Z(5(#)), Z(7(#)))) = Z(4(2(#))) -& add(add(Z(5(#)), Z(7(#))), Z(1(#))) = Z(3(1(#))) -} - diff --git a/lemmagen-demo.auto.proof b/lemmagen-demo.auto.proof deleted file mode 100644 index 7f06277b4d1..00000000000 --- a/lemmagen-demo.auto.proof +++ /dev/null @@ -1,82 +0,0 @@ -\profile "Java Profile"; - -\settings { - "Choice" : { - "JavaCard" : "JavaCard:off", - "Strings" : "Strings:on", - "assertions" : "assertions:safe", - "bigint" : "bigint:on", - "finalFields" : "finalFields:immutable", - "floatRules" : "floatRules:strictfpOnly", - "initialisation" : "initialisation:disableStaticInitialisation", - "intRules" : "intRules:arithmeticSemanticsIgnoringOF", - "integerSimplificationRules" : "integerSimplificationRules:full", - "javaLoopTreatment" : "javaLoopTreatment:efficient", - "mergeGenerateIsWeakeningGoal" : "mergeGenerateIsWeakeningGoal:off", - "methodExpansion" : "methodExpansion:modularOnly", - "modelFields" : "modelFields:treatAsAxiom", - "moreSeqRules" : "moreSeqRules:off", - "permissions" : "permissions:on", - "programRules" : "programRules:Java", - "reach" : "reach:on", - "runtimeExceptions" : "runtimeExceptions:ban", - "sequences" : "sequences:on", - "soundDefaultContracts" : "soundDefaultContracts:on" - }, - "Labels" : { - "UseOriginLabels" : true - }, - "NewSMT" : { - - }, - "SMTSettings" : { - "SelectedTaclets" : [ - - ], - "UseBuiltUniqueness" : false, - "explicitTypeHierarchy" : false, - "instantiateHierarchyAssumptions" : true, - "integersMaximum" : 2147483645, - "integersMinimum" : -2147483645, - "invariantForall" : false, - "maxGenericSorts" : 2, - "useConstantsForBigOrSmallIntegers" : true, - "useUninterpretedMultiplication" : true - }, - "Strategy" : { - "ActiveStrategy" : "Modular JavaDL Strategy", - "MaximumNumberOfAutomaticApplications" : 10000, - "Timeout" : -1, - "options" : { - "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", - "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", - "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_FREE", - "DEP_OPTIONS_KEY" : "DEP_ON", - "LOOP_OPTIONS_KEY" : "LOOP_SCOPE_INV_TACLET", - "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", - "MPS_OPTIONS_KEY" : "MPS_MERGE", - "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_NONE", - "OSS_OPTIONS_KEY" : "OSS_ON", - "QUANTIFIERS_OPTIONS_KEY" : "QUANTIFIERS_NON_SPLITTING_WITH_PROGS", - "QUERYAXIOM_OPTIONS_KEY" : "QUERYAXIOM_ON", - "QUERY_NEW_OPTIONS_KEY" : "QUERY_OFF", - "SPLITTING_OPTIONS_KEY" : "SPLITTING_DELAYED", - "STOPMODE_OPTIONS_KEY" : "STOPMODE_DEFAULT", - "SYMBOLIC_EXECUTION_ALIAS_CHECK_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_ALIAS_CHECK_NEVER", - "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OPTIONS_KEY" : "SYMBOLIC_EXECUTION_NON_EXECUTION_BRANCH_HIDING_OFF", - "USER_TACLETS_OPTIONS_KEY1" : "USER_TACLETS_OFF", - "USER_TACLETS_OPTIONS_KEY2" : "USER_TACLETS_OFF", - "USER_TACLETS_OPTIONS_KEY3" : "USER_TACLETS_OFF", - "VBT_PHASE" : "VBT_SYM_EX" - } - } -} - - - -\problem { - add(Z(5(#)), Z(7(#))) = Z(2(1(#))) -& mul(Z(2(#)), add(Z(5(#)), Z(7(#)))) = Z(4(2(#))) -& add(add(Z(5(#)), Z(7(#))), Z(1(#))) = Z(3(1(#))) -} - From 4294be3f8873f2c2eff0609fd42330b2e8783f32 Mon Sep 17 00:00:00 2001 From: Richard Bubel Date: Sun, 5 Jul 2026 22:47:12 +0200 Subject: [PATCH 20/20] Undo determinism fix of previous commit for backwards compatibility --- .../java/de/uka/ilkd/key/rule/OneStepSimplifier.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java index f7481064dee..e2977085be9 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/OneStepSimplifier.java @@ -207,15 +207,7 @@ private void initIndices(Proof proof) { ImmutableList done = ImmutableList.nil(); for (String ruleSet : ruleSets) { ImmutableList taclets = tacletsForRuleSet(proof, ruleSet, done); - // Sort by name for a deterministic index: the taclets are collected in set - // iteration order, and when several rules match at the same position the - // simplifier applies the first one found. Without a canonical order, the - // aggregated simplification result may differ between proof instances of the - // same problem (e.g. between a proof and its replayed or elaborated copy). - final ArrayList sorted = new ArrayList<>(); - taclets.forEach(sorted::add); - sorted.sort(Comparator.comparing(t -> t.name().toString())); - indices[i] = TacletIndexKit.getKit().createTacletIndex(sorted); + indices[i] = TacletIndexKit.getKit().createTacletIndex(taclets); notSimplifiableCaches[i] = new LRUCache<>(DEFAULT_CACHE_SIZE); i++; done = done.prepend(ruleSet);