diff --git a/key.core.symbolic_execution/src/main/java/de/uka/ilkd/key/symbolic_execution/strategy/SimplifyTermStrategy.java b/key.core.symbolic_execution/src/main/java/de/uka/ilkd/key/symbolic_execution/strategy/SimplifyTermStrategy.java index 5ad174975fd..8f9b35a737b 100644 --- a/key.core.symbolic_execution/src/main/java/de/uka/ilkd/key/symbolic_execution/strategy/SimplifyTermStrategy.java +++ b/key.core.symbolic_execution/src/main/java/de/uka/ilkd/key/symbolic_execution/strategy/SimplifyTermStrategy.java @@ -117,7 +117,7 @@ public Strategy create(Proof proof, StrategyProperties sp) { */ @Override public StrategySettingsDefinition getSettingsDefinition() { - return JavaProfile.DEFAULT.getSettingsDefinition(); + return JavaProfile.getDefault().getSettingsDefinition(); } } } diff --git a/key.core/build.gradle b/key.core/build.gradle index df2ba408900..a6afddbc918 100644 --- a/key.core/build.gradle +++ b/key.core/build.gradle @@ -111,6 +111,7 @@ classes.dependsOn << generateSolverPropsList tasks.withType(Test) { enableAssertions = true + systemProperties(System.properties.findAll { key, value -> key.startsWith("key.") }) } diff --git a/key.core/src/main/antlr4/JmlLexer.g4 b/key.core/src/main/antlr4/JmlLexer.g4 index 8b4eb5880cb..8b2d76b6bad 100644 --- a/key.core/src/main/antlr4/JmlLexer.g4 +++ b/key.core/src/main/antlr4/JmlLexer.g4 @@ -175,6 +175,8 @@ mode expr; /* Java keywords */ BOOLEAN: 'boolean'; BYTE: 'byte'; +CASE: 'case'; +DEFAULT: 'default'; FALSE: 'false'; INSTANCEOF: 'instanceof'; INT: 'int'; @@ -244,6 +246,7 @@ FP_SUBNORMAL: '\\fp_subnormal'; //KeY extension, not official JML FP_ZERO: '\\fp_zero'; //KeY extension, not official JML FREE: '\\free'; //KeY extension, not official JML FRESH: '\\fresh'; +FROM_GOAL: '\\from_goal'; //KeY extension, not official JML INDEX: '\\index'; INDEXOF: '\\seq_indexOf'; //KeY extension, not official JML INTERSECT: '\\intersect'; //KeY extension, not official JML diff --git a/key.core/src/main/antlr4/JmlParser.g4 b/key.core/src/main/antlr4/JmlParser.g4 index 0060642e03b..cfbaeb4fee8 100644 --- a/key.core/src/main/antlr4/JmlParser.g4 +++ b/key.core/src/main/antlr4/JmlParser.g4 @@ -9,9 +9,11 @@ options { tokenVocab=JmlLexer; } @members { private SyntaxErrorReporter errorReporter = new SyntaxErrorReporter(getClass()); public SyntaxErrorReporter getErrorReporter() { return errorReporter;} + private boolean isNextToken(String tokenText) { + return _input.LA(1) != Token.EOF && tokenText.equals(_input.LT(1).getText()); + } } - classlevel_comments: classlevel_comment* EOF; classlevel_comment: classlevel_element | modifiers | set_statement; classlevel_element0: modifiers? (classlevel_element modifiers?); @@ -202,12 +204,35 @@ block_specification: method_specification; block_loop_specification: loop_contract_keyword spec_case ((also_keyword)+ loop_contract_keyword spec_case)*; loop_contract_keyword: LOOP_CONTRACT; -assert_statement: (ASSERT expression | UNREACHABLE) SEMI_TOPLEVEL; +assert_statement: (ASSERT (label=IDENT COLON)? expression | UNREACHABLE) (assertionProof SEMI_TOPLEVEL? | SEMI_TOPLEVEL); //breaks_clause: BREAKS expression; //continues_clause: CONTINUES expression; //returns_clause: RETURNS expression; +// --- proof scripts in JML +assertionProof: BY (proofCmd | LBRACE ( proofCmd )+ RBRACE) ; +proofCmd: + // TODO allow more than one var in obtain + { isNextToken("obtain") }? obtain=IDENT typespec var=IDENT + ( obtKind=EQUAL_SINGLE expression SEMI + | obtKind=SUCH_THAT expression proofCmdSuffix + | obtKind=FROM_GOAL SEMI + ) + | cmd=IDENT ( proofArg )* proofCmdSuffix + ; + +proofCmdSuffix: + SEMI | BY ( proofCmd | LBRACE (proofCmd+ | proofCmdCase+) RBRACE ) + ; + +proofCmdCase: + CASE ( label=STRING_LITERAL )? COLON ( proofCmd )* + | DEFAULT COLON ( proofCmd )* + ; +proofArg: (argLabel=IDENT COLON)? expression; +// --- + mergeparamsspec: MERGE_PARAMS LBRACE diff --git a/key.core/src/main/antlr4/KeYLexer.g4 b/key.core/src/main/antlr4/KeYLexer.g4 index 544c9371a42..7b632d198cd 100644 --- a/key.core/src/main/antlr4/KeYLexer.g4 +++ b/key.core/src/main/antlr4/KeYLexer.g4 @@ -40,6 +40,7 @@ lexer grammar KeYLexer; } private Token tokenBackStorage = null; + // see: https://keyproject.github.io/key-docs/devel/NewKeyParser/#why-does-the-lexer-required-some-pieces-of-java-code @Override public void emit(Token token) { int MAX_K = 10; @@ -219,6 +220,7 @@ AXIOMS : '\\axioms'; PROBLEM : '\\problem'; CHOOSECONTRACT : '\\chooseContract'; PROOFOBLIGATION : '\\proofObligation'; +// for PROOF see: https://keyproject.github.io/key-docs/devel/NewKeyParser/#why-does-the-lexer-required-some-pieces-of-java-code PROOF : '\\proof'; PROOFSCRIPT : '\\proofScript'; CONTRACTS : '\\contracts'; @@ -389,6 +391,7 @@ LESSEQUAL: '<' '=' | '\u2264'; LGUILLEMETS: '<' '<' | '«' | '‹'; RGUILLEMETS: '>''>' | '»' | '›'; IMPLICIT_IDENT: '<' '$'? (LETTER)+ '>' ('$lmtd')? -> type(IDENT); +MATCH_IDENT: '?' IDENT?; EQV: '<->' | '\u2194'; CHAR_LITERAL diff --git a/key.core/src/main/antlr4/KeYParser.g4 b/key.core/src/main/antlr4/KeYParser.g4 index f111efcedec..1424e262436 100644 --- a/key.core/src/main/antlr4/KeYParser.g4 +++ b/key.core/src/main/antlr4/KeYParser.g4 @@ -7,6 +7,7 @@ parser grammar KeYParser; @members { private SyntaxErrorReporter errorReporter = new SyntaxErrorReporter(getClass()); public SyntaxErrorReporter getErrorReporter() { return errorReporter;} +public boolean allowMatchId = false; // used in proof script parsing } options { tokenVocab=KeYLexer; } // use tokens from STLexer.g4 @@ -147,6 +148,7 @@ string_value: STRING_LITERAL; simple_ident : id=IDENT + | {allowMatchId}? id=MATCH_IDENT ; simple_ident_comma_list @@ -873,7 +875,7 @@ proofScriptExpression: | integer | floatnum | string_literal - | LPAREN (term | seq) RPAREN + | LPAREN {allowMatchId=true;} (term | seq) {allowMatchId=false;} RPAREN | simple_ident | abbreviation | literals diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/JavaInfo.java b/key.core/src/main/java/de/uka/ilkd/key/java/JavaInfo.java index 027dfd622e8..0bbeeb58f51 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/JavaInfo.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/JavaInfo.java @@ -18,6 +18,7 @@ import de.uka.ilkd.key.speclang.SpecificationElement; import de.uka.ilkd.key.util.Debug; +import org.jspecify.annotations.Nullable; import org.key_project.logic.Name; import org.key_project.logic.Namespace; import org.key_project.logic.sort.Sort; @@ -430,7 +431,7 @@ public static boolean isVisibleTo(SpecificationElement ax, KeYJavaType visibleTo /** * returns a KeYJavaType having the given sort */ - public KeYJavaType getKeYJavaType(Sort sort) { + public @Nullable KeYJavaType getKeYJavaType(Sort sort) { List l = lookupSort2KJTCache(sort); if (l != null && l.size() > 0) { // Return first KeYJavaType found for sort. diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/Recoder2KeYConverter.java b/key.core/src/main/java/de/uka/ilkd/key/java/Recoder2KeYConverter.java index 02cb218f130..b630c85b5bd 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/Recoder2KeYConverter.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/Recoder2KeYConverter.java @@ -964,7 +964,9 @@ public CatchAllStatement convert(de.uka.ilkd.key.java.recoderext.CatchAllStateme * @return the converted statement */ public JmlAssert convert(de.uka.ilkd.key.java.recoderext.JmlAssert ja) { - return new JmlAssert(ja.getKind(), ja.getCondition(), positionInfo(ja)); + return new JmlAssert(ja.getKind(), ja.getOptLabel(), ja.getCondition(), + ja.getAssertionProof(), + positionInfo(ja)); } // ------------------- declaration --------------------- diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/SourceElement.java b/key.core/src/main/java/de/uka/ilkd/key/java/SourceElement.java index 479a3069304..37fc0be5a50 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/SourceElement.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/SourceElement.java @@ -96,7 +96,8 @@ public interface SourceElement extends SyntaxElement, EqualsModProperty boolean equalsModProperty(Object o, Property property, V... v) { + default boolean equalsModProperty(Object o, Property property, + V... v) { if (!(o instanceof SourceElement)) { return false; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JMLTransformer.java b/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JMLTransformer.java index 6ea193b97d3..cb70582f565 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JMLTransformer.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JMLTransformer.java @@ -440,7 +440,8 @@ private void transformAssertStatement(TextualJMLAssertStatement stat, de.uka.ilkd.key.java.Position pos = ctx.getStartLocation().getPosition(); final Kind kind = stat.getKind(); - JmlAssert jmlAssert = new JmlAssert(kind, ctx); + JmlAssert jmlAssert = + new JmlAssert(kind, ctx, stat.getAssertionProof(), stat.getOptLabel()); try { updatePositionInformation(jmlAssert, pos); doAttach(jmlAssert, astParent, childIndex); diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JmlAssert.java b/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JmlAssert.java index bfbd83712f1..b4a7c948393 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JmlAssert.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/recoderext/JmlAssert.java @@ -6,6 +6,7 @@ import de.uka.ilkd.key.nparser.KeyAst; import de.uka.ilkd.key.speclang.jml.pretranslation.TextualJMLAssertStatement; +import org.jspecify.annotations.Nullable; import recoder.java.ProgramElement; import recoder.java.SourceVisitor; import recoder.java.Statement; @@ -23,6 +24,10 @@ public class JmlAssert extends JavaStatement { */ private final TextualJMLAssertStatement.Kind kind; + /** + * The optional proof for an assert statement (not for assume) + */ + private final KeyAst.@Nullable JMLProofScript assertionProof; /** * The condition of this statement in parse tree form @@ -31,12 +36,21 @@ public class JmlAssert extends JavaStatement { private final KeyAst.Expression condition; /** - * @param kind the kind of this statement - * @param condition the condition for this statement + * The optional label for this assertion (may be null) */ - public JmlAssert(TextualJMLAssertStatement.Kind kind, KeyAst.Expression condition) { + private final @Nullable String optLabel; + + public JmlAssert(TextualJMLAssertStatement.Kind kind, KeyAst.Expression condition, + String optLabel) { + this(kind, condition, null, optLabel); + } + + public JmlAssert(TextualJMLAssertStatement.Kind kind, KeyAst.Expression condition, + KeyAst.@Nullable JMLProofScript assertionProof, String optLabel) { this.kind = kind; this.condition = condition; + this.assertionProof = assertionProof; + this.optLabel = optLabel; } /** @@ -48,6 +62,8 @@ public JmlAssert(JmlAssert proto) { super(proto); this.kind = proto.kind; this.condition = proto.condition; + this.assertionProof = proto.assertionProof; + this.optLabel = proto.optLabel; } public TextualJMLAssertStatement.Kind getKind() { @@ -58,6 +74,10 @@ public KeyAst.Expression getCondition() { return condition; } + public KeyAst.@Nullable JMLProofScript getAssertionProof() { + return assertionProof; + } + @Override public int getChildCount() { return 0; @@ -87,4 +107,8 @@ public void accept(SourceVisitor sourceVisitor) { public Statement deepClone() { return new JmlAssert(this); } + + public String getOptLabel() { + return optLabel; + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/statement/JmlAssert.java b/key.core/src/main/java/de/uka/ilkd/key/java/statement/JmlAssert.java index 732174aa2fd..1d594566a5e 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/statement/JmlAssert.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/statement/JmlAssert.java @@ -8,10 +8,17 @@ import de.uka.ilkd.key.java.PositionInfo; import de.uka.ilkd.key.java.ProgramElement; import de.uka.ilkd.key.java.visitor.Visitor; +import de.uka.ilkd.key.logic.op.LocationVariable; import de.uka.ilkd.key.nparser.KeyAst; import de.uka.ilkd.key.speclang.jml.pretranslation.TextualJMLAssertStatement; +import de.uka.ilkd.key.speclang.njml.JmlIO; +import de.uka.ilkd.key.speclang.njml.JmlParser; import org.key_project.util.ExtList; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * A JML assert statement. @@ -29,21 +36,36 @@ public class JmlAssert extends JavaStatement { */ private final TextualJMLAssertStatement.Kind kind; + /* + * Temporary solution until full jml labels are there ... + * (To be clarified if compatible still) + */ + private final String optLabel; + /** * the condition in parse tree form */ - private KeyAst.Expression condition; + private final KeyAst.Expression condition; + + /** + * the assertion proof in parse tree form + */ + private final KeyAst.@Nullable JMLProofScript assertionProof; /** * @param kind assert or assume * @param condition the condition of this statement + * @param assertionProof the optional proof for an assert statement (not for assume) * @param positionInfo the position information for this statement */ - public JmlAssert(TextualJMLAssertStatement.Kind kind, KeyAst.Expression condition, + public JmlAssert(TextualJMLAssertStatement.Kind kind, String label, KeyAst.Expression condition, + KeyAst.@Nullable JMLProofScript assertionProof, PositionInfo positionInfo) { super(positionInfo); this.kind = kind; + this.optLabel = label; this.condition = condition; + this.assertionProof = assertionProof; } /** @@ -52,11 +74,15 @@ public JmlAssert(TextualJMLAssertStatement.Kind kind, KeyAst.Expression conditio public JmlAssert(ExtList children) { super(children); this.kind = Objects.requireNonNull(children.get(TextualJMLAssertStatement.Kind.class)); + this.optLabel = children.get(String.class); this.condition = Objects.requireNonNull(children.get(KeyAst.Expression.class)); + // script may be null + this.assertionProof = children.get(KeyAst.JMLProofScript.class); } public JmlAssert(JmlAssert other) { - this(other.kind, other.condition, other.getPositionInfo()); + this(other.kind, other.optLabel, other.condition, other.assertionProof, + other.getPositionInfo()); } public TextualJMLAssertStatement.Kind getKind() { @@ -148,6 +174,10 @@ protected int computeHashCode() { return System.identityHashCode(this); } + public KeyAst.@Nullable JMLProofScript getAssertionProof() { + return assertionProof; + } + @Override public int getChildCount() { return 0; @@ -162,4 +192,30 @@ public ProgramElement getChildAt(int index) { public void visit(Visitor v) { v.performActionOnJmlAssert(this); } + + /** + * This method collects all terms contained in this assertion. This is at least the condition. + * If there is a proof script, all terms in the proof script are collected as well. + * + * @return a freshly created list of at least one term + */ + public @NonNull ImmutableList collectTerms() { + ImmutableList result = ImmutableList.of(); + if (assertionProof != null) { + result = result.prepend(assertionProof.collectTerms()); + } + result = result.prepend(condition.ctx); + return result; + } + + public ImmutableList collectVariablesInProof(JmlIO io) { + if (assertionProof != null) { + return assertionProof.getObtainedProgramVars(io); + } + return ImmutableList.of(); + } + + public String getOptLabel() { + return optLabel; + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/java/visitor/CreatingASTVisitor.java b/key.core/src/main/java/de/uka/ilkd/key/java/visitor/CreatingASTVisitor.java index 7a0e26f1fca..a37b126bf4e 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/java/visitor/CreatingASTVisitor.java +++ b/key.core/src/main/java/de/uka/ilkd/key/java/visitor/CreatingASTVisitor.java @@ -1513,6 +1513,8 @@ public void performActionOnJmlAssert(JmlAssert x) { ProgramElement createNewElement(ExtList changeList) { changeList.add(x.getKind()); changeList.add(x.getCondition()); + changeList.add(x.getAssertionProof()); + changeList.add(x.getOptLabel()); return new JmlAssert(changeList); } }; diff --git a/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java b/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java index cd06367349d..32429104bdb 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java +++ b/key.core/src/main/java/de/uka/ilkd/key/logic/TermImpl.java @@ -349,7 +349,7 @@ protected int computeHashCode() { } @Override - public boolean equalsModProperty(Object o, Property property, V... v) { + public boolean equalsModProperty(Object o, Property property, V... v) { if (!(o instanceof JTerm other)) { return false; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/logic/equality/EqualsModProperty.java b/key.core/src/main/java/de/uka/ilkd/key/logic/equality/EqualsModProperty.java index bf5228374b8..2bda6af7c12 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/logic/equality/EqualsModProperty.java +++ b/key.core/src/main/java/de/uka/ilkd/key/logic/equality/EqualsModProperty.java @@ -49,7 +49,7 @@ public interface EqualsModProperty { * @param the type of the additional parameters needed by {@code property} for the * comparison */ - boolean equalsModProperty(Object o, Property property, V... v); + boolean equalsModProperty(Object o, Property property, V... v); /** * Computes the hash code according to the given ignored {@code property}. diff --git a/key.core/src/main/java/de/uka/ilkd/key/macros/ApplyScriptsMacro.java b/key.core/src/main/java/de/uka/ilkd/key/macros/ApplyScriptsMacro.java new file mode 100644 index 00000000000..d1ee6f6148c --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/macros/ApplyScriptsMacro.java @@ -0,0 +1,382 @@ +/* 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.macros; + +import java.util.*; +import java.util.ArrayList; +import java.util.stream.Collectors; + +import de.uka.ilkd.key.control.AbstractUserInterfaceControl; +import de.uka.ilkd.key.control.UserInterfaceControl; +import de.uka.ilkd.key.java.JavaTools; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.java.SourceElement; +import de.uka.ilkd.key.java.statement.JmlAssert; +import de.uka.ilkd.key.java.statement.MethodFrame; +import de.uka.ilkd.key.logic.DefaultVisitor; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.JavaBlock; +import de.uka.ilkd.key.logic.op.*; +import de.uka.ilkd.key.nparser.KeyAst; +import de.uka.ilkd.key.parser.Location; +import de.uka.ilkd.key.proof.*; +import de.uka.ilkd.key.proof.mgt.SpecificationRepository; +import de.uka.ilkd.key.prover.impl.DefaultTaskStartedInfo; +import de.uka.ilkd.key.rule.JmlAssertBuiltInRuleApp; +import de.uka.ilkd.key.scripts.ProofScriptEngine; +import de.uka.ilkd.key.scripts.ScriptCommandAst; +import de.uka.ilkd.key.scripts.ScriptException; +import de.uka.ilkd.key.scripts.TermWithHoles; +import de.uka.ilkd.key.speclang.njml.JmlLexer; +import de.uka.ilkd.key.speclang.njml.JmlParser; +import de.uka.ilkd.key.speclang.njml.JmlParser.ProofArgContext; +import de.uka.ilkd.key.speclang.njml.JmlParser.ProofCmdCaseContext; +import de.uka.ilkd.key.speclang.njml.JmlParser.ProofCmdContext; + +import de.uka.ilkd.key.util.MiscTools; +import org.key_project.logic.Term; +import org.key_project.logic.op.Modality; +import org.key_project.prover.engine.ProverTaskListener; +import org.key_project.prover.engine.TaskStartedInfo; +import org.key_project.prover.rules.RuleApp; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.java.StringUtil; + +import org.antlr.v4.runtime.ParserRuleContext; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ApplyScriptsMacro extends AbstractProofMacro { + + private static final Logger LOGGER = LoggerFactory.getLogger(ApplyScriptsMacro.class); + + private final @Nullable ProofMacro fallBackMacro; + + public ApplyScriptsMacro(ProofMacro fallBackMacro) { + this.fallBackMacro = fallBackMacro; + } + + @Override + public String getName() { + return "Apply scripts macro"; + } + + @Override + public String getCategory() { + return null; + } + + @Override + public String getDescription() { + return "Apply scripts"; + } + + @Override + public boolean canApplyTo(Proof proof, ImmutableList<@NonNull Goal> goals, + PosInOccurrence posInOcc) { + return fallBackMacro != null && fallBackMacro.canApplyTo(proof, goals, posInOcc) + || goals.exists(g -> getJmlAssert(g.node()) != null); + } + + record ObtainAwareTerm(JTerm term) { + JTerm resolve(Map obtainMap, Services services) { + OpReplacer pvr = new OpReplacer(obtainMap, services.getTermFactory()); + JTerm result = pvr.replace(term); + assertNoObtainVarsLeft(result, obtainMap); + return result; + } + + private void assertNoObtainVarsLeft(JTerm term, Map obtainMap) { + var v = new DefaultVisitor() { + @Override + public void visit(Term visited) { + if(obtainMap.containsKey(term.op())) { + throw new RuntimeException("Use of obtain variable before it being obtained: " + term.op()); + } + } + }; + term.execPreOrder(v); + } + } + + private static JmlAssert getJmlAssert(Node node) { + RuleApp ruleApp = node.parent().getAppliedRuleApp(); + if (ruleApp instanceof JmlAssertBuiltInRuleApp) { + JTerm target = (JTerm) ruleApp.posInOccurrence().subTerm(); + if (target.op() instanceof UpdateApplication) { + target = UpdateApplication.getTarget(target); + } + final SourceElement activeStatement = JavaTools.getActiveStatement(target.javaBlock()); + if (activeStatement instanceof JmlAssert jmlAssert + && jmlAssert.getAssertionProof() != null) { + return jmlAssert; + } + } + return null; + } + + private static @Nullable OpReplacer getUpdateReplacer(Goal goal) { + RuleApp ruleApp = goal.node().parent().getAppliedRuleApp(); + Term appliedOn = ruleApp.posInOccurrence().subTerm(); + if (appliedOn.op() instanceof UpdateApplication) { + var update = UpdateApplication.getUpdate((JTerm) appliedOn); + Map updates = new HashMap<>(); + Services services = goal.proof().getServices(); + collectUpdates(update, updates, services); + return new OpReplacer(updates, services.getTermFactory()); + } + return null; + } + + private static void collectUpdates(JTerm update, Map updates, Services services) { + switch(update.op()) { + case ElementaryUpdate eu -> + updates.put(services.getTermBuilder().var((ProgramVariable) eu.lhs()), update.sub(0)); + + case UpdateJunctor uj-> { + collectUpdates(update.sub(0), updates, services); + collectUpdates(update.sub(1), updates, services); + } + + default -> + throw new IllegalStateException("Unexpected update operation: " + update.op().getClass()); + } + } + + private static JavaBlock getJavaBlock(Goal goal) { + RuleApp ruleApp = goal.node().parent().getAppliedRuleApp(); + JTerm appliedOn = (JTerm) ruleApp.posInOccurrence().subTerm(); + if (appliedOn.op() instanceof UpdateApplication) { + appliedOn = UpdateApplication.getTarget(appliedOn); + } + assert appliedOn.op() instanceof Modality; + return appliedOn.javaBlock(); + } + + @Override + public ProofMacroFinishedInfo applyTo(UserInterfaceControl uic, Proof proof, + ImmutableList goals, PosInOccurrence posInOcc, ProverTaskListener listener) + throws Exception { + ArrayList laterGoals = new ArrayList<>(goals.size()); + for (Goal goal : goals) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + + JmlAssert jmlAssert = getJmlAssert(goal.node()); + if (jmlAssert == null) { + laterGoals.add(goal); + continue; + } + + listener.taskStarted(new DefaultTaskStartedInfo(TaskStartedInfo.TaskKind.Other, + "Running attached script from goal " + goal.node().serialNr(), 0)); + + KeyAst.JMLProofScript proofScript = jmlAssert.getAssertionProof(); + Map termMap = getTermMap(jmlAssert, getJavaBlock(goal), proof.getServices()); + // We heavily rely on that variables have been computed before, otherwise this will raise an NPE. + Map obtainMap = makeObtainVarMap(jmlAssert.collectVariablesInProof(null)); + @Nullable OpReplacer updateReplacer = getUpdateReplacer(goal); + List renderedProof = + renderProof(proofScript, termMap, updateReplacer, proof.getServices()); + ProofScriptEngine pse = new ProofScriptEngine(proof); + pse.setInitiallySelectedGoal(goal); + pse.getStateMap().putUserData("jml.obtainVarMap", obtainMap); + pse.getStateMap().getValueInjector().addConverter(JTerm.class, ObtainAwareTerm.class, + oat -> oat.resolve(obtainMap, goal.proof().getServices())); + // TODO: Perhaps have holes also in JML? + pse.getStateMap().getValueInjector().addConverter(TermWithHoles.class, ObtainAwareTerm.class, + oat -> new TermWithHoles(oat.resolve(obtainMap, goal.proof().getServices()))); + pse.getStateMap().getValueInjector().addConverter(boolean.class, ObtainAwareTerm.class, + oat -> Boolean.parseBoolean(oat.term.toString())); + LOGGER.debug("---- Script"); + LOGGER.debug(renderedProof.stream().map(ScriptCommandAst::asCommandLine) + .collect(Collectors.joining("\n"))); + LOGGER.debug("---- End Script"); + + pse.execute((AbstractUserInterfaceControl) uic, renderedProof); + } + listener.taskStarted(new DefaultTaskStartedInfo(TaskStartedInfo.TaskKind.Other, + "Running fallback macro on the remaining goals", 0)); + for (Goal goal : laterGoals) { + if (Thread.interrupted()) { + throw new InterruptedException(); + } + if (fallBackMacro != null) { + fallBackMacro.applyTo(uic, proof, ImmutableList.of(goal), posInOcc, listener); + } + + } + + return new ProofMacroFinishedInfo(this, proof); + } + + + private Map getTermMap(JmlAssert jmlAssert, JavaBlock javaBlock, Services services) { + SpecificationRepository.@Nullable JmlStatementSpec jmlspec = + services.getSpecificationRepository().getStatementSpec(jmlAssert); + if (jmlspec == null) { + throw new IllegalStateException( + "No specification found for JML assert statement at " + jmlAssert); + } + ImmutableList terms = ImmutableList.of(); + for (int i = jmlspec.terms().size() - 1; i >= 1; i--) { + terms = terms.prepend(correctSelfVar(i, javaBlock, jmlspec, services)); + } + ImmutableList jmlExprs = jmlAssert.collectTerms().tail(); + Map result = new IdentityHashMap<>(); + assert terms.size() == jmlExprs.size(); + for (int i = 0; i < terms.size(); i++) { + result.put(jmlExprs.get(i), terms.get(i)); + } + return result; + } + + /** + * For some reason, the self variable in the spec is not the same as the self variable and needs to + * be corrected. + */ + private JTerm correctSelfVar(int index, JavaBlock javaBlock, SpecificationRepository.JmlStatementSpec spec, Services services) { + final MethodFrame frame = JavaTools.getInnermostMethodFrame(javaBlock, services); + final JTerm self = MiscTools.getSelfTerm(frame, services); + return spec.getTerm(services, self, index); + + } + + private Map makeObtainVarMap(ImmutableList locationVariables) { + HashMap result = new HashMap<>(); + for (LocationVariable lv : locationVariables) { + result.put(lv, null); + } + return result; + } + + private static List renderProof(KeyAst.JMLProofScript script, + Map termMap, @Nullable OpReplacer update, Services services) throws ScriptException { + List result = new ArrayList<>(); + // Push current settings onto the settings stack + result.add(new ScriptCommandAst("set", Map.of("stack", "push"), List.of())); + // Prepare by resolving the update + result.add(new ScriptCommandAst("oss", Map.of("recentOnly", true), List.of())); + for (ProofCmdContext proofCmdContext : script.ctx.proofCmd()) { + result.addAll(renderProofCmd(proofCmdContext, termMap, update, services)); + } + // Pop settings stack to restore old settings + result.add(new ScriptCommandAst("set", Map.of("stack", "pop"), List.of())); + return result; + } + + private static List renderProofCmd(ProofCmdContext ctx, + Map termMap, + @Nullable OpReplacer update, Services services) throws ScriptException { + List result = new ArrayList<>(); + + // Push the current branch context + result.add(new ScriptCommandAst("branches", Map.of(), List.of("push"))); + + // Compose the command itself + if(ctx.obtain != null) { + ScriptCommandAst command = renderObtainCommand(ctx, termMap, update, services); + result.add(command); + } else { + ScriptCommandAst command = renderRegularCommand(ctx, termMap, update, services); + result.add(command); + } + + // handle followup proofCmd if present + JmlParser.ProofCmdSuffixContext suffix = ctx.proofCmdSuffix(); + if(suffix != null) { + if (!suffix.proofCmd().isEmpty()) { + result.add(new ScriptCommandAst("branches", Map.of(), List.of("single"))); + for (ProofCmdContext proofCmdContext : suffix.proofCmd()) { + result.addAll(renderProofCmd(proofCmdContext, termMap, update, services)); + } + } + + // handle proofCmdCases if present + for (ProofCmdCaseContext pcase : suffix.proofCmdCase()) { + String label = StringUtil.stripQuotes(pcase.label.getText()); + result.add(new ScriptCommandAst("branches", Map.of("branch", label), + List.of("select"))); + for (ProofCmdContext proofCmdContext : pcase.proofCmd()) { + result.addAll(renderProofCmd(proofCmdContext, termMap, update, services)); + } + } + } + + // Pop the branch stack + result.add(new ScriptCommandAst("branches", Map.of(), List.of("pop"))); + + return result; + } + + private static ScriptCommandAst renderObtainCommand(ProofCmdContext ctx, Map termMap, + @Nullable OpReplacer update, Services services) throws ScriptException { + Map named = new HashMap<>(); + + String argName = switch(ctx.obtKind.getType()) { + case JmlLexer.SUCH_THAT -> "such_that"; + case JmlLexer.EQUAL_SINGLE -> "equals"; + case JmlLexer.FROM_GOAL -> "from_goal"; + default -> throw new ScriptException("Unknown obtain kind: " + ctx.obtKind.getText()); + }; + + named.put("var", ctx.var.getText()); + + if(ctx.expression() == null) { + named.put(argName, true); + } else { + JmlParser.ExpressionContext exp = ctx.expression(); + Object value; + if (isStringLiteral(exp)) { + value = StringUtil.stripQuotes(exp.getText()); + } else { + value = termMap.get(exp); + if (update != null) { + // Wrap in update application if an update is present + value = update.replace((JTerm) value); + } + } + named.put(argName, value); + } + + return new ScriptCommandAst("__obtain", named, List.of(), Location.fromToken(ctx.start)); + } + + private static @NonNull ScriptCommandAst renderRegularCommand(ProofCmdContext ctx, Map termMap, @Nullable OpReplacer update, Services services) { + Map named = new HashMap<>(); + List positional = new ArrayList<>(); + for (ProofArgContext argContext : ctx.proofArg()) { + Object value; + JmlParser.ExpressionContext exp = argContext.expression(); + if (isStringLiteral(exp)) { + value = StringUtil.stripQuotes(exp.getText()); + } else { + value = termMap.get(exp); + if (update != null) { + // Wrap in update application if an update is present + value = update.replace((JTerm) value); + } + } + if (value instanceof JTerm term) { + value = new ObtainAwareTerm(term); + } + if (argContext.argLabel != null) { + named.put(argContext.argLabel.getText(), value); + } else { + positional.add(value); + } + } + return new ScriptCommandAst(ctx.cmd.getText(), named, positional, + Location.fromToken(ctx.start)); + } + + + private static boolean isStringLiteral(JmlParser.ExpressionContext ctx) { + return ctx.start == ctx.stop && ctx.start.getType() == JmlParser.STRING_LITERAL; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwareAutoMacro.java b/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwareAutoMacro.java new file mode 100644 index 00000000000..5a9dca3ea20 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwareAutoMacro.java @@ -0,0 +1,112 @@ +/* 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 */ +//// This file is part of KeY - Integrated Deductive Software Design +//// +//// Copyright (C) 2001-2011 Universitaet Karlsruhe (TH), Germany +//// Universitaet Koblenz-Landau, Germany +//// Chalmers University of Technology, Sweden +//// Copyright (C) 2011-2014 Karlsruhe Institute of Technology, Germany +//// Technical University Darmstadt, Germany +//// Chalmers University of Technology, Sweden +//// +//// The KeY system is protected by the GNU General +//// Public License. See LICENSE.TXT for details. +//// +// +// package de.uka.ilkd.key.macros; +// +// import de.uka.ilkd.key.java.ProofCommandStatement; +// import de.uka.ilkd.key.logic.Name; +// import de.uka.ilkd.key.logic.PosInOccurrence; +// import de.uka.ilkd.key.proof.Goal; +// import de.uka.ilkd.key.proof.Proof; +// import de.uka.ilkd.key.rule.ProofCommandStatementRule; +// import de.uka.ilkd.key.rule.RuleApp; +// import de.uka.ilkd.key.speclang.njml.JmlParser.ProofCmdContext; +// import de.uka.ilkd.key.strategy.RuleAppCost; +// import de.uka.ilkd.key.strategy.RuleAppCostCollector; +// import de.uka.ilkd.key.strategy.Strategy; +// +// import java.util.IdentityHashMap; +// import java.util.Map; +// +// public class ScriptAwareAutoMacro extends StrategyProofMacro { +// +// public ScriptAwareAutoMacro() { super(); } +// +// private Map detectedProofs = new IdentityHashMap<>(); +// +// @Override +// public String getName() { +// return "Script-aware auto mode"; +// } +// +// @Override +// public String getCategory() { +// return "Auto Pilot"; +// } +// +// @Override +// public String getDescription() { +// return "TODO"; +// } +// +// public Map getDetectedProofs() { +// return detectedProofs; +// } +// +// @Override +// public String getScriptCommandName() { +// return "script-auto"; +// } +// +// private static final Name NAME = new Name("Script-aware filter strategy"); +// +// private class ScriptAwareStrategy implements Strategy { +// +// private final Strategy delegate; +// +// public ScriptAwareStrategy(Proof proof, PosInOccurrence posInOcc) { +// this.delegate = proof.getActiveStrategy(); +// } +// +// @Override +// public Name name() { +// return NAME; +// } +// +// @Override +// public boolean isApprovedApp(RuleApp app, PosInOccurrence pio, Goal goal) { +// if (detectedProofs.containsKey(goal)) { +// // we had found a command earlier. +// return false; +// } +// if (app.rule() instanceof ProofCommandStatementRule) { +// detectedProofs.put(goal, ProofCommandStatementRule.getCommand(pio)); +// } +// return delegate.isApprovedApp(app, pio, goal); +// } +// +// @Override +// public void instantiateApp(RuleApp app, PosInOccurrence pio, Goal goal, +// RuleAppCostCollector collector) { +// delegate.instantiateApp(app, pio, goal, collector); +// } +// +// @Override +// public boolean isStopAtFirstNonCloseableGoal() { +// return false; +// } +// +// @Override +// public RuleAppCost computeCost(RuleApp app, PosInOccurrence pos, Goal goal) { +// return delegate.computeCost(app, pos, goal); +// } +// } +// +// @Override +// protected Strategy createStrategy(Proof proof, PosInOccurrence posInOcc) { +// return new ScriptAwareStrategy(proof, posInOcc); +// } +// } diff --git a/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwareMacro.java b/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwareMacro.java new file mode 100644 index 00000000000..397118f2ca8 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwareMacro.java @@ -0,0 +1,64 @@ +/* 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 */ +// This file is part of KeY - Integrated Deductive Software Design +// +// Copyright (C) 2001-2011 Universitaet Karlsruhe (TH), Germany +// Universitaet Koblenz-Landau, Germany +// Chalmers University of Technology, Sweden +// Copyright (C) 2011-2014 Karlsruhe Institute of Technology, Germany +// Technical University Darmstadt, Germany +// Chalmers University of Technology, Sweden +// +// The KeY system is protected by the GNU General +// Public License. See LICENSE.TXT for details. +// + +package de.uka.ilkd.key.macros; + +/** + * This class captures a proof macro which is meant to fully automise KeY proof + * workflow if scripts are present in the JML code. + * + * It is experimental. + * + * It performs the following steps: + *
    + *
  1. Finish symbolic execution + *
  2. Apply macros + *
  3. Try to close provable goals + *
+ * + * @author mattias ulbrich + * @see ScriptAwarePrepMacro + */ +public class ScriptAwareMacro extends SequentialProofMacro { + + private final ProofMacro autoMacro = new SymbolicExecutionOnlyMacro(); // FinishSymbolicExecutionMacro(); + private final ApplyScriptsMacro applyMacro = new ApplyScriptsMacro(new TryCloseMacro()); + + @Override + public String getScriptCommandName() { + return "script-auto"; + } + + @Override + public String getName() { + return "Script-aware Auto"; + } + + @Override + public String getCategory() { + return "Auto Pilot"; + } + + @Override + public String getDescription() { + return "TODO"; + } + + @Override + protected ProofMacro[] createProofMacroArray() { + return new ProofMacro[] { autoMacro, applyMacro }; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwarePrepMacro.java b/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwarePrepMacro.java new file mode 100644 index 00000000000..d2e40deb16b --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/macros/ScriptAwarePrepMacro.java @@ -0,0 +1,64 @@ +/* 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 */ +// This file is part of KeY - Integrated Deductive Software Design +// +// Copyright (C) 2001-2011 Universitaet Karlsruhe (TH), Germany +// Universitaet Koblenz-Landau, Germany +// Chalmers University of Technology, Sweden +// Copyright (C) 2011-2014 Karlsruhe Institute of Technology, Germany +// Technical University Darmstadt, Germany +// Chalmers University of Technology, Sweden +// +// The KeY system is protected by the GNU General +// Public License. See LICENSE.TXT for details. +// + +package de.uka.ilkd.key.macros; + +/** + * This class captures a proof macro which is meant to automise KeY proof + * workflow if scripts are present in the JML code. + * + * It is experimental. + * + * It performs the following steps: + *
    + *
  1. Finish symbolic execution + *
  2. Apply macros + *
  3. It does not try to close provable goals + *
+ * + * @author mattias ulbrich + * @see ScriptAwareMacro + */ +public class ScriptAwarePrepMacro extends SequentialProofMacro { + + private final ProofMacro autoMacro = new SymbolicExecutionOnlyMacro(); // new FinishSymbolicExecutionMacro(); + private final ApplyScriptsMacro applyMacro = new ApplyScriptsMacro(null); + + @Override + public String getScriptCommandName() { + return "script-prep-auto"; + } + + @Override + public String getName() { + return "Script-aware Prep Auto"; + } + + @Override + public String getCategory() { + return "Auto Pilot"; + } + + @Override + public String getDescription() { + return "TODO"; + } + + @Override + protected ProofMacro[] createProofMacroArray() { + return new ProofMacro[] { autoMacro, applyMacro }; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.java b/key.core/src/main/java/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.java new file mode 100644 index 00000000000..52d33cf2ab2 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.java @@ -0,0 +1,193 @@ +/* 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.macros; + +import de.uka.ilkd.key.control.TermLabelVisibilityManager; +import de.uka.ilkd.key.logic.op.Junctor; +import de.uka.ilkd.key.logic.op.ObserverFunction; +import de.uka.ilkd.key.logic.op.UpdateApplication; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.*; +import de.uka.ilkd.key.strategy.Strategy; +import org.jspecify.annotations.NonNull; +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.Rule; +import org.key_project.prover.rules.RuleApp; +import org.key_project.prover.rules.RuleSet; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.prover.strategy.costbased.NumberRuleAppCost; +import org.key_project.util.Streams; +import org.key_project.util.java.StringUtil; + +import java.io.IOException; +import java.util.Arrays; +import java.util.List; +import java.util.Map; +import java.util.WeakHashMap; + +/** + * This macro is very restritive in which rules are allowed to be applied for symbolic + * execution. No reasoning rules should ever be applied. + * + * @author mattias ulbrich + */ +public class SymbolicExecutionOnlyMacro extends StrategyProofMacro { + + private static final List ADMITTED_RULES; + private static final List ADMITTED_RULE_SETS; + static { + try { + ADMITTED_RULES = Arrays.asList( + Streams.toString(SymbolicExecutionOnlyMacro.class.getResourceAsStream("SymbolicExecutionOnlyMacro.admittedRules.txt")).split("\n")); + ADMITTED_RULE_SETS = Arrays.asList( + Streams.toString(SymbolicExecutionOnlyMacro.class.getResourceAsStream("SymbolicExecutionOnlyMacro.admittedRuleSets.txt")).split("\n")); + } catch (IOException e) { + throw new RuntimeException("Failed to load admitted rules for symbolic execution macro.", e); + } + } + + @Override + public String getName() { + return "Symbolic Execution Only"; + } + + @Override + public String getCategory() { + return "Auto Pilot"; + } + + @Override + public String getScriptCommandName() { + return "symbex-only"; + } + + @Override + public String getDescription() { + return "Continue symbolic execution until no more modality is on the sequent."; + } + + @Override + protected Strategy<@NonNull Goal> createStrategy(Proof proof, + PosInOccurrence posInOcc) { + return new FilterSymbexStrategy(proof.getActiveStrategy()); + } + + public static boolean isAdmittedRule(RuleApp ruleApp) { + Rule rule = ruleApp.rule(); + String name = rule.name().toString(); + if (ADMITTED_RULES.contains(name)) { + return true; + } + + if (rule instanceof org.key_project.prover.rules.Taclet taclet) { + for (RuleSet rs : taclet.getRuleSets()) { + if (ADMITTED_RULE_SETS.contains(rs.name().toString())) { + return true; + } + } + } + + if("ifthenelse_split_for".equals(name)) { + Term iteTerm = ruleApp.posInOccurrence().subTerm(); + Term then = iteTerm.sub(1); + Term elze = iteTerm.sub(2); + if(isUpdatedModality(then) && isUpdatedModality(elze)) { + return true; + } + } + + // apply OSS to () calls. + if (rule instanceof OneStepSimplifier) { + PosInOccurrence pio = ruleApp.posInOccurrence(); + var target = pio.subTerm(); + if (isUpdatedModality(target)) { + return true; + } + } + + if(rule instanceof UseOperationContractRule || + rule instanceof JmlAssertRule || + rule instanceof WhileInvariantRule || + rule instanceof LoopScopeInvariantRule ) + return true; + + return false; + } + + /** + * return true if there is a boolean combination of updated modalities + */ + private static boolean isUpdatedModality(Term term) { + while(term.op() instanceof UpdateApplication) { + term = term.sub(1); + } + if(term.op() instanceof Modality) {; + return true; + } + if(term.op() == Junctor.IMP || term.op() == Junctor.AND) { + return term.subs().stream().allMatch(SymbolicExecutionOnlyMacro::isUpdatedModality); + } + return false; + } + + private static class FilterSymbexStrategy extends FilterStrategy { + + private static final Name NAME = new Name(FilterSymbexStrategy.class.getSimpleName()); + private final Map modalityCache = new WeakHashMap<>(); + + public FilterSymbexStrategy(Strategy<@NonNull Goal> delegate) { + super(delegate); + } + + @Override + public Name name() { + return NAME; + } + + @Override + public boolean isApprovedApp(RuleApp app, PosInOccurrence pio, Goal goal) { + if(!hasModality(goal) || isThrowNullBranch(goal)) { + return false; + } + return isAdmittedRule(app) && super.isApprovedApp(app, pio, goal); + } + + /// Special case: Avoid infinite recursion on the "throw null" branch of tryCatchThrow + /// by forbidding continuation on this branch. + private boolean isThrowNullBranch(Goal goal) { + return "Null reference in throw".equals(goal.node().getNodeInfo().getBranchLabel()); + } + + private boolean hasModality(Goal goal) { + return modalityCache.computeIfAbsent(goal.node().sequent(), this::hasModality); + } + + private boolean hasModality(Sequent seq) { + for (SequentFormula formula : seq.asList()) { + if (hasModality(formula.formula())) { + return true; + } + } + return false; + } + + private boolean hasModality(Term term) { + if(term.op() instanceof Modality) { + return true; + } + return term.subs().stream().anyMatch(this::hasModality); + } + + @Override + public boolean isStopAtFirstNonCloseableGoal() { + return false; + } + } + +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/macros/TryCloseSideBranchesMacro.java b/key.core/src/main/java/de/uka/ilkd/key/macros/TryCloseSideBranchesMacro.java new file mode 100644 index 00000000000..1b0dcd06674 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/macros/TryCloseSideBranchesMacro.java @@ -0,0 +1,77 @@ +/* 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.macros; + + +import de.uka.ilkd.key.control.UserInterfaceControl; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Node; +import de.uka.ilkd.key.proof.Proof; +import org.key_project.prover.engine.ProverTaskListener; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.util.collection.ImmutableList; + +public class TryCloseSideBranchesMacro extends TryCloseMacro { + + /** + * Instantiates a new try close macro. + * No changes to the max number of steps. + */ + public TryCloseSideBranchesMacro() { + super(-1); + } + + /** + * Instantiates a new try close macro. + * + * @param numberSteps + * the max number of steps. -1 means no change. + */ + public TryCloseSideBranchesMacro(int numberSteps) { + super(numberSteps); + } + + @Override + public String getName() { + return "Close Provable Goals Below (Only side branches)"; + } + + @Override + public String getScriptCommandName() { + return "tryclose-sidebranches"; + } + + @Override + public String getDescription() { + return "Closes closable goals, leave rest untouched (see settings AutoPrune). " + + "Applies only to supposedly easy side goals (null reference, index out of bounds) " + + "beneath the selected node."; + } + + @Override + public ProofMacroFinishedInfo applyTo(UserInterfaceControl uic, + Proof proof, + ImmutableList goals, + PosInOccurrence posInOcc, + ProverTaskListener listener) throws InterruptedException { + ImmutableList sideGoals = goals.filter(TryCloseSideBranchesMacro::isSideGoal); + + return super.applyTo(uic, proof, sideGoals, posInOcc, listener); + } + + private static boolean isSideGoal(Goal g) { + Node node = g.node(); + while (node != null && node.getNodeInfo() != null + && node.getNodeInfo().getBranchLabel() != null) { + String label = node.getNodeInfo().getBranchLabel(); + if (label.contains("Null Reference") + || label.contains("Index Out of Bounds")) { + return true; + } + node = node.parent(); + } + return false; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/KeyAst.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/KeyAst.java index e3e50418712..95d0cbf698f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/KeyAst.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/KeyAst.java @@ -11,6 +11,9 @@ import java.util.List; import de.uka.ilkd.key.java.Position; +import de.uka.ilkd.key.java.abstraction.KeYJavaType; +import de.uka.ilkd.key.logic.ProgramElementName; +import de.uka.ilkd.key.logic.op.LocationVariable; import de.uka.ilkd.key.nparser.builder.BuilderHelpers; import de.uka.ilkd.key.nparser.builder.ChoiceFinder; import de.uka.ilkd.key.nparser.builder.FindProblemInformation; @@ -21,8 +24,11 @@ import de.uka.ilkd.key.scripts.ScriptCommandAst; import de.uka.ilkd.key.settings.Configuration; import de.uka.ilkd.key.settings.ProofSettings; +import de.uka.ilkd.key.speclang.njml.JmlIO; import de.uka.ilkd.key.speclang.njml.JmlParser; +import de.uka.ilkd.key.speclang.njml.JmlParserBaseVisitor; +import org.key_project.util.collection.ImmutableList; import org.key_project.util.java.StringUtil; import org.antlr.v4.runtime.CharStream; @@ -47,7 +53,7 @@ */ public abstract class KeyAst { - final @NonNull T ctx; + public final @NonNull T ctx; protected KeyAst(@NonNull T ctx) { this.ctx = ctx; @@ -223,6 +229,72 @@ public Expression(JmlParser.@NonNull ExpressionContext ctx) { } } + public static class JMLProofScript extends KeyAst { + + private static class ObtainedVarsVisitor extends JmlParserBaseVisitor { + private ImmutableList collectedVars = ImmutableList.of(); + private final JmlIO io; + + private ObtainedVarsVisitor(JmlIO io) { + this.io = io; + } + + @Override + public Void visitProofCmd(JmlParser.ProofCmdContext ctx) { + if(ctx.obtain != null) { + KeYJavaType type = io.translateType(ctx.typespec()); + ProgramElementName name = new ProgramElementName(ctx.var.getText()); + collectedVars = collectedVars.prepend(new LocationVariable(name, type, true)); + } + return null; + } + } + + private static class TermCollectionVisitor extends JmlParserBaseVisitor { + private ImmutableList collectedTerms = ImmutableList.of(); + + @Override + public Void visitExpression(JmlParser.ExpressionContext ctx) { + collectedTerms = collectedTerms.prepend(ctx); + return null; + } + } + + private ImmutableList obtainedProgramVars; + + public JMLProofScript(JmlParser.@NonNull AssertionProofContext ctx) { + super(ctx); + } + + public static JMLProofScript fromContext(JmlParser.AssertionProofContext ctx) { + if (ctx == null) { + return null; + } else { + return new JMLProofScript(ctx); + } + } + + public ImmutableList getObtainedProgramVars(JmlIO io) { + if(obtainedProgramVars == null) { + var visitor = new ObtainedVarsVisitor(io); + ctx.accept(visitor); + obtainedProgramVars = visitor.collectedVars; + } + return obtainedProgramVars; + } + + /** + * returns a list of all term parse trees in this proof script. + * + * Todo: Consider caching the result if this is called very often. + */ + public @NonNull ImmutableList collectTerms() { + TermCollectionVisitor visitor = new TermCollectionVisitor(); + ctx.accept(visitor); + return visitor.collectedTerms.reverse(); + } + } + public static class Term extends KeyAst { Term(KeYParser.TermContext ctx) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java index 70881d2dfc6..2cbd1d5340b 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/ParsingFacade.java @@ -100,7 +100,7 @@ private static KeYParser createParser(TokenSource lexer) { } - private static KeYParser createParser(CharStream stream) { + public static KeYParser createParser(CharStream stream) { return createParser(createLexer(stream)); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/DefaultBuilder.java b/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/DefaultBuilder.java index 059f24f1696..65b6291f0f4 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/DefaultBuilder.java +++ b/key.core/src/main/java/de/uka/ilkd/key/nparser/builder/DefaultBuilder.java @@ -300,7 +300,7 @@ public Object visitSimple_ident_dots_comma_list( @Override public String visitSimple_ident(KeYParser.Simple_identContext ctx) { - return ctx.IDENT().getText(); + return ctx.id.getText(); } @Override diff --git a/key.core/src/main/java/de/uka/ilkd/key/parser/Location.java b/key.core/src/main/java/de/uka/ilkd/key/parser/Location.java index 4635d37f311..5f630035269 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/parser/Location.java +++ b/key.core/src/main/java/de/uka/ilkd/key/parser/Location.java @@ -11,6 +11,7 @@ import java.util.Optional; import de.uka.ilkd.key.java.Position; +import de.uka.ilkd.key.java.PositionInfo; import de.uka.ilkd.key.util.MiscTools; import org.antlr.v4.runtime.IntStream; @@ -68,6 +69,16 @@ public static Location fromToken(Token token) { public Position getPosition() { return position; } + public static Location fromPositionInfo(PositionInfo info) { + Optional uri = info.getURI(); + if (uri.isEmpty()) { + return UNDEFINED; + } else { + Position pos = info.getStartPosition(); + return new Location(uri.get(), pos); + } + } + /** * Internal string representation. Do not rely on format! */ diff --git a/key.core/src/main/java/de/uka/ilkd/key/pp/PrettyPrinter.java b/key.core/src/main/java/de/uka/ilkd/key/pp/PrettyPrinter.java index df7141b3eea..ca3fc3619d9 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/pp/PrettyPrinter.java +++ b/key.core/src/main/java/de/uka/ilkd/key/pp/PrettyPrinter.java @@ -1955,6 +1955,12 @@ public void performActionOnJmlAssert(JmlAssert jmlAssert) { layouter.print(text); } } + + if (jmlAssert.getAssertionProof() != null) { + // For now: Just say that there is a script. It can be seen in the source pane anyways. + layouter.print(" \\by ..."); + } + layouter.end(); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/NodeInfo.java b/key.core/src/main/java/de/uka/ilkd/key/proof/NodeInfo.java index c6f08143795..73db8410e5c 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/NodeInfo.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/NodeInfo.java @@ -26,6 +26,7 @@ import de.uka.ilkd.key.rule.Taclet; import de.uka.ilkd.key.rule.TacletApp; +import org.jspecify.annotations.Nullable; import org.key_project.logic.Name; import org.key_project.proof.LocationVariableTracker; import org.key_project.prover.rules.RuleApp; @@ -49,7 +50,7 @@ public class NodeInfo { /** firstStatement stripped of method frames */ private SourceElement activeStatement = null; - private String branchLabel = null; + private @Nullable String branchLabel = null; /** flag true if the first and active statement have been determined */ private boolean determinedFstAndActiveStatement = false; @@ -265,7 +266,7 @@ public SourceElement getActiveStatement() { * * @return branch label */ - public String getBranchLabel() { + public @Nullable String getBranchLabel() { return branchLabel; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/Proof.java b/key.core/src/main/java/de/uka/ilkd/key/proof/Proof.java index 06512119546..10e81f08614 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/Proof.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/Proof.java @@ -972,7 +972,7 @@ public boolean isOpenGoal(Node node) { * * @return the goal that belongs to the given node or null if the node is an inner one */ - public Goal getOpenGoal(@NonNull Node node) { + public @Nullable Goal getOpenGoal(@NonNull Node node) { for (final Goal result : openGoals) { if (result.node() == node) { return result; @@ -997,7 +997,7 @@ public boolean isClosedGoal(Node node) { * @return the closed goal that belongs to the given node or null if the node is an inner one or * an open goal */ - public Goal getClosedGoal(Node node) { + public @Nullable Goal getClosedGoal(Node node) { for (final Goal result : closedGoals) { if (result.node() == node) { return result; @@ -1006,6 +1006,20 @@ public Goal getClosedGoal(Node node) { return null; } + /** + * Get the goal (open or closed) belonging to the given node if it exists. + * + * @param node the Node where a corresponding goal is searched + * @return the goal that belongs to the given node or null if the node is an inner one + */ + public @Nullable Goal getGoal(Node node) { + Goal g = getOpenGoal(node); + if (g == null) { + g = getClosedGoal(node); + } + return g; + } + /** * returns the list of goals of the subtree starting with node. * diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/init/AbstractProfile.java b/key.core/src/main/java/de/uka/ilkd/key/proof/init/AbstractProfile.java index 83ac77ecdd7..eb1f56582c1 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/init/AbstractProfile.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/init/AbstractProfile.java @@ -61,6 +61,8 @@ protected AbstractProfile(String standardRuleFilename) { standardRules = new RuleCollection( RuleSourceFactory.fromDefaultLocation(standardRuleFilename), initBuiltInRules()); strategies = getStrategyFactories(); + // NPEs in tests revealed that strategies could contain null elements + assert !strategies.contains(null); this.supportedGCB = computeSupportedGoalChooserBuilder(); this.supportedGC = extractNames(supportedGCB); this.prototype = getDefaultGoalChooserBuilder(); 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 d91cbbd771c..c48012bbd11 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 @@ -48,7 +48,24 @@ public class JavaProfile extends AbstractProfile { public static JavaProfile defaultInstance; public static JavaProfile defaultInstancePermissions; - public static final StrategyFactory DEFAULT = new JavaCardDLStrategyFactory(); + /** + * The default strategy factory to be used if no other strategy factory is + * specified. + * + * Caution: This used to be constructed at class load time, but cyclic reference between + * clauses made the field be read while the class was not yet fully initialized leading to + * null pointer exceptions. So we now use lazy initialization. + * + * (solution suggested by AW) + */ + private static StrategyFactory DEFAULT; + + public static StrategyFactory getDefault() { + if (DEFAULT == null) { + DEFAULT = new JavaCardDLStrategyFactory(); + } + return DEFAULT; + } private boolean permissions = false; @@ -121,7 +138,7 @@ protected ImmutableList computeTermLabelConfiguration() @Override protected ImmutableSet getStrategyFactories() { ImmutableSet set = super.getStrategyFactories(); - set = set.add(DEFAULT); + set = set.add(getDefault()); return set; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/SpecificationRepository.java b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/SpecificationRepository.java index f907dd51fee..5f582d22416 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/SpecificationRepository.java +++ b/key.core/src/main/java/de/uka/ilkd/key/proof/mgt/SpecificationRepository.java @@ -173,6 +173,7 @@ private static Taclet getLimitedToUnlimitedTaclet(IObserverFunction limited, ImmutableSLList.nil(), unlimitedTerm)); tacletBuilder.setName( MiscTools.toValidTacletName("unlimit " + getUniqueNameForObserver(unlimited))); + //tacletBuilder.addRuleSet(new RuleSet(new Name("unlimitObserver"))); return tacletBuilder.getTaclet(); } @@ -1879,10 +1880,11 @@ public JmlStatementSpec addStatementSpec(Statement statement, JmlStatementSpec s * list of terms, in * an immutable fasion. Updates require to create instances. *

- * Note: There is a immutability hole in {@link ProgramVariableCollection} due to mutable + * Note: There is an immutability hole in {@link ProgramVariableCollection} due to mutable * {@link Map} *

- * For {@link de.uka.ilkd.key.java.statement.JmlAssert} this is the formula behind the assert. + * For {@link de.uka.ilkd.key.java.statement.JmlAssert} this is the formula behind the assert + * (Potientially also containing the formulas within the optional proof). * For {@link de.uka.ilkd.key.java.statement.SetStatement} this is the target and the value * terms. * You may want to use the index constant for accessing them: @@ -1909,7 +1911,7 @@ public JTerm term(int index) { } /** - * Retrieve a term with a update to the given {@code self} term. + * Retrieve a term with an update to the given {@code self} term. * * @param services the corresponding services instance * @param self a term which describes the {@code self} object aka. this on the current diff --git a/key.core/src/main/java/de/uka/ilkd/key/prover/impl/DepthFirstGoalChooser.java b/key.core/src/main/java/de/uka/ilkd/key/prover/impl/DepthFirstGoalChooser.java index d76de6e176b..8e11b4ebd22 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/prover/impl/DepthFirstGoalChooser.java +++ b/key.core/src/main/java/de/uka/ilkd/key/prover/impl/DepthFirstGoalChooser.java @@ -74,6 +74,9 @@ protected void updateGoalListHelp(Object node, ImmutableList newGoals) { nextGoals = ImmutableSLList.nil(); + // Only consider automatic goals + newGoals = newGoals.filter(Goal::isAutomatic); + // Remove "node" and goals contained within "newGoals" while (!selectedList.isEmpty()) { final @NonNull Goal goal = selectedList.head(); diff --git a/key.core/src/main/java/de/uka/ilkd/key/rule/JmlAssertRule.java b/key.core/src/main/java/de/uka/ilkd/key/rule/JmlAssertRule.java index 6c82cdcb85c..77f35d755bc 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/rule/JmlAssertRule.java +++ b/key.core/src/main/java/de/uka/ilkd/key/rule/JmlAssertRule.java @@ -15,6 +15,11 @@ import de.uka.ilkd.key.logic.op.Transformer; import de.uka.ilkd.key.logic.op.UpdateApplication; import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.calculus.JavaDLSequentKit; +import de.uka.ilkd.key.proof.mgt.SpecificationRepository; +import de.uka.ilkd.key.rule.inst.SVInstantiations; +import de.uka.ilkd.key.rule.tacletbuilder.AntecSuccTacletGoalTemplate; +import de.uka.ilkd.key.rule.tacletbuilder.NoFindTacletBuilder; import de.uka.ilkd.key.speclang.jml.pretranslation.TextualJMLAssertStatement.Kind; import de.uka.ilkd.key.util.MiscTools; @@ -23,6 +28,7 @@ import org.key_project.prover.rules.RuleAbortException; import org.key_project.prover.rules.RuleApp; import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; import org.key_project.prover.sequent.SequentFormula; import org.key_project.util.collection.ImmutableList; @@ -132,7 +138,8 @@ public IBuiltInRuleApp createApp(PosInOccurrence occurrence, TermServices servic final MethodFrame frame = JavaTools.getInnermostMethodFrame(target.javaBlock(), services); final JTerm self = MiscTools.getSelfTerm(frame, services); - final var spec = services.getSpecificationRepository().getStatementSpec(jmlAssert); + final SpecificationRepository.JmlStatementSpec spec = + services.getSpecificationRepository().getStatementSpec(jmlAssert); if (spec == null) { throw new RuleAbortException( @@ -146,6 +153,8 @@ public IBuiltInRuleApp createApp(PosInOccurrence occurrence, TermServices servic kind == Kind.ASSERT ? OriginTermLabel.SpecType.ASSERT : OriginTermLabel.SpecType.ASSUME)); + final String label = jmlAssert.getOptLabel(); + final ImmutableList result; if (kind == Kind.ASSERT) { result = goal.split(2); @@ -156,7 +165,7 @@ public IBuiltInRuleApp createApp(PosInOccurrence occurrence, TermServices servic throw new RuleAbortException( String.format("Unknown assertion type %s", jmlAssert.getKind())); } - setUpUsageGoal(result.head(), occurrence, update, target, condition, tb, services); + setUpUsageGoal(result.head(), label, occurrence, update, target, condition, tb, services); return result; } @@ -168,7 +177,7 @@ private void setUpValidityRule(Goal goal, goal.changeFormula(new SequentFormula(tb.apply(update, condition)), occurrence); } - private void setUpUsageGoal(Goal goal, PosInOccurrence occurrence, + private void setUpUsageGoal(Goal goal, String label, PosInOccurrence occurrence, JTerm update, JTerm target, JTerm condition, TermBuilder tb, Services services) { goal.setBranchLabel("Usage"); @@ -178,6 +187,15 @@ private void setUpUsageGoal(Goal goal, PosInOccurrence occurrence, tb.prog(((Modality) target.op()).kind(), javaBlock, target.sub(0), null))); goal.changeFormula(new SequentFormula(newTerm), occurrence); + if (label != null) { + NoFindTacletBuilder bld = new NoFindTacletBuilder(); + Sequent ante = JavaDLSequentKit.createAnteSequent( + ImmutableList.of(new SequentFormula(tb.apply(update, condition)))); + bld.addTacletGoalTemplate(new AntecSuccTacletGoalTemplate(ante, ImmutableList.of(), + JavaDLSequentKit.getInstance().getEmptySequent())); + bld.setName(new Name("recall_" + label)); + goal.addTaclet(bld.getNoFindTaclet(), SVInstantiations.EMPTY_SVINSTANTIATIONS, false); + } } @Override diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AbstractCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AbstractCommand.java index d0badbb8e88..dd622d79b27 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/AbstractCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AbstractCommand.java @@ -13,6 +13,7 @@ import de.uka.ilkd.key.scripts.meta.ArgumentsLifter; import de.uka.ilkd.key.scripts.meta.ProofScriptArgument; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -39,12 +40,17 @@ public abstract class AbstractCommand implements ProofScriptCommand { */ protected @Nullable String documentation = null; - protected final EngineState state() { + /** + * The state object of this engine. + */ + protected final @NonNull EngineState state() { return Objects.requireNonNull(state); } /** - * ... + * The POJO class of the parameter object, or null if this command does not take any parameters + * via + * a POJO. */ private final @Nullable Class parameterClazz; @@ -81,6 +87,8 @@ public final void execute(AbstractUserInterfaceControl uiControl, ScriptCommandA /// Executes the command logic with the given parameters `args`. /// + /// This is usually overridden by subclasses. + /// /// @param args an instance of the parameters /// @throws ScriptException if something happened during execution /// @throws InterruptedException if thread was interrupted during execution @@ -96,4 +104,8 @@ public String getDocumentation() { return Objects.requireNonNullElse(documentation, ""); } + @Override + public String getCategory() { + return ArgumentsLifter.extractCategory(getClass(), parameterClazz); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ActivateCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ActivateCommand.java index 4d04f87e091..244ce4bc9e9 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ActivateCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ActivateCommand.java @@ -5,6 +5,7 @@ import de.uka.ilkd.key.control.AbstractUserInterfaceControl; import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.scripts.meta.Documentation; /** * Command for re-activating the first open (not necessarily enabled) {@link Goal} after a "leave" @@ -13,17 +14,18 @@ * * @author Dominic Steinhoefel */ +@Documentation(category = "Control", value = """ + Reactivates the first open (not necessarily enabled) goal. + This can be useful after a 'leave' command to continue + working on a complicated proof where 'tryclose' should not + apply on certain branches temporarily, but where one still + wants to finish the proof.""") public class ActivateCommand extends NoArgumentCommand { @Override public String getName() { return "activate"; } - @Override - public String getDocumentation() { - return ""; - } - @Override public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst args, EngineState state) diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AdditionalRulesStrategy.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AdditionalRulesStrategy.java new file mode 100644 index 00000000000..39240fdfc1f --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AdditionalRulesStrategy.java @@ -0,0 +1,126 @@ +package de.uka.ilkd.key.scripts; + +import org.jspecify.annotations.Nullable; +import org.key_project.prover.rules.Rule; +import de.uka.ilkd.key.macros.FilterStrategy; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.strategy.Strategy; +import org.key_project.logic.Name; +import org.key_project.prover.rules.RuleApp; +import org.key_project.prover.rules.RuleSet; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.strategy.costbased.NumberRuleAppCost; +import org.key_project.prover.strategy.costbased.RuleAppCost; +import org.key_project.prover.strategy.costbased.TopRuleAppCost; +import org.key_project.util.collection.Pair; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +class AdditionalRulesStrategy extends FilterStrategy { + /** Name of that strategy */ + private static final Name NAME = new Name( + AdditionalRulesStrategy.class.getSimpleName()); + + private static final Map TRANSLATIONS = + Map.of("high", "-50", "medium", "1000", "low", "10000"); + private static final RuleAppCost DEFAULT_PRIORITY = NumberRuleAppCost.create(1000); + + private final List> additionalRules; + private final boolean exclusive; + + public AdditionalRulesStrategy(Strategy delegate, String additionalRules, boolean exclusive) { + super(delegate); + this.additionalRules = parseAddRules(additionalRules); + this.exclusive = exclusive; + } + + private List> parseAddRules(String additionalRules) { + List> result = new ArrayList<>(); + for (String entry : additionalRules.trim().split(" *, *")) { + String[] parts = entry.split(" *= *", 2); + RuleAppCost prio; + if (parts.length == 2) { + String prioStr = parts[1]; + if(prioStr.equals("off")) { + prio = TopRuleAppCost.INSTANCE; + } + prioStr = TRANSLATIONS.getOrDefault(prioStr, prioStr); + try { + prio = NumberRuleAppCost.create(Integer.parseInt(prioStr)); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid value for additional rule: " + parts[1]); + } + } else { + prio = DEFAULT_PRIORITY; + } + + result.add(new Pair<>(parts[0], prio)); + } + return result; + } + + @Override + public Name name() { + return NAME; + } + + @Override + public RuleAppCost computeCost(RuleApp app, PosInOccurrence pio, Goal goal) { + RuleAppCost localCost = computeLocalCost(app.rule()); + if (localCost != null) { + return localCost; + } + return super.computeCost(app, pio, goal); + } + + @Override + public boolean isApprovedApp(RuleApp app, PosInOccurrence pio, Goal goal) { + RuleAppCost localCost = computeLocalCost(app.rule()); + if (localCost != null) { + return true; + } + if (exclusive) { + return false; + } else { + return super.isApprovedApp(app, pio, goal); + } + } + + private @Nullable RuleAppCost computeLocalCost(Rule rule) { + String name = rule.name().toString(); + Optional cost = lookup(name); + if(cost.isPresent()) { + return cost.get(); + } + + if (rule instanceof Taclet taclet) { + for (RuleSet rs : taclet.getRuleSets()) { + String rname = rs.name().toString(); + cost = lookup(rname); + if(cost.isPresent()) { + return cost.get(); + } + } + } + + return null; + } + + private Optional lookup(String name) { + return additionalRules.stream() + .filter(nameAndPrio -> name.matches(nameAndPrio.first)) + .findFirst() + .map(p -> p.second); + } + + @Override + public boolean isStopAtFirstNonCloseableGoal() { + return false; + } + + +} \ No newline at end of file diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AllCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AllCommand.java index d185a3af2c0..aad15803e3f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/AllCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AllCommand.java @@ -7,8 +7,18 @@ import de.uka.ilkd.key.control.AbstractUserInterfaceControl; import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.scripts.meta.Documentation; import de.uka.ilkd.key.scripts.meta.ProofScriptArgument; +@Documentation(category = "Control", value = """ + Executes a given block of script commands on all open goals. + The current goal is set to each open goal in turn while executing the block. + It expects exactly one positional argument, which is the block to be executed on each goal. + + #### Examples: + * `onAll { smt solver="z3"; }` + * `onAll { auto; }` + """) public class AllCommand implements ProofScriptCommand { @Override public List getArguments() { @@ -40,13 +50,4 @@ public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst arg public String getName() { return "onAll"; } - - /** - * {@inheritDoc} - */ - @Override - public String getDocumentation() { - return """ - Applies the given command to all the open goals."""; - } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AssertCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AssertCommand.java old mode 100755 new mode 100644 index 287f892cb49..8e5d9ba5e40 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/AssertCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AssertCommand.java @@ -1,64 +1,35 @@ /* This file is part of KeY - https://key-project.org * KeY is licensed under the GNU General Public License Version 2 * SPDX-License-Identifier: GPL-2.0-only */ -package de.uka.ilkd.key.scripts; - - -import de.uka.ilkd.key.scripts.meta.Documentation; -import de.uka.ilkd.key.scripts.meta.Option; - -/** - * Halts the script if some condition is not met. - *

- * See exported documentation at {@link Parameters} at the end of this file. - * - * @author lanzinger - */ -public class AssertCommand extends AbstractCommand { - - /** - * Instantiates a new assert command. - */ - public AssertCommand() { - super(Parameters.class); - } - - @Override - public void execute(ScriptCommandAst arguments) throws ScriptException, InterruptedException { - var args = state().getValueInjector().inject(new Parameters(), arguments); - - if (args.goals == null) { - throw new ScriptException("No parameter specified!"); - } - - if (state().getProof().openEnabledGoals().size() != args.goals) { - throw new ScriptException("Assertion failed: number of open goals is " - + state().getProof().openGoals().size() + ", but should be " + args.goals); - } - } - - @Override - public String getName() { - return "assert"; - } - - /** - * The Assigned parameters (currently only the passed goals). - */ - @Documentation(""" - The assert command checks if the number of open and enabled goals is equal to the given number. - If not, the script is halted with an error message. - - Deprecated: This command is deprecated and should not be used in new scripts. - The name of this command is likely to change since "assert" will - be used for a more general purpose. You may find that this is called - "failUnless". - """) - public static class Parameters { - /** - * The number of open and enabled goals. - */ - @Option("goals") - public Integer goals; - } -} +/// * 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.scripts; +// +/// ** +// * An assertion which essentially performs a cut. +// * +// * The only difference is that this implementation tampers with the labels of the resulting goals +/// to +// * allow them to be +// * better recognized in the script engine. +// * +// * (Unlike in other systems, in KeY the assertion does not remove the original goal formula since +// * that is not well-defined in sequent calculus.) +// */ +// public class AssertCommand extends CutCommand { +// +// @Override +// public String getName() { +// return "assert"; +// } +// +// @Override +// public void execute(ScriptCommandAst arguments) throws ScriptException, InterruptedException { +// var args = state().getValueInjector().inject(new Parameters(), arguments); +// var node = state().getFirstOpenAutomaticGoal().node(); +// execute(state(), args); +// // node.proof().getGoal(node.child(0)).setBranchLabel("Validity"); +// // node.proof().getGoal(node.child(1)).setBranchLabel("Usage"); +// } +// } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AssertOpenGoalsCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AssertOpenGoalsCommand.java new file mode 100755 index 00000000000..f4aa0ff05ab --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AssertOpenGoalsCommand.java @@ -0,0 +1,61 @@ +/* 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.scripts; + + +import de.uka.ilkd.key.scripts.meta.Documentation; +import de.uka.ilkd.key.scripts.meta.Option; + +/** + * Halts the script if the expected number of open and enabled goals is not met. + *

+ * See exported documentation at {@link Parameters} at the end of this file. + * + * @author lanzinger + */ +public class AssertOpenGoalsCommand extends AbstractCommand { + + /** + * Instantiates a new assert command. + */ + public AssertOpenGoalsCommand() { + super(Parameters.class); + } + + @Override + public void execute(ScriptCommandAst arguments) throws ScriptException, InterruptedException { + var args = state().getValueInjector().inject(new Parameters(), arguments); + + if (args.goals == null) { + throw new ScriptException("No parameter specified!"); + } + + if (state().getProof().openEnabledGoals().size() != args.goals) { + throw new ScriptException("Assertion failed: number of open goals is " + + state().getProof().openGoals().size() + ", but should be " + args.goals); + } + } + + @Override + public String getName() { + return "assertOpenGoals"; + } + + /** + * The Assigned parameters (currently only the passed goals). + */ + @Documentation(category = "Control", value = """ + The assert command checks if the number of open and enabled goals is equal to the given number. + If not, the script is halted with an error message. + + Note: This command was called "assert" originally. + """) + public static class Parameters { + /** + * The number of open and enabled goals. + */ + @Option("goals") + public Integer goals; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AssumeCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AssumeCommand.java index 27e2d753301..c997292a3cb 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/AssumeCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AssumeCommand.java @@ -34,16 +34,6 @@ public String getName() { return "assume"; } - @Override - public String getDocumentation() { - return """ - The assume command is an unsound taclet rule and takes one argument: - - The command adds the formula passed as argument to the antecedent - a formula #2 to which the command is applied"""; - } - - public void execute(ScriptCommandAst arguments) throws ScriptException, InterruptedException { var parameter = state().getValueInjector() .inject(new FormulaParameter(), arguments); @@ -57,6 +47,10 @@ public void execute(ScriptCommandAst arguments) throws ScriptException, Interrup state.getFirstOpenAutomaticGoal().apply(app); } + @Documentation(category = "Control", value = """ + The assume command is an **unsound** taclet rule and adds a formula to the antecedent of the current goal + Can be used for debug and proof exploration purposes. + """) public static class FormulaParameter { @Argument @Documentation("The formula to be assumed.") diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AutoCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AutoCommand.java index 5a92b99b744..c6199a2c88a 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/AutoCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AutoCommand.java @@ -12,10 +12,9 @@ import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.init.Profile; import de.uka.ilkd.key.prover.impl.ApplyStrategy; -import de.uka.ilkd.key.scripts.meta.Documentation; -import de.uka.ilkd.key.scripts.meta.Flag; -import de.uka.ilkd.key.scripts.meta.Option; +import de.uka.ilkd.key.scripts.meta.*; import de.uka.ilkd.key.strategy.FocussedBreakpointRuleApplicationManager; +import de.uka.ilkd.key.strategy.Strategy; import de.uka.ilkd.key.strategy.StrategyProperties; import org.key_project.prover.engine.ProverCore; @@ -48,11 +47,6 @@ public String getName() { return "auto"; } - @Override - public String getDocumentation() { - return "The AutoCommand invokes the automatic strategy \"Auto\""; - } - @Override public void execute(ScriptCommandAst args) throws ScriptException, InterruptedException { var arguments = state().getValueInjector().inject(new AutoCommand.Parameters(), args); @@ -69,7 +63,7 @@ public void execute(ScriptCommandAst args) throws ScriptException, InterruptedEx goals = state().getProof().openGoals(); } else { final Goal goal = state().getFirstOpenAutomaticGoal(); - goals = ImmutableSLList.nil().prepend(goal); + goals = ImmutableList.of(goal); if (arguments.matches != null || arguments.breakpoint != null) { setupFocussedBreakpointStrategy( // @@ -79,8 +73,8 @@ public void execute(ScriptCommandAst args) throws ScriptException, InterruptedEx // set the max number of steps if given int oldNumberOfSteps = state().getMaxAutomaticSteps(); - if (arguments.getSteps() > 0) { - state().setMaxAutomaticSteps(arguments.getSteps()); + if (arguments.maxSteps > 0) { + state().setMaxAutomaticSteps(arguments.maxSteps); } // set model search if given @@ -99,6 +93,14 @@ public void execute(ScriptCommandAst args) throws ScriptException, InterruptedEx SetCommand.updateStrategySettings(state(), activeStrategyProperties); + final Strategy originalStrategy = state.getProof().getActiveStrategy(); + if (arguments.additionalRules != null) { + state.getProof().setActiveStrategy(new AdditionalRulesStrategy(originalStrategy, arguments.additionalRules, false)); + } + if (arguments.onlyRules != null) { + state.getProof().setActiveStrategy(new AdditionalRulesStrategy(originalStrategy, arguments.onlyRules, true)); + } + // Give some feedback applyStrategy.addProverTaskObserver(uiControl); @@ -120,6 +122,7 @@ public void execute(ScriptCommandAst args) throws ScriptException, InterruptedEx activeStrategyProperties.setProperty(ov.settingName, ov.oldValue); } } + state.getProof().setActiveStrategy(originalStrategy); SetCommand.updateStrategySettings(state(), activeStrategyProperties); } } @@ -134,7 +137,7 @@ private Map prepareOriginalValues() { res.put("classAxioms", new OriginalValue(CLASS_AXIOM_OPTIONS_KEY, CLASS_AXIOM_FREE, CLASS_AXIOM_OFF)); res.put("dependencies", new OriginalValue(DEP_OPTIONS_KEY, DEP_ON, DEP_OFF)); - // ... add further (boolean for the moment) setings here. + // ... add further (boolean for the moment) settings here. return res; } @@ -168,50 +171,53 @@ private void setupFocussedBreakpointStrategy(final String maybeMatchesRegEx, new AbstractProofControl.FocussedAutoModeTaskListener(services.getProof())); } - @Documentation(""" - The AutoCommand is a command that invokes the automatic strategy "Auto" of KeY. - It can be used to automatically prove a goal or a set of goals. - Use with care, as this command may leave the proof state in an unpredictable state + @Documentation(category = "Fundamental", value =""" + The AutoCommand invokes the automatic strategy "Auto" of KeY (which is also launched by + when clicking the "Auto" button in the GUI). + It can be used to try to automatically prove the current goal. + Use with care, as this command may leave the proof in a incomprehensible state with many open goals. Use the command with "close" to make sure the command succeeds for fails without changes.""") - public static class Parameters { + public static class Parameters implements ValueInjector.VerifyableParameters { // @ TODO Deprecated with the higher order proof commands? @Flag(value = "all") - @Documentation("Apply the strategy on all open goals. There is a better syntax for that now.") + @Documentation("*Deprecated*. Apply the strategy on all open goals. There is a better syntax for that now.") public boolean onAllOpenGoals = false; @Option(value = "steps") - @Documentation("The maximum number of steps to be performed.") - public int maxSteps = -1; + @Documentation("The maximum number of proof steps to be performed.") + public @Nullable int maxSteps = -1; /** * Run on formula matching the given regex */ @Option(value = "matches") - @Documentation("Run on formula matching the given regex.") + @Documentation("Run on the formula matching the given regex.") public @Nullable String matches = null; /** * Run on formula matching the given regex */ @Option(value = "breakpoint") - @Documentation("Run on formula matching the given regex.") + @Documentation("When doing symbolic execution by auto, this option can be used to set a Java statement at which " + + "symbolic execution has to stop.") public @Nullable String breakpoint = null; @Flag(value = "modelsearch") - @Documentation("Enable model search. Better for some types of arithmetic problems. Sometimes a lot worse") + @Documentation("Enable model search. Better for some (types of) arithmetic problems. Sometimes a lot worse.") public boolean modelSearch; @Flag(value = "expandQueries") - @Documentation("Expand queries by modalities.") + @Documentation("Automatically expand occurrences of query symbols using additional modalities on the sequent.") public boolean expandQueries; @Flag(value = "classAxioms") @Documentation(""" - Enable class axioms. This expands model methods and fields and invariants quite eagerly. \ - May lead to divergence.""") + Enable automatic and eager expansion of symbols. This expands class invariants, model methods and + fields and invariants quite eagerly. May be an enabler (if a few definitions need to expanded), + may be a showstopper (if expansion increases the complexity on the sequent too much).""") public boolean classAxioms; @Flag(value = "dependencies") @@ -220,8 +226,30 @@ public static class Parameters { without that its definition is known. May be an enabler, may be a showstopper.""") public boolean dependencies; - public int getSteps() { - return maxSteps; + @Option(value = "add") + @Documentation(""" + Additional rules to be used by the auto strategy. The rules have to be given as a + comma-separated list of rule names and rule set names. Each entry can be assigned to a priority + (high, low, medium or a natural number) using an equals sign. + Cannot be combined with the 'only' parameter. + """) + public @Nullable String additionalRules; + + @Option(value = "only") + @Documentation(""" + Limit the rules to be used by the auto strategy. The rules have to be given as a + comma-separated list of rule names and rule set names. Each entry can be assigned to a priority + (high, low, medium or a natural number) using an equals sign. + All rules application which do not match the given names will be disabled. + Cannot be combined with the 'add' parameter. + """) + public @Nullable String onlyRules; + + @Override + public void verifyParameters() throws IllegalArgumentException, InjectionException { + if(onlyRules != null && additionalRules != null) { + throw new InjectionException("Parameters 'add' and 'only' are mutually exclusive."); + } } } @@ -229,7 +257,7 @@ private static final class OriginalValue { private final String settingName; private final String trueValue; private final String falseValue; - private String oldValue; + private @Nullable String oldValue; private OriginalValue(String settingName, String trueValue, String falseValue) { this.settingName = settingName; diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/AxiomCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/AxiomCommand.java index 182f94916ec..5da8270eb51 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/AxiomCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/AxiomCommand.java @@ -10,6 +10,7 @@ import de.uka.ilkd.key.rule.TacletApp; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.logic.Name; import org.key_project.logic.op.sv.SchemaVariable; @@ -25,6 +26,10 @@ * Use the {@link AssumeCommand} "assume" instead. */ @Deprecated(forRemoval = true) +@Documentation(""" + This command is deprecated and should not be used in new scripts. + Use the equivalent `assume` command instead. + """) public class AxiomCommand extends AssumeCommand { private static final Name TACLET_NAME = new Name("cut"); diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/BranchesCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/BranchesCommand.java new file mode 100644 index 00000000000..9d6c186b7b9 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/BranchesCommand.java @@ -0,0 +1,161 @@ +/* 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.scripts; + +import java.util.ArrayList; +import java.util.Deque; +import java.util.Iterator; +import java.util.LinkedList; +import java.util.List; +import java.util.Optional; +import java.util.Stack; + +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.rule.TacletApp; +import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Option; + +import org.key_project.prover.rules.tacletbuilder.TacletGoalTemplate; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.Nullable; + +public class BranchesCommand extends AbstractCommand { + + public BranchesCommand() { + super(Parameters.class); + } + + @Override + public String getName() { + return "branches"; + } + + @Override + public void execute(ScriptCommandAst arguments) throws ScriptException, InterruptedException { + var args = state().getValueInjector().inject(new BranchesCommand.Parameters(), arguments); + + Stack stack = (Stack) state.getUserData("_branchStack"); + if (stack == null) { + stack = new Stack<>(); + state.putUserData("_branchStack", stack); + } + + if (args.mode == null) { + throw new ScriptException("For 'branches', a mode must be specified"); + } + + switch (args.mode) { + case "push": + ensureSingleGoal(); + Node node = state.getFirstOpenAutomaticGoal().node(); + // this is the first goal. The parent is the decision point + stack.push(node.serialNr()); + break; + case "pop": + stack.pop(); + break; + case "select": + Node root = findNodeByNumber(proof, stack.peek()); + Goal goal; + if (args.branch == null) { + goal = findGoalByNode(state.getProof(), root.child(args.child)); + } else { + goal = findGoalByName(root, args.branch); + } + state.setGoal(goal); + break; + case "single": + root = findNodeByNumber(proof, stack.peek()); + TacletApp ta = (TacletApp) root.getAppliedRuleApp(); + ImmutableList templates = ta.taclet().goalTemplates(); + + int no = 0; + int found = -1; + for (TacletGoalTemplate template : templates) { + if (!"main".equals(template.tag())) { + if (found != -1) { + throw new ScriptException("More than one non-main goal found"); + } + found = no; + } + no++; + } + if (found == -1) { + throw new ScriptException("No single non-main goal found"); + } + + // For some reason, the child index is reversed between the node and the templates + found = templates.size() - 1 - found; + goal = findGoalByNode(proof, root.child(found)); + state.setGoal(goal); + break; + default: + throw new ScriptException( + "Unknown mode " + args.mode + " for the 'branches' command"); + } + } + + private void ensureSingleGoal() { + // state. + } + + private Goal findGoalByName(Node root, String branch) throws ScriptException { + Iterator it = root.childrenIterator(); + List knownBranchLabels = new ArrayList<>(); + int number = 1; + while (it.hasNext()) { + Node node = it.next(); + String label = node.getNodeInfo().getBranchLabel(); + if (label == null) { + label = "Case " + number; + } + knownBranchLabels.add(label); + if (branch.equals(label)) { + return findGoalByNode(root.proof(), node); + } + number ++; + } + throw new ScriptException( + "Unknown branch " + branch + ". Known branches are " + knownBranchLabels); + } + + private static Goal findGoalByNode(Proof proof, Node node) throws ScriptException { + Optional result = + proof.openEnabledGoals().stream().filter(g -> g.node() == node).findAny(); + if (result.isEmpty()) { + throw new ScriptException(); + } + return result.get(); + } + + private Node findNodeByNumber(Proof proof, int serial) throws ScriptException { + Deque todo = new LinkedList<>(); + todo.add(proof.root()); + while (!todo.isEmpty()) { + Node n = todo.remove(); + if (n.serialNr() == serial) { + return n; + } + Iterator it = n.childrenIterator(); + while (it.hasNext()) { + todo.add(it.next()); + } + } + throw new ScriptException(); + } + + public static class Parameters { + /** A formula defining the goal to select */ + @Argument + public String mode; + @Option(value = "branch") + public @Nullable String branch; + @Option(value = "child") + public @Nullable Integer child; + } + +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/CheatCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/CheatCommand.java new file mode 100644 index 00000000000..e3194efcb71 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/CheatCommand.java @@ -0,0 +1,54 @@ +/* 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.scripts; + +import de.uka.ilkd.key.control.AbstractUserInterfaceControl; +import de.uka.ilkd.key.proof.calculus.JavaDLSequentKit; +import de.uka.ilkd.key.rule.NoFindTaclet; +import de.uka.ilkd.key.rule.NoPosTacletApp; +import de.uka.ilkd.key.rule.Taclet; +import de.uka.ilkd.key.rule.TacletApp; + +import de.uka.ilkd.key.scripts.meta.Documentation; +import org.key_project.logic.ChoiceExpr; +import org.key_project.logic.Name; +import org.key_project.prover.rules.ApplicationRestriction; +import org.key_project.prover.rules.TacletApplPart; +import org.key_project.prover.rules.TacletAttributes; +import org.key_project.util.collection.DefaultImmutableMap; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.ImmutableSet; + +@Documentation(category = "Internal", value = """ + Use this to close a goal unconditionally. This is unsound and should only + be used for testing and proof debugging purposes. It is similar to 'sorry' + in Isabelle or 'admit' in Rocq. + """) +public class CheatCommand extends NoArgumentCommand { + private static final Taclet CHEAT_TACLET; + + static { + TacletApplPart applPart = + new TacletApplPart(JavaDLSequentKit.getInstance().getEmptySequent(), + new ApplicationRestriction(ApplicationRestriction.IN_SEQUENT_STATE), ImmutableList.of(), + ImmutableList.of(), ImmutableList.of(), ImmutableList.of()); + CHEAT_TACLET = + new NoFindTaclet(new Name("CHEAT"), applPart, ImmutableList.of(), ImmutableList.of(), + new TacletAttributes("cheat", null), DefaultImmutableMap.nilMap(), ChoiceExpr.TRUE, + ImmutableSet.empty()); + } + + @Override + public String getName() { + return "cheat"; + } + + @Override + public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst ast, + EngineState state) + throws ScriptException, InterruptedException { + TacletApp app = NoPosTacletApp.createNoPosTacletApp(CHEAT_TACLET); + state.getFirstOpenAutomaticGoal().apply(app); + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/CutCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/CutCommand.java index 454393b0fb8..cc89557ddcc 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/CutCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/CutCommand.java @@ -3,12 +3,15 @@ * SPDX-License-Identifier: GPL-2.0-only */ package de.uka.ilkd.key.scripts; +import java.util.List; + import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.rule.NoPosTacletApp; import de.uka.ilkd.key.rule.Taclet; import de.uka.ilkd.key.rule.TacletApp; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.logic.Name; import org.key_project.logic.op.sv.SchemaVariable; @@ -30,13 +33,10 @@ public String getName() { return "cut"; } + // From within JML scripts, "assert" is more common than "cut" @Override - public String getDocumentation() { - return """ - CutCommand has as script command name "cut" - - As parameters: - * a formula with the id "#2"""; + public List getAliases() { + return List.of(getName(), "assert"); } @Override @@ -51,13 +51,21 @@ static void execute(EngineState state, Parameters args) throws ScriptException { TacletApp app = NoPosTacletApp.createNoPosTacletApp(cut); SchemaVariable sv = app.uninstantiatedVars().iterator().next(); - app = app.addCheckedInstantiation(sv, args.formula, - state.getProof().getServices(), true); + var formula = state.getProof().getServices().getTermBuilder().convertToFormula(args.formula); + + app = app.addCheckedInstantiation(sv, formula, state.getProof().getServices(), true); state.getFirstOpenAutomaticGoal().apply(app); } + @Documentation(category = "Fundamental", value = """ + The cut command makes a case distinction (a cut) on a formula on the current proof goal. + From within JML scripts, the alias 'assert' is more common than using 'cut'. + If followed by a `\\by proof` suffix in JML, it refers the sequent where + the cut formula is introduced to the succedent (i.e. where it is to be established). + """) public static class Parameters { @Argument + @Documentation("The formula to make the case distinction on.") public @MonotonicNonNull JTerm formula; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/DependencyContractCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/DependencyContractCommand.java new file mode 100644 index 00000000000..587b7bf5799 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/DependencyContractCommand.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.scripts; + +import java.util.ArrayList; +import java.util.List; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.UseDependencyContractApp; +import de.uka.ilkd.key.scripts.meta.Documentation; +import de.uka.ilkd.key.scripts.meta.Option; + +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.key_project.logic.PosInTerm; +import org.key_project.logic.Term; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableArray; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.Nullable; + +/** + * The DependencyContractCommand applies a dependency contract to a selected formula in the current + * goal. See documentation of {@link Parameters} for more information. + */ +public class DependencyContractCommand extends AbstractCommand { + + public DependencyContractCommand() { + super(Parameters.class); + } + + @Override + public String getName() { + return "dependency"; + } + + @Override + public void execute(ScriptCommandAst command) throws ScriptException, InterruptedException { + + Parameters arguments = state().getValueInjector().inject(new Parameters(), command); + + final Goal goal = state.getFirstOpenAutomaticGoal(); + + if (arguments.heap == null) { + Services services = goal.proof().getServices(); + arguments.heap = services.getTermFactory() + .createTerm(services.getTypeConverter().getHeapLDT().getHeap()); + } + + List pios = find(arguments.on, goal.sequent()); + + if (pios.isEmpty()) { + throw new ScriptException("dependency contract not applicable."); + } else if (pios.size() > 1) { + throw new ScriptException("no unique application"); + } + + PosInOccurrence pio = pios.get(0); + ImmutableList builtins = goal.ruleAppIndex().getBuiltInRules(goal, pio); + for (IBuiltInRuleApp builtin : builtins) { + if (builtin instanceof UseDependencyContractApp) { + apply(goal, (UseDependencyContractApp) builtin, arguments); + } + } + + } + + private List find(JTerm term, Sequent sequent) { + List pios = new ArrayList<>(); + for (SequentFormula sf : sequent.antecedent()) { + PosInOccurrence pio = new PosInOccurrence(sf, PosInTerm.getTopLevel(), true); + find(pios, term, pio); + } + + for (SequentFormula sf : sequent.succedent()) { + PosInOccurrence pio = new PosInOccurrence(sf, PosInTerm.getTopLevel(), false); + find(pios, term, pio); + } + return pios; + } + + private void find(List pios, JTerm term, PosInOccurrence pio) { + Term subTerm = pio.subTerm(); + if (term.equals(subTerm)) { + pios.add(pio); + } else { + ImmutableArray subs = subTerm.subs(); + for (int i = 0; i < subs.size(); i++) { + find(pios, term, pio.down(i)); + } + } + } + + private void apply(Goal goal, UseDependencyContractApp ruleApp, Parameters arguments) { + JTerm on = arguments.on; + JTerm[] subs = on.subs().toArray(new JTerm[0]); + subs[0] = arguments.heap; + Services services = goal.proof().getServices(); + JTerm replaced = + services.getTermFactory().createTerm(on.op(), subs, on.boundVars(), on.getLabels()); + List pios = find(replaced, goal.sequent()); + ruleApp = ruleApp.setStep(pios.get(0)); + ruleApp = ruleApp.tryToInstantiateContract(services); + goal.apply(ruleApp); + } + + @Documentation(category = "Fundamental", value = """ + The dependency command applies a dependency contract to a specified term in the current goal. + Dependency contracts allow you to do modular reasoning. If for a heap-dependent function symbol, + no changes occur inside the dependency set of this function, the result remains the same. + This can be applied to model methods, model fields or invariants. + """) + public static class Parameters { + + @Documentation("The term to which the dependency contract should be applied. " + + "This term must occur in the current goal. " + + "And it must be the invocation of a heap-dependent observer function symbol.") + @Option(value = "on") + public @MonotonicNonNull JTerm on; + + @Documentation("The heap term to be compared against. If not given, the default heap is used.") + @Option(value = "heap") + public @Nullable JTerm heap; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/EchoCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/EchoCommand.java index 306a7d43c32..1095212d468 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/EchoCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/EchoCommand.java @@ -6,6 +6,7 @@ import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -32,11 +33,13 @@ public void execute(ScriptCommandAst args) } } + @Documentation(category = "Control", value = """ + A simple "print" command for giving progress feedback to the + human verfier during lengthy executions. + """) public static class Parameters { - /** - * The message to show. - */ @Argument + @Documentation("The message to be printed.") public @MonotonicNonNull String message; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/EngineState.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/EngineState.java index 848559a302e..e20878e6d78 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/EngineState.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/EngineState.java @@ -6,6 +6,7 @@ import java.nio.file.Path; import java.nio.file.Paths; import java.util.Deque; +import java.util.HashMap; import java.util.LinkedList; import java.util.Objects; import java.util.Optional; @@ -53,7 +54,6 @@ public class EngineState { private final AbbrevMap abbrevMap = new AbbrevMap(); private final ValueInjector valueInjector = createDefaultValueInjector(); - private final ExprEvaluator exprEvaluator = new ExprEvaluator(this); private @Nullable Consumer observer; private Path baseFileName = Paths.get("."); @@ -61,6 +61,8 @@ public class EngineState { private @Nullable Goal goal; private @Nullable Node lastSetGoalNode; + private final HashMap userData = new HashMap<>(); + /** * If set to true, outputs all commands to observers and console. Otherwise, only shows explicit * echo messages. @@ -78,61 +80,31 @@ public EngineState(Proof proof, ProofScriptEngine engine) { this.engine = engine; } + /// add converters for types used in proof scripts, + /// add support for parse trees private ValueInjector createDefaultValueInjector() { - var v = ValueInjector.createDefault(); + ValueInjector v = ValueInjector.createDefault(); + + // from string to ... v.addConverter(JTerm.class, String.class, (str) -> this.toTerm(str, null)); v.addConverter(Sequent.class, String.class, this::toSequent); v.addConverter(Sort.class, String.class, this::toSort); - addContextTranslator(v, String.class); - addContextTranslator(v, JTerm.class); - addContextTranslator(v, Integer.class); - addContextTranslator(v, Byte.class); - addContextTranslator(v, Long.class); - addContextTranslator(v, Boolean.class); - addContextTranslator(v, Character.class); - addContextTranslator(v, Sequent.class); - addContextTranslator(v, Integer.TYPE); - addContextTranslator(v, Byte.TYPE); - addContextTranslator(v, Long.TYPE); - addContextTranslator(v, Boolean.TYPE); - addContextTranslator(v, Character.TYPE); - addContextTranslator(v, JTerm.class); - addContextTranslator(v, Sequent.class); - addContextTranslator(v, Semisequent.class); - addContextTranslator(v, ScriptBlock.class); - return v; - } + // to terms with holes + v.addConverter(TermWithHoles.class, String.class, + str -> TermWithHoles.fromString(this, str)); - private void addContextTranslator(ValueInjector v, Class aClass) { - Converter converter = - (ProofScriptExpressionContext a) -> convertToString(v, aClass, a); - v.addConverter(aClass, ProofScriptExpressionContext.class, converter); + // to sequents with holes + v.addConverter(SequentWithHoles.class, String.class, + str -> SequentWithHoles.fromString(this, str)); + + // from KeY parse tree to everything + ExprEvaluator exprEvaluator = new ExprEvaluator(this); + exprEvaluator.addConvertersToValueInjector(v); + return v; } @SuppressWarnings("unchecked") - private R convertToString(ValueInjector inj, Class aClass, - ProofScriptExpressionContext ctx) - throws Exception { - try { - if (aClass == String.class && ctx.string_literal() != null) { - return inj.getConverter(aClass, String.class) - .convert(StringUtil.trim(ctx.string_literal().getText(), '"')); - } - if (aClass == String.class) { - return inj.getConverter(aClass, String.class).convert(ctx.getText()); - } - - T value = (T) ctx.accept(exprEvaluator); - Class tClass = (Class) value.getClass(); - if (aClass.isAssignableFrom(value.getClass())) { - return aClass.cast(value); - } - return inj.getConverter(aClass, tClass).convert(value); - } catch (ConversionException | NoSpecifiedConverterException e) { - return inj.getConverter(aClass, String.class).convert(ctx.getText()); - } - } protected static Goal getGoal(ImmutableList openGoals, Node node) { for (Goal goal : openGoals) { @@ -143,7 +115,7 @@ protected static Goal getGoal(ImmutableList openGoals, Node node) { return null; } - public void setGoal(Goal g) { + public void setGoal(@Nullable Goal g) { goal = g; lastSetGoalNode = Optional.ofNullable(g).map(Goal::node).orElse(null); } @@ -359,7 +331,11 @@ public NamespaceSet getCurrentNamespaces() { } } - public ExprEvaluator getEvaluator() { - return exprEvaluator; + public void putUserData(String key, @Nullable Object val) { + userData.put(key, val); + } + + public @Nullable Object getUserData(String key) { + return userData.get(key); } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ExitCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ExitCommand.java index a3c79c478f5..3b4fbf8a9d0 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ExitCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ExitCommand.java @@ -5,7 +5,12 @@ import de.uka.ilkd.key.control.AbstractUserInterfaceControl; +import de.uka.ilkd.key.scripts.meta.Documentation; +@Documentation(category = "Control", value = """ + Exits the currently running script context unconditionally. + (In the future, there may try-catch blocks to react to this). + """) public class ExitCommand extends NoArgumentCommand { @Override public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst args, @@ -18,9 +23,4 @@ public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst arg public String getName() { return "exit"; } - - @Override - public String getDocumentation() { - return "Kills the script execution."; - } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ExpandDefCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ExpandDefCommand.java new file mode 100644 index 00000000000..8eca2a44b61 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ExpandDefCommand.java @@ -0,0 +1,125 @@ +/* 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.scripts; + +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.equality.TermLabelsProperty; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.rule.PosTacletApp; +import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.scripts.meta.Option; + +import org.key_project.logic.PosInTerm; +import org.key_project.logic.Term; +import org.key_project.prover.proof.rulefilter.TacletFilter; +import org.key_project.prover.rules.Taclet; +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.Nullable; + +public class ExpandDefCommand extends AbstractCommand { + + private static final ExpansionFilter FILTER = new ExpansionFilter(); + + public ExpandDefCommand() { + super(Parameters.class); + } + + @Override + public String getName() { + return "expand"; + } + + @Override + public void execute(ScriptCommandAst command) throws ScriptException, InterruptedException { + var args = state().getValueInjector().inject(new Parameters(), command); + Goal g = state().getFirstOpenAutomaticGoal(); + TacletApp theApp = makeRuleApp(args, state()); + + ImmutableList completions = + theApp.findIfFormulaInstantiations(g.sequent(), g.proof().getServices()); + if (completions == null || completions.isEmpty()) { + throw new ScriptException("Cannot complete the rule app"); + } + + TacletApp app = completions.head(); + app = app.tryToInstantiate(g.proof().getServices().getOverlay(g.getLocalNamespaces())); + if (app == null || !app.complete()) { + throw new ScriptException("Cannot complete the rule app"); + } + + g.apply(app); + + } + + private TacletApp makeRuleApp(Parameters p, EngineState state) throws ScriptException { + + Goal g = state.getFirstOpenAutomaticGoal(); + Proof proof = state.getProof(); + + ImmutableList apps = ImmutableList.of(); + for (SequentFormula anteForm : g.sequent().antecedent()) { + apps = apps.prepend(g.ruleAppIndex().getTacletAppAtAndBelow(FILTER, + new PosInOccurrence(anteForm, PosInTerm.getTopLevel(), true), proof.getServices())); + } + + for (SequentFormula succForm : g.sequent().succedent()) { + apps = apps.prepend(g.ruleAppIndex().getTacletAppAtAndBelow(FILTER, + new PosInOccurrence(succForm, PosInTerm.getTopLevel(), false), + proof.getServices())); + } + + if (p.on != null) { + apps = apps.filter( + it -> it instanceof PosTacletApp && + p.on.matches(it.posInOccurrence())); + } else if (p.formula != null) { + apps = apps.filter( + it -> it instanceof PosTacletApp && + p.formula.matchesToplevel(it.posInOccurrence().sequentFormula())); + } else { + throw new ScriptException("Either 'formula' or 'on' must be specified"); + } + + if (apps.isEmpty()) { + throw new ScriptException("There is no expansion rule app that matches 'on'"); + } else if (p.occ != null && p.occ >= 0) { + if (p.occ >= apps.size()) { + throw new ScriptException( + "The 'occ' parameter is beyond the number of occurrences."); + } + return apps.get(p.occ); + } else { + if (apps.size() != 1) { + throw new ScriptException("The 'on' parameter is not unique"); + } + return apps.head(); + } + + } + + public static class Parameters { + @Option(value = "on") + public @Nullable TermWithHoles on; + @Option(value = "occ") + public @Nullable Integer occ; + @Option(value = "formula") + public @Nullable TermWithHoles formula; + + } + + private static class ExpansionFilter extends TacletFilter { + + @Override + protected boolean filter(Taclet taclet) { + String name = taclet.name().toString(); + return name.startsWith("Class_invariant_axiom_for") || + name.startsWith("Definition_axiom_for"); + } + } + +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ExprEvaluator.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ExprEvaluator.java index 4862f59dcad..9466c3edabb 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ExprEvaluator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ExprEvaluator.java @@ -5,15 +5,28 @@ import java.net.URI; +import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.nparser.KeYParser; import de.uka.ilkd.key.nparser.KeYParser.*; import de.uka.ilkd.key.nparser.KeYParserBaseVisitor; import de.uka.ilkd.key.nparser.KeyAst; import de.uka.ilkd.key.nparser.builder.ExpressionBuilder; +import de.uka.ilkd.key.proof.calculus.JavaDLSequentKit; +import de.uka.ilkd.key.scripts.meta.ConversionException; +import de.uka.ilkd.key.scripts.meta.Converter; +import de.uka.ilkd.key.scripts.meta.NoSpecifiedConverterException; +import de.uka.ilkd.key.scripts.meta.ValueInjector; +import de.uka.ilkd.key.util.ANTLRUtil; +import org.key_project.logic.sort.Sort; +import org.key_project.prover.sequent.Semisequent; import org.key_project.prover.sequent.Sequent; import org.antlr.v4.runtime.ParserRuleContext; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.prover.sequent.SequentKit; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.java.StringUtil; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -21,7 +34,6 @@ /// Evaluates expression inside of proof script to their appropriate type. /// -/// - [JmlParser.ExpressionContext]: [Term] /// - [SeqContext]: [Sequent] /// - [Boolean_literalContext]: [Boolean] /// - [IntegerContext]: [Integer] @@ -31,7 +43,7 @@ /// @author Alexander Weigl /// @version 1 (18.01.25) /// @see de.uka.ilkd.key.nparser.KeYParser.ProofScriptExpressionContext -class ExprEvaluator extends KeYParserBaseVisitor { +class ExprEvaluator { private static final Logger LOGGER = LoggerFactory.getLogger(ExprEvaluator.class); private final EngineState state; @@ -39,78 +51,102 @@ class ExprEvaluator extends KeYParserBaseVisitor { this.state = engineState; } - @Override - public Object visitProofScriptCodeBlock(ProofScriptCodeBlockContext ctx) { - URI uri = KeyAst.ProofScript.getUri(ctx.start); - return KeyAst.ProofScript.asAst(uri, ctx); + private Object evaluateExpression(ParserRuleContext ctx) { + var expressionBuilder = + new ExpressionBuilder(state.getProof().getServices(), state.getCurrentNamespaces()); + expressionBuilder.setAbbrevMap(state.getAbbreviations()); + var t = ctx.accept(expressionBuilder); + var warnings = expressionBuilder.getBuildingIssues(); + warnings.forEach(it -> LOGGER.warn("{}", it)); + warnings.clear(); + return t; } - @Override - public Object visitBoolean_literal(Boolean_literalContext ctx) { - return Boolean.parseBoolean(ctx.getText()); - } + public void addConvertersToValueInjector(ValueInjector v) { + v.addConverter(String.class, ProofScriptExpressionContext.class, this::convertToString); + v.addConverter(Boolean.class, ProofScriptExpressionContext.class, this::convertToBoolean); + v.addConverter(boolean.class, ProofScriptExpressionContext.class, this::convertToBoolean); + v.addConverter(Integer.class, ProofScriptExpressionContext.class, this::convertToInteger); + v.addConverter(int.class, ProofScriptExpressionContext.class, this::convertToInteger); + v.addConverter(JTerm.class, ProofScriptExpressionContext.class, this::convertToTerm); + v.addConverter(Sequent.class, ProofScriptExpressionContext.class, this::convertToSequent); + v.addConverter(TermWithHoles.class, ProofScriptExpressionContext.class, + ctx -> TermWithHoles.fromProofScriptExpression(state, ctx)); + v.addConverter(SequentWithHoles.class, ProofScriptExpressionContext.class, + ctx -> SequentWithHoles.fromParserContext(state, ctx)); + v.addConverter(ScriptBlock.class, ProofScriptExpressionContext.class, this::convertToScriptBlock); - @Override - public Object visitChar_literal(KeYParser.Char_literalContext ctx) { - return ctx.getText().charAt(1); // skip "'" } - @Override - public Object visitInteger(IntegerContext ctx) { - return Integer.parseInt(ctx.getText()); + private ScriptBlock convertToScriptBlock(ProofScriptExpressionContext ctx) throws ConversionException { + if(ctx.proofScriptCodeBlock() == null) { + throw new ConversionException("Need a script block here, not: " + ANTLRUtil.reconstructOriginal(ctx)); + } + URI uri = KeyAst.ProofScript.getUri(ctx.start); + return KeyAst.ProofScript.asAst(uri, ctx.proofScriptCodeBlock()); } - @Override - public Object visitFloatLiteral(KeYParser.FloatLiteralContext ctx) { - return Float.parseFloat(ctx.getText()); + private JTerm convertToTerm(ProofScriptExpressionContext ctx) throws ConversionException { + try { + if (ctx.string_literal() != null) { + String text = StringUtil.trim(ctx.string_literal().getText(), '"'); + return state.toTerm(text, null); + } else if (ctx.proofScriptCodeBlock() != null) { + throw new ConversionException("A block cannot be used as a term"); + } else { + return (JTerm) evaluateExpression((ParserRuleContext) ctx.getChild(0)); + } + } catch (Exception e) { + throw new ConversionException("Cannot convert expression to term: " + ANTLRUtil.reconstructOriginal(ctx)); + } } - @Override - public Object visitDoubleLiteral(DoubleLiteralContext ctx) { - return Double.parseDouble(ctx.getText()); + private Sequent convertToSequent(ProofScriptExpressionContext ctx) throws ConversionException { + try { + if (ctx.string_literal() != null) { + String text = StringUtil.trim(ctx.string_literal().getText(), '"'); + return state.toSequent(text); + } else if (ctx.proofScriptCodeBlock() != null) { + throw new ConversionException("A block cannot be used as a sequent"); + } else if (ctx.seq() != null) { + return (Sequent) evaluateExpression(ctx.seq()); + } else { + JTerm term = (JTerm) evaluateExpression((ParserRuleContext) ctx.getChild(0)); + return JavaDLSequentKit.createSuccSequent(ImmutableList.of(new SequentFormula(term))); + } + } catch (Exception e) { + throw new ConversionException("Cannot convert expression to sequent: " + ANTLRUtil.reconstructOriginal(ctx)); + } } - @Override - public String visitString_literal(String_literalContext ctx) { - return trim(ctx.getText(), '"'); + private Boolean convertToBoolean(ProofScriptExpressionContext ctx) throws ConversionException { + if (ctx.boolean_literal() != null) { + return Boolean.parseBoolean(ctx.boolean_literal().getText()); + } else if (ctx.string_literal() != null) { + String text = StringUtil.trim(ctx.string_literal().getText(), '"'); + return Boolean.parseBoolean(text); + } else { + throw new ConversionException("Cannot convert expression to boolean: " + ANTLRUtil.reconstructOriginal(ctx)); + } } - @Override - public Sequent visitSeq(SeqContext ctx) { - var expressionBuilder = - new ExpressionBuilder(state.getProof().getServices(), state.getCurrentNamespaces()); - expressionBuilder.setAbbrevMap(state.getAbbreviations()); - var t = (Sequent) ctx.accept(expressionBuilder); - var warnings = expressionBuilder.getBuildingIssues(); - warnings.forEach(it -> LOGGER.warn("{}", it)); - warnings.clear(); - return t; - - } - - @Override - public Object visitSimple_ident(Simple_identContext ctx) { - return evaluateExpression(ctx); + private Integer convertToInteger(ProofScriptExpressionContext proofScriptExpressionContext) throws ConversionException { + if (proofScriptExpressionContext.integer() != null) { + return Integer.parseInt(proofScriptExpressionContext.integer().getText()); + } else if (proofScriptExpressionContext.string_literal() != null) { + String text = StringUtil.trim(proofScriptExpressionContext.string_literal().getText(), '"'); + return Integer.parseInt(text); + } else { + throw new ConversionException("Cannot convert expression to integer: " + ANTLRUtil.reconstructOriginal(proofScriptExpressionContext)); + } } - @Override - public Object visitTerm(KeYParser.TermContext ctx) { - return evaluateExpression(ctx); + private String convertToString(ProofScriptExpressionContext ctx) { + if (ctx.string_literal() != null) { + return StringUtil.trim(ctx.string_literal().getText(), '"'); + } else { + return ANTLRUtil.reconstructOriginal(ctx).trim(); + } } - private Object evaluateExpression(ParserRuleContext ctx) { - var expressionBuilder = - new ExpressionBuilder(state.getProof().getServices(), state.getCurrentNamespaces()); - expressionBuilder.setAbbrevMap(state.getAbbreviations()); - var t = ctx.accept(expressionBuilder); - var warnings = expressionBuilder.getBuildingIssues(); - warnings.forEach(it -> LOGGER.warn("{}", it)); - warnings.clear(); - return t; - } - - @Override - protected Object aggregateResult(Object aggregate, Object nextResult) { - return nextResult == null ? aggregate : nextResult; - } -} +} \ No newline at end of file diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/FocusCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/FocusCommand.java index 6f9b1f724a2..0b12d0eaeda 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/FocusCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/FocusCommand.java @@ -15,6 +15,7 @@ import de.uka.ilkd.key.rule.inst.SVInstantiations; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.logic.Name; import org.key_project.logic.PosInTerm; import org.key_project.logic.Term; @@ -48,17 +49,31 @@ public FocusCommand() { super(Parameters.class); } + @Documentation(category = "Fundamental", value = """ + The command "focus" allows you to select formulas from the current sequent + to focus verification on. This means that all other formulas are discarded + (i.e. hidden using `hide_right`, `hide_left`). + + Benefits are: The automation is guided into focussing on a relevant set of + formulas. + + The selected set of sequent formulas can be regarded as an equivalent to a + believed "unsat core" of the sequent. + + #### Examples: + - `focus x > 2 ==> x > 1` only keeps the mentioned to formulas in the current goal + removing all other formulas that could distract the automation. + """) static class Parameters { @Argument - public @MonotonicNonNull Sequent toKeep; + @Documentation("The sequent containing the formulas to keep. It may contain placeholder symbols.") + public @MonotonicNonNull SequentWithHoles toKeep; } @Override public void execute(ScriptCommandAst args) throws ScriptException, InterruptedException { - var s = state().getValueInjector().inject(new Parameters(), args); - - Sequent toKeep = s.toKeep; - hideAll(toKeep); + Parameters s = state().getValueInjector().inject(new Parameters(), args); + hideAll(s.toKeep); } @Override @@ -72,38 +87,19 @@ public String getName() { * @param toKeep sequent containing formulas to keep * @throws ScriptException if no goal is currently open */ - private void hideAll(Sequent toKeep) throws ScriptException { + private void hideAll(SequentWithHoles toKeep) throws ScriptException { Goal goal = state.getFirstOpenAutomaticGoal(); assert goal != null : "not null by contract of the method"; - // The formulas to keep in the antecedent - ImmutableList keepAnte = toKeep.antecedent().asList() - .map(SequentFormula::formula); - ImmutableList ante = - goal.sequent().antecedent().asList(); - - for (SequentFormula seqFormula : ante) { - // This means "!keepAnte.contains(seqFormula.formula)" but with equality mod renaming! - if (!keepAnte.exists( - it -> { - Term formula = seqFormula.formula(); - return RENAMING_TERM_PROPERTY.equalsModThisProperty(it, formula); - })) { + for (SequentFormula seqFormula : goal.sequent().antecedent().asList()) { + if(!toKeep.containsAntecendent(seqFormula)) { Taclet tac = getHideTaclet("left"); makeTacletApp(goal, seqFormula, tac, true); } } - ImmutableList keepSucc = - toKeep.succedent().asList().map(SequentFormula::formula); - ImmutableList succ = - goal.sequent().succedent().asList(); - for (SequentFormula seqFormula : succ) { - if (!keepSucc.exists( - it -> { - Term formula = seqFormula.formula(); - return RENAMING_TERM_PROPERTY.equalsModThisProperty(it, formula); - })) { + for (SequentFormula seqFormula : goal.sequent().succedent().asList()) { + if(!toKeep.containsSuccedent(seqFormula)) { Taclet tac = getHideTaclet("right"); makeTacletApp(goal, seqFormula, tac, false); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/HideCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/HideCommand.java index 6f37e696469..77ef42f4302 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/HideCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/HideCommand.java @@ -11,6 +11,7 @@ import de.uka.ilkd.key.rule.TacletApp; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.logic.Name; import org.key_project.logic.PosInTerm; import org.key_project.logic.Term; @@ -25,7 +26,7 @@ import static de.uka.ilkd.key.logic.equality.TermLabelsProperty.TERM_LABELS_PROPERTY; /** - * Proof script command to hide a formula from the sequent. + * Proof script command to hide formulas from the sequent. * * Usage: * @@ -99,9 +100,14 @@ public String getName() { return "hide"; } + @Documentation(category = "Control", value = """ + The hide command hides all formulas of the current proof goal that are in the given sequent. + The formulas in the given sequent are hidden using the taclets hide_left and hide_right. + """) public static class Parameters { @Argument @MonotonicNonNull + @Documentation("The sequent containing the formulas to hide. Placeholders are allowed.") public Sequent sequent; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/InstantiateCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/InstantiateCommand.java index a4f795b1011..45424416ce6 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/InstantiateCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/InstantiateCommand.java @@ -15,6 +15,7 @@ import de.uka.ilkd.key.proof.RuleAppIndex; import de.uka.ilkd.key.rule.PosTacletApp; import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.scripts.meta.Documentation; import de.uka.ilkd.key.scripts.meta.Flag; import de.uka.ilkd.key.scripts.meta.Option; @@ -30,6 +31,7 @@ import org.key_project.util.collection.ImmutableList; import org.key_project.util.collection.ImmutableSLList; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.jspecify.annotations.Nullable; import static de.uka.ilkd.key.logic.equality.RenamingTermProperty.RENAMING_TERM_PROPERTY; @@ -220,35 +222,42 @@ public String getName() { return "instantiate"; } - @Override - public String getDocumentation() { - return """ - instantiate var=a occ=2 with="a_8" hide -

- instantiate formula="\\forall int a; phi(a)" with="a_8\" - """; - } - - /** - * - */ + @Documentation(category = "Fundamental", value = """ + Instantiate a universally quantified formula (in the antecedent; + or an existentially quantified formula in succedent) by a term. + One of `var` or `formula` must be specified. If `var` is given, the formula is determined by looking for + a particular occurrence of a quantifier over that variable name. + If `formula` is given, that quantified formula is used directly. + `with` must be specified. + + #### Examples: + + * `instantiate var:a occ:2 with:a_8 hide` + * `instantiate formula:"\\forall int a; phi(a)" with="a_8"` + """) public static class Parameters { + @Documentation("The toplevel quantified formula to instantiate. Placeholder matching symbols can be used.") @Option(value = "formula") @Nullable public JTerm formula; + @Documentation("The name of the bound variable to instantiate.") @Option(value = "var") @Nullable public String var; + @Documentation("The occurrence number of the quantifier over 'var' in the sequent starting at 1. Default is 1.") @Option(value = "occ") public @Nullable int occ = 1; + @Documentation("If given, the rule used for instantiation is the one that hides the instantiated formula to " + + "prevent it from being used for further automatic proof steps.") @Flag("hide") public boolean hide; + @Documentation("The term to instantiate the bound variable with. Must be given.") @Option(value = "with") - public @Nullable JTerm with; + public @MonotonicNonNull JTerm with; } private static class TacletNameFilter extends TacletFilter { diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/JavascriptCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/JavascriptCommand.java index d3bb13607c9..77ef4459391 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/JavascriptCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/JavascriptCommand.java @@ -13,10 +13,29 @@ import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.prover.sequent.Sequent; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +/** + * This command allows to execute arbitrary JavaScript code. The code is executed in a context where + * the current selected goal is available as {@code goal} and a function {@code setVar(v,t)} is + * available to set an abbreviation (where {@code v} is the name of the variable including the + * leading {@code @} and {@code t} is either a term or a string that can be parsed as a term). + *

+ * Example: + * + *

+ * javascript {
+ *   var x = goal.getAntecedent().get(0).getFormula();
+ *   setVar("@myVar", x);
+ * }
+ * 
+ * + * This command is powerful but should be used with care, as it can easily lead to unsound proofs if + * used incorrectly. + */ public class JavascriptCommand extends AbstractCommand { private static final String PREAMBLE = """ @@ -52,7 +71,25 @@ public String getName() { return "javascript"; } + @Documentation(category = "Internal", value = """ + This command allows to execute arbitrary JavaScript code. The code is executed in a context where + the current selected goal is available as `goal` and a function `setVar(v,t)` is + available to set an abbreviation (where `v` is the name of the variable including the + leading `@` and `t` is either a term or a string that can be parsed as a term). + + #### Example: + ``` + javascript { + var x = goal.getAntecedent().get(0).getFormula(); + setVar("@myVar", x); + } + ``` + + This command is powerful but should be used with care, as it can easily lead to unsound proofs if + used incorrectly. + """) public static class Parameters { + @Documentation("The JavaScript code to execute.") @Argument public @MonotonicNonNull String script; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/LeaveCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/LeaveCommand.java index c2c9643ee74..a7d3689e61f 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/LeaveCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/LeaveCommand.java @@ -6,9 +6,13 @@ import de.uka.ilkd.key.control.AbstractUserInterfaceControl; import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +@Documentation(category = "Control", value = """ + Leave the current goal as it is. Technically, this + marks the current goal to be 'interactive' that is ignored by script commands or calls to automation.""") public class LeaveCommand extends NoArgumentCommand { private static final Logger LOGGER = LoggerFactory.getLogger(LeaveCommand.class.getName()); diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/LetCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/LetCommand.java index 14a4aa1cfe9..0faa7026522 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/LetCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/LetCommand.java @@ -10,9 +10,12 @@ import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.nparser.KeYParser; import de.uka.ilkd.key.pp.AbbrevMap; +import de.uka.ilkd.key.scripts.meta.Documentation; +import de.uka.ilkd.key.scripts.meta.OptionalVarargs; import de.uka.ilkd.key.scripts.meta.ProofScriptArgument; import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; /// The *let* command lets you introduce entries to the abbreviation table. /// ``` @@ -32,42 +35,58 @@ /// * Apr,2025 (weigl): remove {@code force} in favor of {@code letf}. /// * Jan,2025 (weigl): add new parameter {@code force} to override bindings. @NullMarked -public class LetCommand implements ProofScriptCommand { +public class LetCommand extends AbstractCommand { + + @Documentation(category = "Fundamental", value = """ + The let command lets you introduce entries to the abbreviation table. + + let @abbrev1=term1 ... @abbrev2=term2; + + or + + letf @abbrev1=term1 ... @abbrev2=term2; + + One or more key-value pairs are supported where key starts is @ followed by an identifier and + value is a term. + If letf if used instead of let, the let bindings are overridden otherwise conflicts results into an exception.""") + + + public static class Parameters { + @Documentation("Key-value pairs where key is the name of the abbreviation (starting with @) and value is a term.") + @OptionalVarargs(as = JTerm.class) + public Map namedArgs = Map.of(); + } + + public LetCommand() { + super(Parameters.class); + } + @Override public List getArguments() { return List.of(); } + public void execute(ScriptCommandAst ast) throws ScriptException, InterruptedException { + var args = state().getValueInjector().inject(new Parameters(), ast); - @Override - public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst args, - EngineState stateMap) throws ScriptException, InterruptedException { - - AbbrevMap abbrMap = stateMap.getAbbreviations(); + AbbrevMap abbrMap = state().getAbbreviations(); - boolean force = "letf".equals(args.commandName()); + boolean force = "letf".equals(ast.commandName()); - for (Map.Entry entry : args.namedArgs().entrySet()) { + for (Map.Entry entry : args.namedArgs.entrySet()) { String key = entry.getKey(); - if (key.startsWith("#") || key.equals("force")) { - continue; - } - if (!key.startsWith("@")) { - throw new ScriptException("Unexpected parameter to let, only @var allowed: " + key); + if (key.startsWith("@")) { + // get rid of @ + key = key.substring(1); } - // get rid of @ - key = key.substring(1); - if (abbrMap.containsAbbreviation(key) && !force) { throw new ScriptException(key + " is already fixed in this script"); } + try { - final var termCtx = (KeYParser.ProofScriptExpressionContext) entry.getValue(); - final var value = termCtx.accept(stateMap.getEvaluator()); - final var term = stateMap.getValueInjector().convert(value, JTerm.class); - abbrMap.put(term, key, true); + abbrMap.put(entry.getValue(), key, true); } catch (Exception e) { throw new ScriptException(e); } @@ -80,11 +99,6 @@ public String getName() { return "let"; } - @Override - public String getDocumentation() { - return ""; - } - @Override public List getAliases() { return List.of(getName(), "letf"); diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/MacroCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/MacroCommand.java index bd7763f428f..cffb75e9f57 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/MacroCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/MacroCommand.java @@ -19,13 +19,18 @@ import de.uka.ilkd.key.scripts.meta.Option; import de.uka.ilkd.key.scripts.meta.OptionalVarargs; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.key_project.logic.PosInTerm; import org.key_project.prover.engine.TaskStartedInfo; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.sequent.Sequent; import org.jspecify.annotations.Nullable; - +/** + * Command to invoke a user-defined macro (like from UI) + * + * See Parameters for documentation. + */ public class MacroCommand extends AbstractCommand { private static final Map macroMap = loadMacroMap(); @@ -164,10 +169,22 @@ private static String formatTermString(String str) { .replace(" +", " "); } + @Documentation(category = "Fundamental", value = """ + The MacroCommand invokes one of KeY's macros. The macro must be registered to KeY's services. + + The command takes the name of the macro as first argument, followed by optional + parameters to configure the macro. + + The macro is applied to the first open automatic goal in the proof. + + #### Examples: + * `macro "prop-split"` + * `macro "auto-pilot"` + """) public static class Parameters { @Argument @Documentation("Macro name") - public String macroName; + public @MonotonicNonNull String macroName; @Documentation("Run on formula number \"occ\" parameter") @Option(value = "occ") @@ -179,6 +196,7 @@ public static class Parameters { public @Nullable String matches = null; /** Variable macro parameters */ + @Documentation("Macro parameters, given as varargs with prefix 'arg_'. E.g. arg_param1=value1") @OptionalVarargs(as = String.class, prefix = "arg_") public Map instantiations = new HashMap<>(); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ObtainCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ObtainCommand.java new file mode 100644 index 00000000000..2576e9250d5 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ObtainCommand.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.scripts; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.op.*; +import de.uka.ilkd.key.proof.*; +import de.uka.ilkd.key.rule.*; +import de.uka.ilkd.key.scripts.meta.*; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.jspecify.annotations.Nullable; +import org.key_project.logic.Name; +import org.key_project.logic.PosInTerm; +import org.key_project.logic.op.sv.SchemaVariable; +import de.uka.ilkd.key.rule.Taclet; +import org.key_project.prover.sequent.FormulaChangeInfo; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SemisequentChangeInfo; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.ImmutableSet; + +import java.util.*; + +/** + * Command that applies a calculus rule All parameters are passed as strings and converted by the + * command. The parameters are: + *
    + *
  1. #2 = rule name
  2. + *
  3. on= key.core.logic.Term on which the rule should be applied to as String (find part of the + * rule)
  4. + *
  5. formula= toplevel formula in which term appears in
  6. + *
  7. occ = occurrence number
  8. + *
  9. inst_= instantiation
  10. + *
+ */ +public class ObtainCommand extends AbstractCommand { + + private static final Name INTRO_TACLET_NAME = new Name("intro"); + private static final Name ALL_RIGHT_TACLET_NAME = new Name("allRight"); + + public ObtainCommand() { + super(Parameters.class); + } + + @Override + public String getName() { + return "__obtain"; + } + + @Override + public void execute(ScriptCommandAst ast) + throws ScriptException, InterruptedException { + var args = state().getValueInjector().inject(new Parameters(), ast); + + var obtainMap = (Map)state().getUserData("jml.obtainVarMap"); + if(obtainMap == null) { + throw new ScriptException("No obtain variable map found. This command must be used within a JML proof."); + } + + LocationVariable var = obtainMap.keySet().stream() + .filter(lv -> lv.name().toString().equals(args.var)) + .findAny().orElseThrow(() -> new ScriptException("No such obtain variable registered: " + args.var)); + + JTerm skolem; + if (args.equals != null) { + skolem = executeEquals(var, args.equals); + } else if (args.suchThat != null) { + skolem = executeSuchThat(var, args.suchThat); + } else if (args.fromGoal) { + skolem = executeFromGoal(var); + } else { + throw new ScriptException("Exactly one of 'such_that', 'equals', or 'from_goal' must be given."); + } + + obtainMap.put(var, skolem.op(JFunction.class)); + + } + + private JTerm executeFromGoal(LocationVariable var) throws ScriptException { + Goal goal = state.getFirstOpenAutomaticGoal(); + + // This works under the assumption that the first succedent formula is the "goal" formula. + SequentFormula sequentFormula = identifySequentFormula(goal.node()); + JTerm formula = (JTerm) sequentFormula.formula(); + while(formula.op() instanceof UpdateApplication) { + formula = formula.sub(1); + } + if(formula.op() != Quantifier.ALL) { + throw new ScriptException("For 'obtain \\from_goal, the goal formula needs to be a universally quantified formula."); + } + + Services services = state().getProof().getServices(); + Taclet intro = state.getProof().getEnv().getInitConfigForEnvironment() + .lookupActiveTaclet(ALL_RIGHT_TACLET_NAME); + TacletApp app = NoPosTacletApp.createNoPosTacletApp(intro); + + SchemaVariable sk = getSV(app.uninstantiatedVars(), "sk"); + String name = VariableNameProposer.DEFAULT.getNameProposal(var.name().toString(), services, null); + app = app.createSkolemConstant(name, sk, var.sort(), true, services); + + SchemaVariable b = getSV(app.uninstantiatedVars(), "b"); + app = app.addCheckedInstantiation(b, formula.sub(0), services, true); + + SchemaVariable u = getSV(app.uninstantiatedVars(), "u"); + app = app.addCheckedInstantiation(u, services.getTermBuilder().var((LogicVariable) formula.boundVars().get(0)), services, true); + app = app.setPosInOccurrence(new PosInOccurrence(sequentFormula, PosInTerm.getTopLevel(), false), services); + + goal.apply(app); + return app.instantiations().getInstantiation(sk); + } + + private SequentFormula identifySequentFormula(Node node) { + SemisequentChangeInfo changes = node.getNodeInfo().getSequentChangeInfo().getSemisequentChangeInfo(false); + ImmutableList added = changes.addedFormulas(); + if(!added.isEmpty()) { + if(added.size() == 1) { + return added.get(0); + } + } else { + ImmutableList modified = changes.modifiedFormulas(); + if(modified.size() == 1) { + return modified.get(0).newFormula(); + } + } + throw new IllegalStateException("Multiple or no formulas modified or added in last step, cannot identify sequent formula to skolemize."); + } + + private JTerm executeSuchThat(LocationVariable var, @Nullable JTerm suchThat) { + throw new UnsupportedOperationException("such_that not yet supported in obtain."); + } + + private JTerm executeEquals(LocationVariable var, @Nullable JTerm equals) throws ScriptException { + Services services = state().getProof().getServices(); + Taclet intro = state.getProof().getEnv().getInitConfigForEnvironment() + .lookupActiveTaclet(INTRO_TACLET_NAME); + TacletApp app = NoPosTacletApp.createNoPosTacletApp(intro); + SchemaVariable sk = getSV(app.uninstantiatedVars(), "sk"); + SchemaVariable t = getSV(app.uninstantiatedVars(), "t"); + String name = VariableNameProposer.DEFAULT.getNameProposal(var.name().toString(), services, null); + app = app.createSkolemConstant(name, sk, var.sort(), true, services); + app = app.addCheckedInstantiation(t, equals, services, true); + state.getFirstOpenAutomaticGoal().apply(app); + return app.instantiations().getInstantiation(sk); + } + + private static SchemaVariable getSV(ImmutableSet schemaVars, String name) { + for (SchemaVariable schemaVar : schemaVars) { + if(schemaVar.name().toString().equals(name)) { + return schemaVar; + } + } + throw new NoSuchElementException("No schema variable with name " + name); + } + + @Documentation(category = "JML", value = """ + Command that introduces a fresh variable with a given name and sort. + Exactly one of `such_that`, `equals`, or `from_goal` must be given. + + The command should not be called directly, but is used internally by + the JML script support within KeY. + """) + public static class Parameters implements ValueInjector.VerifyableParameters { + @Option(value = "var") + @Documentation("Name of the variable to be instantiated.") + public @MonotonicNonNull String var; + + @Option(value = "such_that") + @Documentation("Condition that is to be established for the fresh variable.") + public @Nullable JTerm suchThat; + + @Option(value = "from_goal") + @Documentation("Top-level formula in which the term appears.") + public @Nullable boolean fromGoal = false; + + @Option(value = "equals") + @Documentation("Represented term for which this is an abbreviation.") + public @Nullable JTerm equals; + + @Override + public void verifyParameters() throws IllegalArgumentException, InjectionException { + int cnt = 0; + if(suchThat != null) cnt++; + if(equals != null) cnt++; + if(fromGoal) cnt++; + if(cnt != 1) { + throw new InjectionException("Exactly one of 'such_that', 'equals', or 'from_goal' must be given."); + } + } + } + +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/OneStepSimplifierCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/OneStepSimplifierCommand.java new file mode 100644 index 00000000000..ac2ff7b63f4 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/OneStepSimplifierCommand.java @@ -0,0 +1,94 @@ +/* This file is part of KeY - https://key-project.org + * KeY is licensed under the GNU General Public License Version 2 + * SPDX-License-Identifier: GPL-2.0-only */ +package de.uka.ilkd.key.scripts; + +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.rule.OneStepSimplifierRuleApp; +import de.uka.ilkd.key.scripts.meta.Documentation; +import de.uka.ilkd.key.scripts.meta.Flag; +import de.uka.ilkd.key.scripts.meta.Option; + +import org.key_project.logic.PosInTerm; +import org.key_project.prover.sequent.*; +import org.key_project.util.collection.ImmutableList; + +import org.jspecify.annotations.Nullable; + +public class OneStepSimplifierCommand extends AbstractCommand { + + public OneStepSimplifierCommand() { + super(Parameters.class); + } + + @Override + public String getName() { + return "oss"; + } + + @Override + public void execute(ScriptCommandAst command) throws ScriptException, InterruptedException { + + var arguments = state().getValueInjector().inject(new Parameters(), command); + + final Goal goal = state.getFirstOpenAutomaticGoal(); + + if(Boolean.TRUE.equals(arguments.recentOnly)) { + SequentChangeInfo sci = goal.node().getNodeInfo().getSequentChangeInfo(); + var ante = sci.addedFormulas(true) + .prepend(sci.modifiedFormulas(true).map(FormulaChangeInfo::newFormula)); + applyOSS(ante, goal, true); + + var succ = sci.addedFormulas(false) + .prepend(sci.modifiedFormulas(false).map(FormulaChangeInfo::newFormula)); + applyOSS(succ, goal, false); + return; + } + + if (Boolean.TRUE.equals(arguments.antecedent)) { + applyOSS(goal.sequent().antecedent(), goal, true); + } + + if (Boolean.TRUE.equals(arguments.succedent)) { + applyOSS(goal.sequent().succedent(), goal, false); + } + } + + + private static void applyOSS(Iterable antecedent, Goal goal, boolean inAntec) { + for (SequentFormula sf : antecedent) { + ImmutableList builtins = goal.ruleAppIndex().getBuiltInRules(goal, + new PosInOccurrence(sf, PosInTerm.getTopLevel(), inAntec)); + for (IBuiltInRuleApp builtin : builtins) { + if (builtin instanceof OneStepSimplifierRuleApp) { + goal.apply(builtin); + } + } + } + } + + + @Documentation(category = "Fundamental", value = """ + The oss command applies the *one step simplifier* on the current proof goal. + This simplifier applies a set of built-in simplification rules to the formulas in the sequent. + It can be configured to apply the one step simplifier only on the antecedent or succedent. + By default, it is applied on both sides of the sequent. + """) + public static class Parameters { + @Documentation("Application of the one step simplifier can be forbidden on the antecedent side by setting " + + "this option to false. Default is true.") + @Option(value = "antecedent") + public @Nullable Boolean antecedent = Boolean.TRUE; + + @Documentation("Application of the one step simplifier can be forbidden on the succedent side by setting " + + "this option to false. Default is true.") + @Option(value = "succedent") + public @Nullable Boolean succedent = Boolean.TRUE; + + @Documentation("Limit the application to the recently added or changed formulas. Deactivates the " + + "antecedent and succedent options.") + @Flag("recentOnly") + public @Nullable Boolean recentOnly = Boolean.FALSE; + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptCommand.java index d4fb03c6735..a8bf31ab11e 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptCommand.java @@ -6,6 +6,7 @@ import java.util.List; import de.uka.ilkd.key.control.AbstractUserInterfaceControl; +import de.uka.ilkd.key.scripts.meta.ArgumentsLifter; import de.uka.ilkd.key.scripts.meta.ProofScriptArgument; import org.jspecify.annotations.NullMarked; @@ -38,15 +39,19 @@ void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst args, EngineState stateMap) throws ScriptException, InterruptedException; - /// Returns the name of this proof command. The name should be constant and not be clash with - /// the - /// name of other commands. The name is essential for finding this command within an hashmap. + /// Returns the name of this proof command. The name must be a constant and not be clash with + /// the name of other commands. The name is used to identify the command in a script. The name + /// must be amongst the aliases returned by [#getAliases()]. /// /// @return a non-null, non-empty string /// @see ProofScriptEngine + /// @see #getAliases() String getName(); - /// Announce a list of potential aliases of this command. + /// Announce a list of aliases of this command. + /// + /// Aliases of different commands should be disjoint, otherwise the first command found + /// will be executed. /// /// The command can react differently for each alias. The call name is given to /// [#execute(AbstractUserInterfaceControl,ScriptCommandAst,EngineState)] @@ -55,6 +60,7 @@ void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst args, /// /// @return an unmodifiable list of alias names under which command can be called, including /// [#getName()] + /// @see #getName() default List getAliases() { return List.of(getName()); } @@ -62,5 +68,12 @@ default List getAliases() { /// A documentation for the commands. /// /// @return a non-null string - String getDocumentation(); + default String getDocumentation() { + return ArgumentsLifter.extractDocumentation(getName(), getClass(), null); + } + + /// A category name for this command. This is used to group commands in the UI or documentation. + default String getCategory() { + return ArgumentsLifter.extractCategory(getClass(), null); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptEngine.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptEngine.java index 4212e60ec03..1fd2bceb78a 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptEngine.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ProofScriptEngine.java @@ -13,6 +13,7 @@ import java.util.stream.Collectors; import de.uka.ilkd.key.control.AbstractUserInterfaceControl; +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; import de.uka.ilkd.key.nparser.KeYParser; import de.uka.ilkd.key.nparser.KeyAst; import de.uka.ilkd.key.nparser.ParsingFacade; @@ -31,47 +32,24 @@ * @author Alexander Weigl */ public class ProofScriptEngine { - private static final Map COMMANDS = loadCommands(); private static final Logger LOGGER = LoggerFactory.getLogger(ProofScriptEngine.class); - private final List script; - /** - * The initially selected goal. + * The collection of known script commands. */ - private final @Nullable Goal initiallySelectedGoal; + private static final Map COMMANDS = loadCommands(); /** * The engine state map. */ - private EngineState stateMap; - - private @Nullable Consumer commandMonitor; - - public ProofScriptEngine(Path file) throws IOException { - this(ParsingFacade.parseScript(file), null); - } - - public ProofScriptEngine(KeyAst.ProofScript script) { - this(script, null); - } - - /** - * Instantiates a new proof script engine. - * - * @param script the script - * @param initiallySelectedGoal the initially selected goal - */ - public ProofScriptEngine(KeyAst.ProofScript script, Goal initiallySelectedGoal) { - this(script.asAst(), initiallySelectedGoal); - } + private final EngineState stateMap; - public ProofScriptEngine(List script, Goal initiallySelectedGoal) { - this.initiallySelectedGoal = initiallySelectedGoal; - this.script = script; + public ProofScriptEngine(Proof proof) { + super(); + this.stateMap = new EngineState(proof, this); } - private static Map loadCommands() { + static Map loadCommands() { Map result = new HashMap<>(); var loader = ServiceLoader.load(ProofScriptCommand.class); @@ -84,49 +62,48 @@ private static Map loadCommands() { return result; } - public void execute(AbstractUserInterfaceControl uiControl, Proof proof) - throws IOException, InterruptedException, ScriptException { - stateMap = new EngineState(proof, this); + public void setInitiallySelectedGoal(@Nullable Goal initiallySelectedGoal) { + this.stateMap.setGoal(initiallySelectedGoal); + } - if (initiallySelectedGoal != null) { - stateMap.setGoal(initiallySelectedGoal); - } + public void execute(AbstractUserInterfaceControl uiControl, ScriptBlock block) + throws ScriptException, InterruptedException { + execute(uiControl, block.commands()); + } - if (script.isEmpty()) { // no commands given, no work to do - return; - } - // add the filename (if available) to the statemap. - try { - URI url = script.getFirst().location().fileUri(); - stateMap.setBaseFileName(Paths.get(url)); - } catch (NullPointerException | InvalidPathException ignored) { - // weigl: occurs on windows platforms, due to the fact - // that the URI contains "" from ANTLR4 when read by string - // "<" is illegal on windows - } - - // add the observer (if installed) to the state map - if (commandMonitor != null) { - stateMap.setObserver(commandMonitor); - } + public void execute(AbstractUserInterfaceControl uiControl, Path file) + throws ScriptException, InterruptedException, IOException { + KeyAst.ProofScript script = ParsingFacade.parseScript(file); execute(uiControl, script); } - public void execute(AbstractUserInterfaceControl uiControl, ScriptBlock block) + public void execute(AbstractUserInterfaceControl ui, KeyAst.ProofScript script) throws ScriptException, InterruptedException { - execute(uiControl, block.commands()); + execute(ui, script.asAst()); } public void execute(AbstractUserInterfaceControl uiControl, List commands) throws InterruptedException, ScriptException { - if (script.isEmpty()) { // no commands given, no work to do + if (commands.isEmpty()) { // no commands given, no work to do return; } - Location start = script.getFirst().location(); + Location start = commands.getFirst().location(); Proof proof = stateMap.getProof(); + // add the filename (if available) to the statemap. + try { + if(start != null) { + URI url = start.fileUri(); + stateMap.setBaseFileName(Paths.get(url)); + } + } catch (InvalidPathException ignored) { + // weigl: occurs on windows platforms, due to the fact + // that the URI contains "" from ANTLR4 when read by string + // "<" is illegal on windows + } + int cnt = 0; for (ScriptCommandAst ast : commands) { if (Thread.interrupted()) { @@ -138,14 +115,14 @@ public void execute(AbstractUserInterfaceControl uiControl, List LOGGER.debug("{}", g.sequent())); + LOGGER.debug("Commands: {}", commands.stream() + .map(ScriptCommandAst::asCommandLine) + .collect(Collectors.joining("\n"))); throw new ScriptException( String.format("Error while executing script: %s%n%nCommand: %s%nPosition: %s%n", @@ -188,18 +169,6 @@ public void execute(AbstractUserInterfaceControl uiControl, List monitor) { - this.commandMonitor = monitor; + this.stateMap.setObserver(monitor); } public static ProofScriptCommand getCommand(String commandName) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/RuleCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/RuleCommand.java index 8c2b4796c40..510584fefc7 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/RuleCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/RuleCommand.java @@ -4,6 +4,7 @@ package de.uka.ilkd.key.scripts; import java.util.*; +import java.util.stream.Collectors; import de.uka.ilkd.key.java.Services; import de.uka.ilkd.key.logic.*; @@ -14,6 +15,7 @@ import de.uka.ilkd.key.proof.RuleAppIndex; import de.uka.ilkd.key.rule.*; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import de.uka.ilkd.key.scripts.meta.Option; import de.uka.ilkd.key.scripts.meta.OptionalVarargs; @@ -24,6 +26,7 @@ import org.key_project.prover.proof.rulefilter.TacletFilter; import org.key_project.prover.rules.RuleApp; import org.key_project.prover.rules.Taclet; +import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; import org.key_project.prover.sequent.PosInOccurrence; import org.key_project.prover.sequent.SequentFormula; import org.key_project.util.collection.ImmutableList; @@ -58,23 +61,6 @@ public String getName() { return "rule"; } - @Override - public String getDocumentation() { - return """ - Command that applies a calculus rule. - All parameters are passed as strings and converted by the command. - - The parameters are: -
    -
  1. #2 = rule name
  2. -
  3. on= key.core.logic.Term on which the rule should be applied to as String (find part of the rule)
  4. -
  5. formula= toplevel formula in which term appears in
  6. -
  7. occ = occurrence number
  8. -
  9. inst_= instantiation
  10. -
- """; - } - @Override public void execute(ScriptCommandAst params) throws ScriptException, InterruptedException { @@ -160,9 +146,11 @@ private TacletApp instantiateTacletApp(final Parameters p, final EngineState sta ImmutableList assumesCandidates = theApp .findIfFormulaInstantiations(state.getFirstOpenAutomaticGoal().sequent(), services); - assumesCandidates = ImmutableList.fromList(filterList(p, assumesCandidates)); + assumesCandidates = ImmutableList.fromList(filterList(services, p, assumesCandidates)); - if (assumesCandidates.size() != 1) { + if (assumesCandidates.size() == 0) { + throw new ScriptException("No \\assumes instantiation"); + } else if (assumesCandidates.size() != 1) { throw new ScriptException("Not a unique \\assumes instantiation"); } @@ -241,7 +229,7 @@ private IBuiltInRuleApp builtInRuleApp(Parameters p, EngineState state, BuiltInR throw new ScriptException("No matching applications."); } - if (p.occ < 0) { + if (p.occ == null || p.occ < 0) { if (matchingApps.size() > 1) { throw new ScriptException("More than one applicable occurrence"); } @@ -249,7 +237,7 @@ private IBuiltInRuleApp builtInRuleApp(Parameters p, EngineState state, BuiltInR return matchingApps.get(0); } else { if (p.occ >= matchingApps.size()) { - throw new ScriptException("Occurence " + p.occ + throw new ScriptException("Occurrence " + p.occ + " has been specified, but there are only " + matchingApps.size() + " hits."); } @@ -260,7 +248,7 @@ private IBuiltInRuleApp builtInRuleApp(Parameters p, EngineState state, BuiltInR private TacletApp findTacletApp(Parameters p, EngineState state) throws ScriptException { ImmutableList allApps = findAllTacletApps(p, state); - List matchingApps = filterList(p, allApps); + List matchingApps = filterList(state.getProof().getServices(), p, allApps); if (matchingApps.isEmpty()) { throw new ScriptException("No matching applications."); @@ -268,7 +256,11 @@ private TacletApp findTacletApp(Parameters p, EngineState state) throws ScriptEx if (p.occ < 0) { if (matchingApps.size() > 1) { - throw new ScriptException("More than one applicable occurrence"); + // todo make a nice string here! + throw new ScriptException("More than one applicable occurrence:\n" + + matchingApps.stream().map( + ap -> ap.posInOccurrence().subTerm() + " " + ap.matchConditions()) + .collect(Collectors.joining("\n") )); } return matchingApps.get(0); } else { @@ -290,7 +282,7 @@ private ImmutableList findBuiltInRuleApps(Parameters p, EngineS ImmutableList allApps = ImmutableSLList.nil(); for (SequentFormula sf : g.node().sequent().antecedent()) { - if (!isFormulaSearchedFor(p, sf, services)) { + if (!isSequentFormulaSearchedFor(p, sf, services)) { continue; } @@ -299,7 +291,7 @@ private ImmutableList findBuiltInRuleApps(Parameters p, EngineS } for (SequentFormula sf : g.node().sequent().succedent()) { - if (!isFormulaSearchedFor(p, sf, services)) { + if (!isSequentFormulaSearchedFor(p, sf, services)) { continue; } @@ -321,7 +313,7 @@ private ImmutableList findAllTacletApps(Parameters p, EngineState sta ImmutableList allApps = ImmutableSLList.nil(); for (SequentFormula sf : g.node().sequent().antecedent()) { - if (!isFormulaSearchedFor(p, sf, services)) { + if (!isSequentFormulaSearchedFor(p, sf, services)) { continue; } @@ -330,7 +322,7 @@ private ImmutableList findAllTacletApps(Parameters p, EngineState sta } for (SequentFormula sf : g.node().sequent().succedent()) { - if (!isFormulaSearchedFor(p, sf, services)) { + if (!isSequentFormulaSearchedFor(p, sf, services)) { continue; } @@ -350,8 +342,7 @@ private ImmutableList findAllTacletApps(Parameters p, EngineState sta * @param sf The {@link SequentFormula} to check. * @return true if sf matches. */ - private boolean isFormulaSearchedFor(Parameters p, - SequentFormula sf, Services services) + private boolean isSequentFormulaSearchedFor(Parameters p, SequentFormula sf, Services services) throws ScriptException { Term term = sf.formula(); final boolean satisfiesFormulaParameter = @@ -380,13 +371,14 @@ private static String formatTermString(String str) { /* * Filter those apps from a list that are according to the parameters. */ - private List filterList(Parameters p, ImmutableList list) { + private List filterList(Services services, Parameters p, + ImmutableList list) { List matchingApps = new ArrayList<>(); + TermComparisonWithHoles matcher = p.on == null ? null : p.on.getMatcher(); for (TacletApp tacletApp : list) { + boolean add = true; if (tacletApp instanceof PosTacletApp pta) { - JTerm term = (JTerm) pta.posInOccurrence().subTerm(); - boolean add = - p.on == null || RENAMING_TERM_PROPERTY.equalsModThisProperty(term, p.on); + add = matcher == null || matcher.matches(pta.posInOccurrence()); for (var entry : pta.instantiations().getInstantiationMap()) { final SchemaVariable sv = entry.key(); @@ -398,6 +390,10 @@ private List filterList(Parameters p, ImmutableList list) || userInst.equalsModProperty(ptaInst, IRRELEVANT_TERM_LABELS_PROPERTY); } + if(tacletApp.assumesFormulaInstantiations() != null) { + add &= checkAssumes(p, tacletApp.assumesFormulaInstantiations(), services); + } + if (add) { matchingApps.add(pta); } @@ -406,27 +402,62 @@ private List filterList(Parameters p, ImmutableList list) return matchingApps; } + private boolean checkAssumes(Parameters p, ImmutableList ifFormulaInstantiations, Services services) { + if(p.assumes == null) { + // no "assumes" restrictions specified. + return true; + } + + return p.assumes.matches(ifFormulaInstantiations); + } + + + @Documentation(category = "Fundamental", value = """ + This command can be used to apply a calculus rule to the currently active open goal. + + #### Examples: + - `rule cut inst_cutFormula: (a > 0)` applies the cut rule on the formula `a > 0` like the cut command. + - `rule and_right on=(__ & __)` applies the rule `and_right` to the second occurrence + of a conjunction in the succedent. + - `rule my_rule on=(f(x)) formula="f\\(.*search.*\\)"` applies the rule `my_rule` to the term + `f(x)` in a formula matching the regular expression. + """) public static class Parameters { @Argument + @Documentation("Name of the rule to be applied.") public @MonotonicNonNull String rulename; @Option(value = "on") - public @Nullable JTerm on; + @Documentation("Term on which the rule should be applied to (matching the 'find' clause of the rule). " + + "This may contain placeholders.") + public @Nullable TermWithHoles on; @Option(value = "formula") + @Documentation("Top-level formula in which the term appears. This may contain placeholders.") public @Nullable JTerm formula; @Option(value = "occ") - public @Nullable int occ = -1; + @Documentation("Occurrence number if more than one occurrence matches. The first occurrence is 1. " + + "If ommitted, there must be exactly one occurrence.") + public @Nullable Integer occ = -1; /** * Represents a part of a formula (may use Java regular expressions as long as supported by * proof script parser). Rule is applied to the sequent formula which matches that string. */ + @Documentation("Instead of giving the toplevl formula completely, a regular expression can be " + + "specified to match the toplevel formula.") @Option(value = "matches") public @Nullable String matches = null; + + @Option(value = "assumes") + @Documentation(""" + If the rule has an `\\assumes` clause, this can be used to restrict the instantiations + """) + public @Nullable SequentWithHoles assumes; @OptionalVarargs(as = JTerm.class, prefix = "inst_") + @Documentation("Instantiations for term schema variables used in the rule.") public Map instantiations = new HashMap<>(); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SMTCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SMTCommand.java index b507e5ca40c..96f94da6d87 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SMTCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SMTCommand.java @@ -7,6 +7,7 @@ import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.rule.IBuiltInRuleApp; +import de.uka.ilkd.key.scripts.meta.Documentation; import de.uka.ilkd.key.scripts.meta.Flag; import de.uka.ilkd.key.scripts.meta.Option; import de.uka.ilkd.key.settings.DefaultSMTSettings; @@ -23,6 +24,11 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Command to invoke an SMT solver on the current goal(s). + * + * See Parameters for documentation. + */ public class SMTCommand extends AbstractCommand { private static final Logger LOGGER = LoggerFactory.getLogger(SMTCommand.class); @@ -108,11 +114,21 @@ private SolverTypeCollection computeSolvers(String value) throws ScriptException return new SolverTypeCollection(value, 1, types); } + @Documentation(category = "Fundamental", value = """ + The smt command invokes an SMT solver on the current goal(s). + By default, it uses the Z3 solver on the first open automatic goal. + If the option 'all' is given, it runs on all open goals. + If the option 'solver' is given, it uses the specified solver(s) instead of Z3. + Multiple solvers can be specified by separating their names with commas. + The available solvers depend on your system: KeY supports at least z3, cvc5. + """) public static class SMTCommandArguments { @Option("solver") public String solver = "Z3"; + @Deprecated @Flag(value = "all") + @Documentation("*Deprecated!* Apply the command on all open goals instead of only the first open automatic goal.") public boolean all = false; @Option(value = "timeout") diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveInstCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveInstCommand.java index 61bf7156e1f..2b39adbfaea 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveInstCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveInstCommand.java @@ -9,6 +9,7 @@ import de.uka.ilkd.key.pp.AbbrevMap; import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.logic.Name; import org.key_project.logic.op.Function; import org.key_project.logic.op.sv.SchemaVariable; @@ -24,6 +25,11 @@ * * @author Dominic Steinhoefel */ +@Documentation(category = "Internal", value = """ + Saves the instantiation of a schema variable by the last taclet application into an abbreviation for later use. + A nice use case is a manual loop invariant rule application, where the newly introduced anonymizing Skolem constants can be saved for later interactive instantiations. + As for the let command, it is not allowed to call this command multiple times with the same name argument (all names used for remembering instantiations are "final"). + """) public class SaveInstCommand extends AbstractCommand { public SaveInstCommand() { super(null); diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveNewNameCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveNewNameCommand.java index 49d28077f40..42d0c818376 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveNewNameCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SaveNewNameCommand.java @@ -12,6 +12,8 @@ import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Node; import de.uka.ilkd.key.rule.TacletApp; +import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import de.uka.ilkd.key.scripts.meta.Option; import org.key_project.logic.Name; @@ -87,9 +89,20 @@ public void execute(ScriptCommandAst arguments) throws ScriptException, Interrup } } + @Documentation(category = "Internal", value = """ + Special "Let" usually to be applied immediately after a manual rule application. Saves a new name + introduced by the last rule which matches certain criteria into an abbreviation for + later use. A nice use case is a manual loop invariant rule application, where the newly + introduced anonymizing Skolem constants can be saved for later interactive instantiations. As for + the let command, it is not allowed to call this command multiple times with the same name + argument (all names used for remembering instantiations are "final"). + """) public static class Parameters { - @Option(value = "#2") + @Documentation("The abbreviation to store the new name under, must start with @") + @Argument public String abbreviation; + + @Documentation("A regular expression to match the new name against, must match exactly one name") @Option(value = "matches") public String matches; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SchemaVarCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SchemaVarCommand.java index eef052c8e0d..06ac00c5099 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SchemaVarCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SchemaVarCommand.java @@ -10,6 +10,7 @@ import de.uka.ilkd.key.pp.AbbrevMap; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.logic.Name; import org.key_project.logic.sort.Sort; @@ -18,6 +19,7 @@ /** * */ +@Deprecated public class SchemaVarCommand extends AbstractCommand { public SchemaVarCommand() { @@ -65,6 +67,9 @@ public String getName() { return "schemaVar"; } + @Documentation(category = "Internal", value = """ + Defines a schema variable that can be used in subsequent commands. + """) public static class Parameters { @Argument(0) public @MonotonicNonNull String type; diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommand.java index 66d1ec2b656..978edf073a2 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommand.java @@ -9,10 +9,15 @@ import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +/** + * Includes and runs another script file. + * See Parameters for more documentation. + */ public class ScriptCommand extends AbstractCommand { private static final Logger LOGGER = LoggerFactory.getLogger(ProofScriptCommand.class); @@ -21,7 +26,9 @@ public ScriptCommand() { super(Parameters.class); } + @Documentation(category = "Control", value = "Includes and runs another script file.") public static class Parameters { + @Documentation("The filename of the script to include. May be relative to the current script.") @Argument public @MonotonicNonNull String filename; } @@ -38,9 +45,9 @@ public void execute(ScriptCommandAst ast) throws ScriptException, InterruptedExc LOGGER.info("Included script {}", file); try { - ProofScriptEngine pse = new ProofScriptEngine(file); + ProofScriptEngine pse = new ProofScriptEngine(proof); pse.setCommandMonitor(state().getObserver()); - pse.execute(uiControl, proof); + pse.execute(uiControl, file); } catch (NoSuchFileException e) { // The message is very cryptic otherwise. throw new ScriptException("Script file '" + file + "' not found", e); diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommandAst.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommandAst.java index 69bc8c728ea..4fe7bfc0f01 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommandAst.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptCommandAst.java @@ -11,6 +11,7 @@ import de.uka.ilkd.key.nparser.KeyAst; import de.uka.ilkd.key.parser.Location; +import de.uka.ilkd.key.util.ANTLRUtil; import org.antlr.v4.runtime.ParserRuleContext; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; @@ -73,12 +74,11 @@ public static String asReadableString(Object value) { if (value instanceof ScriptBlock b) { return b.asCommandLine(); } - if (value instanceof KeYParser.ProofScriptCodeBlockContext ctx) { asReadableString(KeyAst.ProofScript.asAst(null, ctx)); } if (value instanceof ParserRuleContext ctx) { - return ctx.getText(); + return ANTLRUtil.reconstructOriginal(ctx); } return Objects.toString(value); } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptException.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptException.java index f55b350e369..4fc21d7dcef 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptException.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptException.java @@ -12,7 +12,7 @@ public class ScriptException extends Exception implements HasLocation { private static final long serialVersionUID = -1200219771837971833L; - private final Location location; + private final @Nullable Location location; public ScriptException() { super(); @@ -24,7 +24,7 @@ public ScriptException(String message, Location location, Throwable cause) { this.location = location; } - public ScriptException(String message, Location location) { + public ScriptException(String message, @Nullable Location location) { super(message); this.location = location; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptLineParser.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptLineParser.java index c853a825023..e4f7d40c5f7 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptLineParser.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/ScriptLineParser.java @@ -15,9 +15,10 @@ import org.jspecify.annotations.Nullable; /** + * This class was used to parse script lines before the parsing was integrated into the general + * ANTLR parser for KeY files. * * @author mattias ulbrich - * */ class ScriptLineParser { @@ -248,7 +249,8 @@ private void exc(int c) throws ScriptException { } private Location getLocation() { - return new Location(fileURI, Position.newOneBased(line, col)); + Position pos = line >= 1 ? Position.newOneBased(line, col) : Position.UNDEFINED; + return new Location(fileURI, pos); } public int getOffset() { diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SelectCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SelectCommand.java index 650ef75542d..71bb5a35d40 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SelectCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SelectCommand.java @@ -12,8 +12,11 @@ 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.scripts.meta.Documentation; +import de.uka.ilkd.key.scripts.meta.InjectionException; import de.uka.ilkd.key.scripts.meta.Option; +import de.uka.ilkd.key.scripts.meta.ValueInjector; import org.key_project.logic.Term; import org.key_project.prover.sequent.Semisequent; import org.key_project.prover.sequent.Sequent; @@ -24,12 +27,15 @@ import static de.uka.ilkd.key.logic.equality.RenamingTermProperty.RENAMING_TERM_PROPERTY; +/** + * The SelectCommand selects a goal in the current proof. See documentation of {@link Parameters} + * for more information. + */ public class SelectCommand extends AbstractCommand { public SelectCommand() { super(Parameters.class); } - @Override public void execute(ScriptCommandAst params) throws ScriptException, InterruptedException { var args = state().getValueInjector().inject(new Parameters(), params); @@ -147,19 +153,48 @@ public String getName() { return "select"; } - public static class Parameters { + @Documentation(category = "Control", value = """ + The select command selects a goal in the current proof. + Exactly one of the parameters must be given. + The next command will then continue on the selected goal. + + #### Examples: + - `select formula: (x > 0)` + - `select number: -2` + - `select branch: "Loop Invariant"` + """) + public static class Parameters implements ValueInjector.VerifyableParameters { /** A formula defining the goal to select */ + @Documentation("A formula defining the goal to select. May contain placeholder symbols. If there is a formula " + + "matching the given formula in multiple goals, the first one is selected.") @Option(value = "formula") public @Nullable JTerm formula; + /** * The number of the goal to select, starts with 0. Negative indices are also allowed: -1 is * the last goal, -2 the second-to-last, etc. */ + @Documentation("The number of the goal to select, starts with 0. Negative indices are also allowed: -1 is " + + "the last goal, -2 the second-to-last, etc.") @Option(value = "number") public @Nullable Integer number; + /** The name of the branch to select */ + @Documentation("The name of the branch to select. If there are multiple branches with the same name, " + + "the first one is selected.") @Option(value = "branch") public @Nullable String branch; - } + @Override + public void verifyParameters() throws IllegalArgumentException, InjectionException { + int cnt = 0; + if (formula != null) cnt++; + if (number != null) cnt++; + if (branch != null) cnt++; + if (cnt != 1) { + throw new InjectionException( + "Exactly one of 'formula', 'branch' or 'number' are required"); + } + } + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SequentWithHoles.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SequentWithHoles.java new file mode 100644 index 00000000000..6b67ab25ab2 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SequentWithHoles.java @@ -0,0 +1,102 @@ +/* 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.scripts; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.ldt.JavaDLTheory; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.NamespaceSet; +import de.uka.ilkd.key.logic.op.JFunction; +import de.uka.ilkd.key.logic.op.SortDependingFunction; +import de.uka.ilkd.key.logic.sort.GenericSort; +import de.uka.ilkd.key.nparser.KeYParser; +import de.uka.ilkd.key.nparser.KeyAst; +import de.uka.ilkd.key.nparser.ParsingFacade; +import de.uka.ilkd.key.nparser.builder.ExpressionBuilder; +import de.uka.ilkd.key.util.parsing.BuildingIssue; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.ParserRuleContext; +import org.jspecify.annotations.NullMarked; +import org.key_project.logic.Name; +import org.key_project.logic.PosInTerm; +import org.key_project.logic.sort.AbstractSort; +import org.key_project.logic.sort.Sort; +import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.ImmutableSet; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; + +@NullMarked +public class SequentWithHoles { + + + private final List antecedent; + private final List succedent; + + private SequentWithHoles(List antecedent, List succedent) { + this.antecedent = antecedent; + this.succedent = succedent; + } + + private static final Logger LOGGER = LoggerFactory.getLogger(SequentWithHoles.class); + + public static SequentWithHoles fromString(EngineState engineState, String str) { + KeyAst.Seq seq = ParsingFacade.parseSequent(CharStreams.fromString(str)); + return fromParserContext(engineState, seq.ctx); + } + + public static SequentWithHoles fromParserContext(EngineState state, ParserRuleContext ctx) { + + if(ctx instanceof KeYParser.ProofScriptExpressionContext psctx) { + ctx = psctx.seq(); + } + + if(ctx instanceof KeYParser.SeqContext seqCtx) { + List antecedent = new java.util.ArrayList<>(); + KeYParser.SemisequentContext semseq = seqCtx.ant; + while(semseq != null && semseq.term() != null) { + antecedent.add(TermWithHoles.fromParserContext(state, semseq.term())); + semseq = semseq.semisequent(); + } + + List succedent = new java.util.ArrayList<>(); + semseq = seqCtx.suc; + while(semseq != null && semseq.term() != null) { + succedent.add(TermWithHoles.fromParserContext(state, semseq.term())); + semseq = semseq.semisequent(); + } + return new SequentWithHoles(antecedent, succedent); + } + + throw new IllegalArgumentException("Not a sequent: " + ctx.getText()); + } + + // TODO currently this does not check if the instantiation is on the correct side ... + public boolean matches(ImmutableList ifFormulaInstantiations) { + + for (AssumesFormulaInstantiation assF : ifFormulaInstantiations) { + var form = assF.getSequentFormula(); + if (antecedent.stream().noneMatch(f -> f.matchesToplevel(form)) && + succedent.stream().noneMatch(f -> f.matchesToplevel(form))) { + return false; + } + } + + return true; + } + + public boolean containsAntecendent(SequentFormula seqFormula) { + return antecedent.stream().anyMatch(f -> f.matchesToplevel(seqFormula)); + } + + public boolean containsSuccedent(SequentFormula seqFormula) { + return succedent.stream().anyMatch(f -> f.matchesToplevel(seqFormula)); + } +} \ No newline at end of file diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SetCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SetCommand.java index c0943975128..addd3beead8 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SetCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SetCommand.java @@ -6,11 +6,13 @@ import java.util.HashMap; import java.util.Map; +import java.util.Stack; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.proof.init.Profile; import de.uka.ilkd.key.rule.OneStepSimplifier; +import de.uka.ilkd.key.scripts.meta.Documentation; import de.uka.ilkd.key.scripts.meta.Option; import de.uka.ilkd.key.scripts.meta.OptionalVarargs; import de.uka.ilkd.key.settings.ProofSettings; @@ -18,7 +20,7 @@ import de.uka.ilkd.key.strategy.StrategyFactory; import de.uka.ilkd.key.strategy.StrategyProperties; -import org.checkerframework.checker.nullness.qual.Nullable; +import org.jspecify.annotations.Nullable; public class SetCommand extends AbstractCommand { public SetCommand() { @@ -29,12 +31,18 @@ public SetCommand() { public void execute(ScriptCommandAst arguments) throws ScriptException, InterruptedException { var args = state.getValueInjector().inject(new Parameters(), arguments); + if (args.settings.isEmpty()) { + throw new IllegalArgumentException( + "You have to set oss, steps, stack, or key(s) and value(s)."); + } + args.settings.remove("oss"); args.settings.remove("steps"); + args.settings.remove("stack"); final Proof proof = state.getProof(); - final StrategyProperties newProps = + StrategyProperties newProps = proof.getSettings().getStrategySettings().getActiveStrategyProperties(); if (args.oneStepSimplification != null) { @@ -44,6 +52,42 @@ public void execute(ScriptCommandAst arguments) throws ScriptException, Interrup Strategy.updateStrategySettings(proof, newProps); OneStepSimplifier.refreshOSS(proof); } + + if (args.proofSteps != null) { + state.setMaxAutomaticSteps(args.proofSteps); + } + + if (args.stackAction != null) { + Stack stack = + (Stack) state.getUserData("settingsStack"); + if (stack == null) { + stack = new Stack<>(); + state.putUserData("settingsStack", stack); + } + switch (args.stackAction) { + case "push": + stack.push(newProps.clone()); + break; + case "pop": + // TODO sensible error if empty + var resetProps = stack.pop(); + updateStrategySettings(state, resetProps); + break; + default: + throw new IllegalArgumentException("stack must be either push or pop."); + } + } else if (args.userKey != null) { + String[] kv = args.userKey.split(":", 2); + if (kv.length != 2) { + throw new IllegalArgumentException( + "userData must be of the form key:value. Use userData:\"myKey:myValue\"."); + } + state.putUserData("user." + kv[0], kv[1]); + } else { + throw new IllegalArgumentException( + "You have to set oss, steps, stack, or key(s) and value(s)."); + } + if (args.proofSteps != null) { state.setMaxAutomaticSteps(args.proofSteps); } @@ -68,7 +112,7 @@ public void execute(ScriptCommandAst arguments) throws ScriptException, Interrup * quite complicated implementation, which is inspired by StrategySelectionView. */ - public static void updateStrategySettings(EngineState state, StrategyProperties p) { + protected static void updateStrategySettings(EngineState state, StrategyProperties p) { final Proof proof = state.getProof(); final Strategy strategy = getStrategy(state, p); @@ -104,20 +148,26 @@ public String getName() { } public static class Parameters { - /** - * One Step Simplification parameter - */ + + @Documentation("Enable/disable one-step simplification") @Option(value = "oss") public @Nullable Boolean oneStepSimplification; - /** - * Maximum number of proof steps parameter - */ + @Documentation("Maximum number of proof steps") @Option(value = "steps") public @Nullable Integer proofSteps; - /***/ + @Documentation("key-value pairs to set") @OptionalVarargs public Map settings = HashMap.newHashMap(0); + + @Documentation("Push or pop the current settings to/from a stack of settings (mostly used internally)") + @Option(value = "stack") + public @Nullable String stackAction; + + @Documentation("Set user-defined key-value pair (Syntax: userData:\"key:value\")") + @Option(value = "userData") + public @Nullable String userKey; + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SetEchoCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SetEchoCommand.java index 843b4038f59..ff84be60a72 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SetEchoCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SetEchoCommand.java @@ -6,11 +6,16 @@ import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * A simple "echo" command for giving feedback to human observers during lengthy executions. + * An internal command to switch on/off echoing of executed commands. */ +@Deprecated +@Documentation(category = "Internal", value = """ + An internal command to switch on/off echoing of executed commands. + """) public class SetEchoCommand extends AbstractCommand { public SetEchoCommand() { super(Parameters.class); diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SetFailOnClosedCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SetFailOnClosedCommand.java index 34119c7b6de..3ca852901ea 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SetFailOnClosedCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SetFailOnClosedCommand.java @@ -6,6 +6,7 @@ import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** @@ -15,7 +16,11 @@ * complexity in a try-and-error manner, etc.). * * @author Dominic Steinhoefel + * + * @deprecated This should be merged in the {@link SetCommand} with a parameter like "failonclosed". */ +@Deprecated +@Documentation(category = "Control", value = "") public class SetFailOnClosedCommand extends AbstractCommand { public SetFailOnClosedCommand() { super(Parameters.class); diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/SkipCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/SkipCommand.java index 18418fb0729..f3c99991b26 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/SkipCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/SkipCommand.java @@ -4,7 +4,9 @@ package de.uka.ilkd.key.scripts; import de.uka.ilkd.key.control.AbstractUserInterfaceControl; +import de.uka.ilkd.key.scripts.meta.Documentation; +@Documentation(category = "Control", value = "Does exactly nothing.") public class SkipCommand extends NoArgumentCommand { @Override public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst args, @@ -16,9 +18,4 @@ public void execute(AbstractUserInterfaceControl uiControl, ScriptCommandAst arg public String getName() { return "skip"; } - - @Override - public String getDocumentation() { - return "Does exactly nothing. Really nothing."; - } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/TermComparisonWithHoles.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/TermComparisonWithHoles.java new file mode 100644 index 00000000000..984c44bf9f1 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/TermComparisonWithHoles.java @@ -0,0 +1,326 @@ +package de.uka.ilkd.key.scripts; + +import de.uka.ilkd.key.java.NameAbstractionTable; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.equality.RenamingTermProperty; +import de.uka.ilkd.key.logic.op.*; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.key_project.logic.PosInTerm; +import org.key_project.logic.Term; +import org.key_project.logic.op.Operator; +import org.key_project.logic.op.QuantifiableVariable; +import org.key_project.prover.rules.instantiation.AssumesFormulaInstantiation; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.Sequent; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.ImmutableSLList; +import org.key_project.util.collection.Pair; + +import java.util.*; + +import static de.uka.ilkd.key.scripts.TermWithHoles.*; + +/** + * A property that can be used for comparisons for terms. + * All term labels are ignored in this equality check. Additionally, holes (represented by the + * SortDependingFunction with name "_" and the Predicate with name "__") are treated as wildcards that + * match any subterm. + * + * @author Mattias Ulbrich + */ +@NullMarked +public class TermComparisonWithHoles { + + private final JTerm referenceTerm; + + TermComparisonWithHoles(JTerm referenceTerm) { + this.referenceTerm = Objects.requireNonNull(referenceTerm); + } + + public final boolean matches(PosInOccurrence pio) { + JTerm term = (JTerm) pio.subTerm(); + if (term.equalsModProperty(referenceTerm, RenamingTermProperty.RENAMING_TERM_PROPERTY)) { + return true; + } + + PosInTerm focus = findFocus(referenceTerm); + if(focus == null) { + focus = PosInTerm.getTopLevel(); + } + + List focusPaths = new ArrayList<>(); + expandFocusPaths(focus, pio.posInTerm(), PosInTerm.getTopLevel(), focusPaths); + + for(PosInTerm fpath : focusPaths) { + PosInTerm pit = pio.posInTerm().firstN(pio.depth() - fpath.depth()); + JTerm startTerm = (JTerm) pit.getSubTerm(pio.sequentFormula().formula()); + + boolean result = unifyHelp(referenceTerm, startTerm, + ImmutableSLList.nil(), + ImmutableSLList.nil(), + fpath); + if (result) { + return true; + } + } + + return false; + } + + private void expandFocusPaths(PosInTerm focus, PosInTerm input, PosInTerm base, List collected) { + if(focus.isTopLevel()) { + // fully matched: + collected.add(base); + return; + } + + if(input.isTopLevel()) { + // input is too shallow + return; + } + + if(focus.getIndex() == (char)-1) { + // ellipsis found, we need to expand + expandFocusPaths(focus.up(), input, base, collected); + expandFocusPaths(focus, input.up(), base.prepend((char) input.getIndex()), collected); + } else { + if(focus.getIndex() == input.getIndex()) { + expandFocusPaths(focus.up(), input.up(), base.prepend((char) input.getIndex()), collected); + } else { + // mismatch + return; + } + } + } + + public final boolean matchesToplevel(SequentFormula sf) { + // we use antecedent here since it does not matter and is never read ... + return matches(new PosInOccurrence(sf, PosInTerm.getTopLevel(), true)); + } + + private static @Nullable PosInTerm findFocus(Term pattern) { + var op = pattern.op(); + if (op instanceof JFunction) { + if(op.name().equals(TermWithHoles.FOCUS_NAME)) { + return PosInTerm.getTopLevel(); + } + if(op.name().equals(TermWithHoles.ELLIPSIS_NAME)) { + PosInTerm subFocus = findFocus(pattern.sub(0)); + if(subFocus != null) { + return subFocus.prepend((char)-1); + } else { + return null; + } + } + } + for (int i = 0; i < pattern.arity(); i++) { + Term sub = pattern.sub(i); + PosInTerm subFocus = findFocus(sub); + if(subFocus != null) { + return subFocus.prepend((char)i); + } + } + return null; + } + + + /** + * Compares two terms modulo bound renaming + * + * @param t0 the first term -- potentially containing holes + * @param t1 the second term + * @param ownBoundVars variables bound above the current position + * @param cmpBoundVars variables bound above the current position + * @return true is returned iff the terms are equal modulo + * bound renaming + */ + private static boolean unifyHelp(JTerm t0, JTerm t1, + ImmutableList ownBoundVars, + ImmutableList cmpBoundVars, + @Nullable PosInTerm expectedFocus) { + + if (t0 == t1 && ownBoundVars.equals(cmpBoundVars)) { + return true; + } + + Operator op = t0.op(); + if(op instanceof SortDependingFunction sdop) { + if(sdop.getKind().equals(HOLE_SORT_DEP_NAME)) { + return true; + } + } else if(op.name().equals(HOLE_PREDICATE_NAME) || op.name().equals(HOLE_NAME)) { + return true; + } else if(op.name().equals(FOCUS_NAME)) { + if(expectedFocus == null || !expectedFocus.isTopLevel()) { + // focus annotation not at expected position + return false; + } + return unifyHelp(t0.sub(0), t1, ownBoundVars, cmpBoundVars, null); + } else if(op.name().equals(ELLIPSIS_NAME)) { + // return true if it hits one subterm ... + Set> deepAllSubs = new HashSet<>(); + computeSubterms(t1, expectedFocus, deepAllSubs); + var lookfor = t0.sub(0); + return deepAllSubs.stream().anyMatch(t -> unifyHelp(lookfor, t.first, ownBoundVars, cmpBoundVars, t.second)); + } + + + final Operator op0 = t0.op(); + + if (op0 instanceof QuantifiableVariable) { + return handleQuantifiableVariable(t0, t1, ownBoundVars, + cmpBoundVars); + } + + final Operator op1 = t1.op(); + + if (op0 != op1) { + return false; + } + +// nat = handleJava(t0, t1, nat); +// if (nat == FAILED) { +// return false; +// } + + return descendRecursively(t0, t1, ownBoundVars, cmpBoundVars, expectedFocus); + } + + private static void computeSubterms(JTerm t, @Nullable PosInTerm expectedPos, Set> deepAllSubs) { + deepAllSubs.add(new Pair<>(t, expectedPos)); + for(int i = 0; i < t.arity(); i++) { + computeSubterms(t.sub(i), nextFocusPos(expectedPos, i), deepAllSubs); + } + } + + private static boolean handleQuantifiableVariable(JTerm t0, JTerm t1, + ImmutableList ownBoundVars, + ImmutableList cmpBoundVars) { + if (!((t1.op() instanceof QuantifiableVariable) && compareBoundVariables( + (QuantifiableVariable) t0.op(), (QuantifiableVariable) t1.op(), + ownBoundVars, cmpBoundVars))) { + return false; + } + return true; + } + + /** + * compare two quantifiable variables if they are equal modulo renaming + * + * @param ownVar first QuantifiableVariable to be compared + * @param cmpVar second QuantifiableVariable to be compared + * @param ownBoundVars variables bound above the current position + * @param cmpBoundVars variables bound above the current position + */ + private static boolean compareBoundVariables(QuantifiableVariable ownVar, + QuantifiableVariable cmpVar, + ImmutableList ownBoundVars, + ImmutableList cmpBoundVars) { + + final int ownNum = indexOf(ownVar, ownBoundVars); + final int cmpNum = indexOf(cmpVar, cmpBoundVars); + + if (ownNum == -1 && cmpNum == -1) { + // if both variables are not bound the variables have to be the + // same object + return ownVar == cmpVar; + } + + // otherwise the variables have to be bound at the same point (and both + // be bound) + return ownNum == cmpNum; + } + + private static int indexOf(QuantifiableVariable var, + ImmutableList list) { + int res = 0; + while (!list.isEmpty()) { + if (list.head() == var) { + return res; + } + ++res; + list = list.tail(); + } + return -1; + } + + + private static NameAbstractionTable checkNat(NameAbstractionTable nat) { + if (nat == null) { + return new NameAbstractionTable(); + } + return nat; + } + + private static boolean descendRecursively(JTerm t0, JTerm t1, + ImmutableList ownBoundVars, + ImmutableList cmpBoundVars, + PosInTerm expectedFocus) { + + for (int i = 0; i < t0.arity(); i++) { + ImmutableList subOwnBoundVars = ownBoundVars; + ImmutableList subCmpBoundVars = cmpBoundVars; + + if (t0.varsBoundHere(i).size() != t1.varsBoundHere(i).size()) { + return false; + } + for (int j = 0; j < t0.varsBoundHere(i).size(); j++) { + final QuantifiableVariable ownVar = t0.varsBoundHere(i).get(j); + final QuantifiableVariable cmpVar = t1.varsBoundHere(i).get(j); + if (ownVar.sort() != cmpVar.sort()) { + return false; + } + + subOwnBoundVars = subOwnBoundVars.prepend(ownVar); + subCmpBoundVars = subCmpBoundVars.prepend(cmpVar); + } + + PosInTerm nextFocus = nextFocusPos(expectedFocus, i); + + boolean newConstraint = unifyHelp(t0.sub(i), t1.sub(i), + subOwnBoundVars, subCmpBoundVars, nextFocus); + + if (!newConstraint) { + return false; + } + } + + return true; + } + + private static @Nullable PosInTerm nextFocusPos(PosInTerm expectedFocus, int i) { + if(expectedFocus != null && !expectedFocus.isTopLevel() && expectedFocus.getIndexAt(0) == i) { + // we are on the path to the focus + return expectedFocus.lastN(expectedFocus.depth() - 1); + } else { + return null; + } + } + + public List> findTopLevelMatchesInSequent(Sequent sequent) { + List> matches = new ArrayList<>(); + for (SequentFormula sf : sequent.antecedent()) { + if (matchesToplevel(sf)) { + matches.add(new Pair<>(true, sf)); + } + } + for (SequentFormula sf : sequent.succedent()) { + if (matchesToplevel(sf)) { + matches.add(new Pair<>(false, sf)); + } + } + return matches; + } + + + public @Nullable Pair findUniqueToplevelMatchInSequent(Sequent sequent) { + List> matches = findTopLevelMatchesInSequent(sequent); + if (matches.size() != 1) { + return null; + } else { + return matches.getFirst(); + } + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/TermWithHoles.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/TermWithHoles.java new file mode 100644 index 00000000000..c52e1115c6d --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/TermWithHoles.java @@ -0,0 +1,147 @@ +/* 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.scripts; + +import de.uka.ilkd.key.control.AbstractProofControl; +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.ldt.JavaDLTheory; +import de.uka.ilkd.key.logic.JTerm; +import de.uka.ilkd.key.logic.NamespaceSet; +import de.uka.ilkd.key.logic.op.JFunction; +import de.uka.ilkd.key.logic.op.SortDependingFunction; +import de.uka.ilkd.key.logic.sort.GenericSort; +import de.uka.ilkd.key.nparser.KeYParser; +import de.uka.ilkd.key.nparser.KeyAst; +import de.uka.ilkd.key.nparser.ParsingFacade; +import de.uka.ilkd.key.nparser.builder.ExpressionBuilder; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.proof.Proof; +import de.uka.ilkd.key.proof.init.Profile; +import de.uka.ilkd.key.prover.impl.ApplyStrategy; +import de.uka.ilkd.key.scripts.meta.*; +import de.uka.ilkd.key.strategy.FocussedBreakpointRuleApplicationManager; +import de.uka.ilkd.key.strategy.Strategy; +import de.uka.ilkd.key.strategy.StrategyProperties; +import de.uka.ilkd.key.util.parsing.BuildingIssue; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.tree.ParseTree; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; +import org.key_project.logic.Name; +import org.key_project.logic.sort.AbstractSort; +import org.key_project.logic.sort.Sort; +import org.key_project.prover.engine.ProverCore; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.prover.strategy.RuleApplicationManager; +import org.key_project.util.collection.ImmutableArray; +import org.key_project.util.collection.ImmutableList; +import org.key_project.util.collection.ImmutableSLList; +import org.key_project.util.collection.ImmutableSet; +import org.key_project.util.java.StringUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.List; +import java.util.Objects; + +@NullMarked +public class TermWithHoles { + + private final JTerm term; + public TermWithHoles(JTerm term) { + this.term = Objects.requireNonNull(term); + } + + public static final Name HOLE_NAME = new Name("?"); + public static final Name HOLE_PREDICATE_NAME = new Name("?fml"); + public static final Name HOLE_SORT_DEP_NAME = new Name("?"); + public static final Name FOCUS_NAME = new Name("?focus"); + public static final Name ELLIPSIS_NAME = new Name("?find"); + + private static final Logger LOGGER = LoggerFactory.getLogger(TermWithHoles.class); + + public boolean matches(PosInOccurrence posInOccurrence) { + return getMatcher().matches(posInOccurrence); + } + + public boolean matchesToplevel(SequentFormula sf) { + return getMatcher().matchesToplevel(sf); + } + + public TermComparisonWithHoles getMatcher() { + return new TermComparisonWithHoles(term); + } + + private static class NothingSort extends AbstractSort { + private final Services services; + + public NothingSort(Services services) { + super(new Name("Nothing"), true); + this.services = services; + } + + @Override + public ImmutableSet extendsSorts() { + return ImmutableSet.from(services.getNamespaces().sorts().allElements()).remove(this); + } + + @Override + public boolean extendsTrans(Sort s) { + return true; + } + } + + public static TermWithHoles fromString(EngineState engineState, String str) { + KeYParser p = ParsingFacade.createParser(CharStreams.fromString(str)); + p.allowMatchId = true; + KeYParser.TermContext term = p.termEOF().term(); + p.getErrorReporter().throwException(); + return fromParserContext(engineState, term); + } + + public static TermWithHoles fromProofScriptExpression(EngineState engineState, KeYParser.ProofScriptExpressionContext ctx) throws ConversionException { + if(ctx.string_literal() != null) { + String text = StringUtil.stripQuotes(ctx.string_literal().getText()); + return fromString(engineState, text); + } else if(ctx.proofScriptCodeBlock() != null) { + throw new ConversionException("A block cannot be used as a term"); + } else if(ctx.seq() != null) { + throw new ConversionException("A sequent cannot be used as a term"); + } else { + return fromParserContext(engineState, ctx.getRuleContext(ParserRuleContext.class, 0)); + } + } + + public static TermWithHoles fromParserContext(EngineState state, ParseTree ctx) { + var expressionBuilder = + new ExpressionBuilder(state.getProof().getServices(), enrichState(state)); + expressionBuilder.setAbbrevMap(state.getAbbreviations()); + JTerm t = (JTerm) ctx.accept(expressionBuilder); + List warnings = expressionBuilder.getBuildingIssues(); + warnings.forEach(it -> LOGGER.warn("{}", it)); + warnings.clear(); + return new TermWithHoles(t); + } + + private static NamespaceSet enrichState(EngineState state) { + NamespaceSet ns = state.getProof().getServices().getNamespaces().copy(); + + // Sort Nothing as bottom sort + NothingSort nothing = new NothingSort(state.getProof().getServices()); + ns.sorts().add(nothing); + + ns.functions().addSafely(new JFunction(HOLE_NAME, nothing)); + ns.functions().addSafely(new JFunction(HOLE_PREDICATE_NAME, JavaDLTheory.FORMULA)); + ns.functions().addSafely(new JFunction(FOCUS_NAME, nothing, JavaDLTheory.ANY)); + ns.functions().addSafely(new JFunction(ELLIPSIS_NAME, nothing, JavaDLTheory.ANY)); + GenericSort g = new GenericSort(new Name("G")); + ns.functions().addSafely(SortDependingFunction.createFirstInstance(g, HOLE_SORT_DEP_NAME, g, + new Sort[0], false)); + return ns; + } + +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/UnhideCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/UnhideCommand.java index b7622eb9a78..c4d902aa13a 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/UnhideCommand.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/UnhideCommand.java @@ -11,6 +11,7 @@ import de.uka.ilkd.key.rule.NoPosTacletApp; import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; import org.key_project.logic.Term; import org.key_project.logic.op.sv.SchemaVariable; import org.key_project.prover.proof.rulefilter.TacletFilter; @@ -21,7 +22,7 @@ import org.checkerframework.checker.nullness.qual.MonotonicNonNull; /** - * Proof script command to insert a formula hidden earlier in the proof. + * Proof script command to insert formulas hidden earlier in the proof. * * Usage: * @@ -83,7 +84,12 @@ public String getName() { return "unhide"; } + @Documentation(category = "Control", value = """ + The unhide command re-inserts formulas that have been hidden earlier in the proof using the hide command. + It takes a sequent as parameter and re-inserts all formulas in this sequent that have been hidden earlier. + """) public static class Parameters { + @Documentation("The sequent containing the formulas to be re-inserted. Placeholders are allowed.") @Argument public @MonotonicNonNull Sequent sequent; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/WitnessCommand.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/WitnessCommand.java new file mode 100644 index 00000000000..c5d7f94721e --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/WitnessCommand.java @@ -0,0 +1,138 @@ +/* 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.scripts; + +import de.uka.ilkd.key.java.Services; +import de.uka.ilkd.key.logic.*; +import de.uka.ilkd.key.logic.op.Quantifier; +import de.uka.ilkd.key.proof.Goal; +import de.uka.ilkd.key.rule.*; +import de.uka.ilkd.key.scripts.meta.Argument; +import de.uka.ilkd.key.scripts.meta.Documentation; +import de.uka.ilkd.key.scripts.meta.Option; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; +import org.key_project.logic.Name; +import org.key_project.logic.PosInTerm; +import org.key_project.logic.op.Operator; +import org.key_project.logic.op.sv.SchemaVariable; +import org.key_project.prover.sequent.PosInOccurrence; +import org.key_project.prover.sequent.SequentFormula; +import org.key_project.util.collection.Pair; + +import java.util.NoSuchElementException; +import java.util.Set; +import java.util.regex.Pattern; + +/** + * witness "\exists int x; phi(x)" as="x_12" + *

+ * witness "\forall int x; phi(x)" as="x_13" + *

+ * witness "\exists int x; phi(x)" as="x_14" cut=true + * + * Possibly with an assertion before to make sure that the formula is on the sequent. + * + * @author mulbrich + */ +public class WitnessCommand extends AbstractCommand { + + private static final Pattern GOOD_NAME = Pattern.compile("[a-zA-Z][a-zA-Z0-9_]*"); + private static final Name ANTEC_TACLET = new Name("exLeft"); + private static final Name SUCC_TACLET = new Name("allRight"); + + public WitnessCommand() { + super(Parameters.class); + } + + @Override + public String getName() { + return "witness"; + } + + @Override + public void execute(ScriptCommandAst ast) throws ScriptException, InterruptedException { + + Parameters params = state().getValueInjector().inject(new Parameters(), ast); + + Goal goal = state.getFirstOpenAutomaticGoal(); + Services services = state.getProof().getServices(); + + TermComparisonWithHoles comp = params.formula.getMatcher(); + + // First component: true for antecedent, false for succedent + Pair match = comp.findUniqueToplevelMatchInSequent(goal.node().sequent()); + if (match == null) { + throw new ScriptException("Cannot unique match the formula argument"); + } + + Operator op = match.second.formula().op(); + Operator expected = match.first ? Quantifier.EX : Quantifier.ALL; + if (op != expected) { + throw new ScriptException("Expected quantifier " + expected + ", but got " + op); + } + + if(!GOOD_NAME.matcher(params.as).matches()) { + throw new ScriptException("Invalid name: " + params.as); + } + + NamespaceSet nss = services.getNamespaces(); + Name asName = new Name(params.as); + if(nss.functions().lookup(asName) != null) { + throw new ScriptException("Name already used as function or predicate: " + params.as); + } + if(nss.programVariables().lookup(asName) != null) { + throw new ScriptException("Name already used as program variable: " + params.as); + } + + Name tacletName = match.first ? ANTEC_TACLET : SUCC_TACLET; + FindTaclet taclet = (FindTaclet) state.getProof().getEnv().getInitConfigForEnvironment() + .lookupActiveTaclet(tacletName); + PosInOccurrence pio = new PosInOccurrence(match.second, PosInTerm.getTopLevel(), match.first); + MatchConditions mc = new MatchConditions(); + TacletApp app = PosTacletApp.createPosTacletApp(taclet, mc, pio, services); + Set schemaVars = taclet.collectSchemaVars(); + app = app.addInstantiation(getSV(schemaVars, "u"), + services.getTermBuilder().tf().createTerm(match.second.formula().boundVars().get(0)), + true, services); + app = app.addInstantiation(getSV(schemaVars, "b"), match.second.formula().sub(0), true, services); + app = app.createSkolemConstant(params.as, getSV(schemaVars, "sk"), match.second.formula().boundVars().get(0).sort(), true, services); + + Goal g = state.getFirstOpenAutomaticGoal(); + g.apply(app); + } + + private static SchemaVariable getSV(Set schemaVars, String name) { + for (SchemaVariable schemaVar : schemaVars) { + if(schemaVar.name().toString().equals(name)) { + return schemaVar; + } + } + throw new NoSuchElementException("No schema variable with name " + name); + } + + @Documentation(category = "Fundamental", value = """ + Provides a witness symbol for an existential or universal quantifier. + The given formula must be present on the sequent. Placeholders are allowed. + The command fails if the formula cannot be uniquely matched on the sequent. + The witness symbol `as` must be a valid identifier and not already used as function, predicate, or + program variable name. The new function symbol is created as a Skolem constant. + + #### Example: + + If the sequent contains the formula `\\exists int x; x > 0` in the antecedent then the command + `witness "\\exists int x; x > 0" as="x_12"` will introduce the witness symbol `x_12` for which "x_12 > 0` + holds and is added to the antecedent. + """) + public static class Parameters { + @Documentation("The name of the witness symbol to be created.") + @Option(value = "as") + public @MonotonicNonNull String as; + + @Documentation("The formula containing the quantifier for which a witness should be provided. Placeholders are allowed.") + @Argument + public @MonotonicNonNull TermWithHoles formula; + } + +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Argument.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Argument.java index ef796c2c62e..a3e82ea5856 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Argument.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Argument.java @@ -16,6 +16,6 @@ /// Position of this argument in the positional argument list. /// /// @return a non-null string - /// @see ScriptCommandAst#positionalArgs() + /// @see de.uka.ilkd.key.scripts.ScriptCommandAst#positionalArgs() int value() default 0; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java index cced4bcc96d..e37627457ff 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ArgumentsLifter.java @@ -5,18 +5,21 @@ import java.lang.reflect.Field; import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Comparator; -import java.util.List; +import java.util.*; +import de.uka.ilkd.key.scripts.AbstractCommand; +import de.uka.ilkd.key.scripts.ProofScriptCommand; import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * @author Alexander Weigl * @version 1 (21.04.17) */ public final class ArgumentsLifter { + private static final String OPEN_BRACKET = "\u27e8"; + private static final String CLOSE_BRACKET = "\u27e9"; + private ArgumentsLifter() { } @@ -40,21 +43,21 @@ public static String generateCommandUsage(String commandName, Class parameter var sb = new StringBuilder(commandName); for (var meta : args) { sb.append(' '); - sb.append(meta.isRequired() ? '<' : '['); + if(!meta.isRequired() || meta.isFlag()) + sb.append("["); if (meta.isPositional()) { - sb.append(meta.getName()); + sb.append(OPEN_BRACKET + meta.getType().getSimpleName() + " (" + meta.getName() + ")" + CLOSE_BRACKET); } if (meta.isOption()) { sb.append(meta.getName()); - sb.append(": "); - sb.append(meta.getField().getType().getName()); + sb.append(":"); + sb.append(OPEN_BRACKET + meta.getField().getType().getSimpleName() + CLOSE_BRACKET); } if (meta.isFlag()) { sb.append(meta.getName()); - sb.append("[: true/false]"); } if (meta.isPositionalVarArgs()) { @@ -64,8 +67,9 @@ public static String generateCommandUsage(String commandName, Class parameter if (meta.isOptionalVarArgs()) { sb.append("%s...".formatted(meta.getName())); } - - sb.append(meta.isRequired() ? '>' : ']'); + + if(!meta.isRequired() || meta.isFlag()) + sb.append("]"); } @@ -76,48 +80,60 @@ public static String extractDocumentation(String command, Class commandClazz, Class parameterClazz) { StringBuilder sb = new StringBuilder(); + Deprecated dep = commandClazz.getAnnotation(Deprecated.class); + if(dep != null) { + sb.append("**Caution! This proof script command is deprecated, and may be removed soon!**\n\n"); + } + Documentation docCommand = commandClazz.getAnnotation(Documentation.class); if (docCommand != null) { sb.append(docCommand.value()); sb.append("\n\n"); } + if(parameterClazz == null) { + return sb.toString(); + } + Documentation docAn = parameterClazz.getAnnotation(Documentation.class); if (docAn != null) { sb.append(docAn.value()); sb.append("\n\n"); } - sb.append("Usage: ").append(generateCommandUsage(command, parameterClazz)) - .append("\n\n"); + sb.append("#### Usage: \n`").append(generateCommandUsage(command, parameterClazz)) + .append("`\n\n"); - final var args = getSortedProofScriptArguments(parameterClazz); + List args = getSortedProofScriptArguments(parameterClazz); - for (var meta : args) { + sb.append("#### Parameters:\n"); + for (ProofScriptArgument meta : args) { sb.append("\n\n"); if (meta.isPositional()) { - sb.append("* Argument %s (%s): %s".formatted( + sb.append("* `%s` *(%s%s positional argument, type %s)*:
%s".formatted( meta.getName(), - meta.getField().getType(), + meta.isRequired() ? "" : "optional ", + ordinalStr(meta.getArgumentPosition() + 1), + meta.getField().getType().getSimpleName(), meta.getDocumentation())); } if (meta.isOption()) { - sb.append("* Option %s (%s): %s".formatted( + sb.append("* `%s` *(%snamed option, type %s)*:
%s".formatted( meta.getName(), - meta.getField().getType(), + meta.isRequired() ? "" : "optional ", + meta.getField().getType().getSimpleName(), meta.getDocumentation())); } if (meta.isFlag()) { - sb.append("* Option %s [%s]: %s".formatted( + sb.append("* `%s` *(flag)*:
%s".formatted( meta.getName(), - meta.getFlag().defValue(), meta.getDocumentation())); } if (meta.isPositionalVarArgs()) { - sb.append("* %s... (%s): %s".formatted( + sb.append("* `%s...` (%s): %s".formatted( meta.getName(), meta.getPositionalVarargs().as(), meta.getPositionalVarargs().startIndex(), @@ -125,10 +141,9 @@ public static String extractDocumentation(String command, Class commandClazz, } if (meta.isOptionalVarArgs()) { - sb.append("* %s: %s... (%s): %s".formatted( - meta.getOptionalVarArgs(), - meta.getName(), - meta.getField().getType(), + sb.append("* `%s...`: *(options prefixed by `%s`, type %s)*:
%s".formatted( + meta.getName(), meta.getOptionalVarArgs().prefix(), + meta.getOptionalVarArgs().as().getSimpleName(), meta.getDocumentation())); } @@ -137,42 +152,71 @@ public static String extractDocumentation(String command, Class commandClazz, return sb.toString(); } - private static @NonNull List getSortedProofScriptArguments( - Class parameterClazz) { - Comparator optional = - Comparator.comparing(ProofScriptArgument::isOption); - Comparator positional = - Comparator.comparing(ProofScriptArgument::isPositional); - Comparator flagal = Comparator.comparing(ProofScriptArgument::isFlag); - Comparator allargsal = - Comparator.comparing(ProofScriptArgument::isPositionalVarArgs); - Comparator byRequired = - Comparator.comparing(ProofScriptArgument::isRequired); - Comparator byName = Comparator.comparing(ProofScriptArgument::getName); - - Comparator byPos = Comparator.comparing(it -> { - if (it.isPositionalVarArgs()) { - it.getPositionalVarargs().startIndex(); - } - if (it.isPositional()) { - it.getArgument().value(); + private static String ordinalStr(int post) { + if (post % 100 >= 11 && post % 100 <= 13) { + return post + "th"; + } + return switch (post % 10) { + case 1 -> post + "st"; + case 2 -> post + "nd"; + case 3 -> post + "rd"; + default -> post + "th"; + }; + } + + public static String extractCategory(Class commandClazz, @Nullable Class parameterClazz) { + Documentation docCommand = commandClazz.getAnnotation(Documentation.class); + if (docCommand != null && !docCommand.category().isBlank()) { + return docCommand.category(); + } + + if(parameterClazz != null) { + Documentation docAn = parameterClazz.getAnnotation(Documentation.class); + if (docAn != null && !docAn.category().isBlank()) { + return docAn.category(); } + } - return -1; - }); + return "Uncategorized"; + } - var comp = optional - .thenComparing(flagal) - .thenComparing(positional) - .thenComparing(allargsal) - .thenComparing(byRequired) - .thenComparing(byPos) - .thenComparing(byName); + private static @NonNull List getSortedProofScriptArguments( + Class parameterClazz) { +// Comparator optional = +// Comparator.comparing(ProofScriptArgument::isOption); +// Comparator positional = +// Comparator.comparing(ProofScriptArgument::isPositional); +// Comparator flagal = Comparator.comparing(ProofScriptArgument::isFlag); +// Comparator allargsal = +// Comparator.comparing(ProofScriptArgument::isPositionalVarArgs); +// Comparator byRequired = +// Comparator.comparing(ProofScriptArgument::isRequired); +// Comparator byName = Comparator.comparing(ProofScriptArgument::getName); +// +// Comparator byPos = Comparator.comparing(it -> { +// if (it.isPositionalVarArgs()) { +// it.getPositionalVarargs().startIndex(); +// } +// if (it.isPositional()) { +// it.getArgument().value(); +// } +// +// return -1; +// }); +// +// +// var comp = optional +// .thenComparing(flagal) +// .thenComparing(positional) +// .thenComparing(allargsal) +// .thenComparing(byRequired) +// .thenComparing(byPos) +// .thenComparing(byName); var args = Arrays.stream(parameterClazz.getDeclaredFields()) .map(ProofScriptArgument::new) - .sorted(comp) + .sorted(Comparator.comparing(ProofScriptArgument::orderString)) .toList(); return args; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Converter.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Converter.java index ec20894856f..209bd050b01 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Converter.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Converter.java @@ -6,16 +6,17 @@ /** * A {@link Converter} translates an instance of {@code R} to an instance of {@code T}. * - * @param + * @param the result type + * @param the source type * @author Alexander Weigl */ public interface Converter { /** - * Translates the textual representation given in {@code s} to an instance of {@code T}. + * Translates one representation given in {@code s} to an instance of {@code R}. * - * @param s a non-null string - * @return an corresponding instance of T - * @throws Exception if there is an error during the translation (format incorrent etc..) + * @param s a non-null argument to convert + * @return a corresponding instance of T after conversion + * @throws Exception if there is an error during the translation (format incorrect etc ...) */ R convert(T s) throws Exception; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Documentation.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Documentation.java index a4fe62a1766..e3c7a1f62f4 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Documentation.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/Documentation.java @@ -17,4 +17,5 @@ public @interface Documentation { /// @return a non-null string String value(); + String category() default ""; } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/InjectionException.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/InjectionException.java index 41b3735fe3e..d81dab26722 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/InjectionException.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/InjectionException.java @@ -30,4 +30,13 @@ public InjectionException(String message) { public InjectionException(String message, Throwable cause) { super(message, cause); } + + /** + * An injection exception with a cause to be displayed. + * + * @param cause the cause of the exception. + */ + public InjectionException(Throwable cause) { + super(cause); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ProofScriptArgument.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ProofScriptArgument.java index 66939fadcfc..02b11bf41e9 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ProofScriptArgument.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ProofScriptArgument.java @@ -138,4 +138,17 @@ public boolean hasNoAnnotation() { public Class getType() { return field.getType(); } + + /** + * This is used for ordering arguments in the usage string and documention. + * + * A twodigit number is used for positional arguments. 'M' for mandatory, 'O' for optional flags and options. + */ + public String orderString() { + if(isPositional()) + return "%02d".formatted(getArgumentPosition()); + if(isRequired()) + return "M " + getName(); + return "O " + getName(); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/UnknownArgumentException.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/UnknownArgumentException.java new file mode 100644 index 00000000000..3cbd1035958 --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/UnknownArgumentException.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.scripts.meta; + +/** + * Signals if an unknown/unexpected argument has been provided for injection. + * + * @author Mattias Ulbrich + */ +public class UnknownArgumentException extends InjectionException { + + /** + * An argument required exception with no cause (to display). + * + * @param message the respective String message to be passed. + */ + public UnknownArgumentException(String message) { + super(message); + } +} diff --git a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ValueInjector.java b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ValueInjector.java index 4d66ae758a2..ea9088b1023 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ValueInjector.java +++ b/key.core/src/main/java/de/uka/ilkd/key/scripts/meta/ValueInjector.java @@ -6,9 +6,12 @@ import java.util.*; import java.util.stream.Collectors; +import de.uka.ilkd.key.nparser.KeYParser; import de.uka.ilkd.key.scripts.ProofScriptCommand; import de.uka.ilkd.key.scripts.ScriptCommandAst; +import org.key_project.util.java.IntegerUtil; + import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; @@ -46,6 +49,10 @@ private record ConverterKey( Class source, Class target) { } + public interface VerifyableParameters { + void verifyParameters() throws IllegalArgumentException, InjectionException; + } + /** * Injects the given {@code arguments} in the {@code obj}. For more details see * {@link #inject(Object, ScriptCommandAst)} @@ -61,8 +68,7 @@ private record ConverterKey( * @throws ConversionException an converter could not translate the given value in arguments */ public static T injection(ProofScriptCommand command, @NonNull T obj, - ScriptCommandAst arguments) throws ArgumentRequiredException, - InjectionReflectionException, NoSpecifiedConverterException, ConversionException { + ScriptCommandAst arguments) throws InjectionException { return getInstance().inject(obj, arguments); } @@ -122,12 +128,41 @@ public static ValueInjector createDefault() { * @see Flag */ public T inject(T obj, ScriptCommandAst arguments) - throws ConversionException, InjectionReflectionException, NoSpecifiedConverterException, - ArgumentRequiredException { + throws InjectionException { List meta = ArgumentsLifter.inferScriptArguments(obj.getClass()); + Set handledOptions = new HashSet<>(); for (ProofScriptArgument arg : meta) { - injectIntoField(arg, arguments, obj); + handledOptions.addAll(injectIntoField(arg, arguments, obj)); + } + + Optional unhandled = arguments.namedArgs().keySet().stream() + .filter(it -> !handledOptions.contains(it)) + .findAny(); + if (unhandled.isPresent()) { + throw new UnknownArgumentException(String.format( + "Unknown option %s (with value %s) was provided. For command: '%s'", + unhandled.get(), + arguments.namedArgs().get(unhandled.get()), + arguments.commandName())); + } + + Optional unhandledPos = IntegerUtil.indexRangeOf(arguments.positionalArgs()) + .stream() + .filter(it -> !handledOptions.contains(it)) + .findAny(); + if (unhandledPos.isPresent()) { + throw new UnknownArgumentException(String.format( + "Unexpected positional argument or flag provided: %s", + arguments.positionalArgs().get(unhandledPos.get()))); + } + + if (obj instanceof VerifyableParameters vp) { + try { + vp.verifyParameters(); + } catch (IllegalArgumentException e) { + throw new InjectionException(e); + } } return obj; @@ -149,19 +184,22 @@ private Map getStringMap(Object obj, ProofScriptArgument vararg) } } - private void injectIntoField(ProofScriptArgument meta, ScriptCommandAst args, Object obj) + private List injectIntoField(ProofScriptArgument meta, ScriptCommandAst args, Object obj) throws InjectionReflectionException, ArgumentRequiredException, ConversionException, NoSpecifiedConverterException { Object val = null; + List handled = List.of(); if (meta.isPositional()) { final var idx = meta.getArgumentPosition(); if (idx < args.positionalArgs().size()) { val = args.positionalArgs().get(idx); + handled = List.of(idx); } } if (meta.isPositionalVarArgs()) { val = args.positionalArgs(); + handled = IntegerUtil.indexRangeOf(args.positionalArgs()); } if (meta.isOptionalVarArgs()) { @@ -175,28 +213,30 @@ private void injectIntoField(ProofScriptArgument meta, ScriptCommandAst args, Ob } } val = result; + handled = new ArrayList<>(result.keySet()); } if (meta.isOption()) { val = args.namedArgs().get(meta.getName()); + handled = List.of(meta.getName()); } if (meta.isFlag()) { val = args.namedArgs().get(meta.getName()); - System.out.println("X" + val + " " + args.namedArgs() + " " + meta.getName()); if (val == null) { // can also be given w/o colon or equal sign, e.g., "command hide;" - var stringStream = args.positionalArgs().stream() - .map(it -> { - try { - return convert(it, String.class); - } catch (NoSpecifiedConverterException | ConversionException e) { - return ""; - } - }); - // val == true iff the name of the flag appear as a positional argument. - val = stringStream.anyMatch(it -> Objects.equals(it, meta.getName())); - System.out.println(val); + int argNo = 0; + for (Object arg : args.positionalArgs()) { + String s = convert(arg, String.class); + if (s.equals(meta.getName())) { + val = Boolean.TRUE; + handled = List.of(argNo); + break; + } + argNo++; + } + } else { + handled = List.of(meta.getName()); } } @@ -206,7 +246,7 @@ private void injectIntoField(ProofScriptArgument meta, ScriptCommandAst args, Ob if (meta.isRequired() && meta.getField().get(obj) == null) { throw new ArgumentRequiredException(String.format( "Argument %s (of type %s) is required, but %s was given. For command class: '%s'", - meta.getName(), meta.getField().getType(), null, + meta.getName(), meta.getField().getType().getSimpleName(), null, meta.getField().getDeclaringClass())); } } else { @@ -216,7 +256,7 @@ private void injectIntoField(ProofScriptArgument meta, ScriptCommandAst args, Ob } catch (IllegalAccessException e) { throw new InjectionReflectionException("Could not inject values via reflection", e); } - + return handled; } private Object convert(ProofScriptArgument meta, Object val) @@ -249,16 +289,13 @@ private Object convert(ProofScriptArgument meta, Object val) public T convert(Class targetType, Object val) throws NoSpecifiedConverterException, ConversionException { var converter = (Converter) getConverter(targetType, val.getClass()); - if (converter == null) { - throw new NoSpecifiedConverterException( - "No converter registered for class: " + targetType + " from " + val.getClass()); - } try { return (T) converter.convert(val); } catch (Exception e) { throw new ConversionException( String.format("Could not convert value '%s' from type '%s' to type '%s'", - val, val.getClass(), targetType), + ScriptCommandAst.asReadableString(val), + val.getClass().getSimpleName(), targetType.getSimpleName()), e); } } @@ -268,10 +305,6 @@ public T convert(Object val, Class type) @SuppressWarnings("unchecked") var converter = (Converter) getConverter(type, val.getClass()); - if (converter == null) { - throw new NoSpecifiedConverterException( - "No converter registered for class: " + type + " from " + val.getClass(), null); - } try { return converter.convert(val); } catch (Exception e) { @@ -301,17 +334,24 @@ public void addConverter(Converter conv) { /** * Finds a converter for the given class. * - * @param an arbitrary type - * @param ret a non-null class - * @param arg - * @return null or a suitable converter (registered) converter for the requested class. + * @param the result type + * @param the source type + * @param ret the result type class + * @param arg the source type class + * @return a suitable converter (registered) converter for the requested class. null if no such + * converter is known. */ @SuppressWarnings("unchecked") - public @Nullable Converter getConverter(Class ret, Class arg) { - if (ret == arg) { + public @NonNull Converter getConverter(Class ret, Class arg) throws NoSpecifiedConverterException { + if (ret.isAssignableFrom(arg)) { return (T it) -> (R) it; } - return (Converter) converters.get(new ConverterKey<>(ret, arg)); + Converter result = (Converter) converters.get(new ConverterKey<>(ret, arg)); + if (result == null) { + throw new NoSpecifiedConverterException( + "No converter registered for class: " + ret.getName() + " from " + arg.getName()); + } + return result; } @Override diff --git a/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/pretranslation/TextualJMLAssertStatement.java b/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/pretranslation/TextualJMLAssertStatement.java index 849b3f45190..a1856d07fbc 100755 --- a/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/pretranslation/TextualJMLAssertStatement.java +++ b/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/pretranslation/TextualJMLAssertStatement.java @@ -8,18 +8,28 @@ import org.key_project.util.collection.ImmutableSLList; import org.antlr.v4.runtime.RuleContext; +import org.jspecify.annotations.Nullable; /** * A JML assert/assume statement. */ public class TextualJMLAssertStatement extends TextualJMLConstruct { private final KeyAst.Expression context; + private final String optLabel; + private final KeyAst.@Nullable JMLProofScript assertionProof; private final Kind kind; public TextualJMLAssertStatement(Kind kind, KeyAst.Expression clause) { + this(kind, clause, null, null); + } + + public TextualJMLAssertStatement(Kind kind, KeyAst.Expression clause, + KeyAst.@Nullable JMLProofScript assertionProof, String optLabel) { super(ImmutableSLList.nil(), kind.toString() + " " + clause); this.kind = kind; this.context = clause; + this.assertionProof = assertionProof; + this.optLabel = optLabel; } public KeyAst.Expression getContext() { @@ -63,6 +73,10 @@ public Kind getKind() { return kind; } + public String getOptLabel() { + return optLabel; + } + public enum Kind { ASSERT("assert"), ASSUME("assume"); @@ -77,4 +91,8 @@ public String toString() { return name; } } + + public KeyAst.@Nullable JMLProofScript getAssertionProof() { + return assertionProof; + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/JMLSpecFactory.java b/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/JMLSpecFactory.java index 4bad9d0db32..b4178cdae02 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/JMLSpecFactory.java +++ b/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/JMLSpecFactory.java @@ -1505,7 +1505,7 @@ private ProgramVariableCollection createProgramVariablesForStatement(Statement s * @param pm the enclosing method */ public void translateJmlAssertCondition(final JmlAssert jmlAssert, final IProgramMethod pm) { - final var pv = createProgramVariablesForStatement(jmlAssert, pm); + final ProgramVariableCollection pv = createProgramVariablesForStatement(jmlAssert, pm); var io = new JmlIO(services).context(Context.inMethod(pm, tb)) .selfVar(pv.selfVar) .parameters(pv.paramVars) @@ -1513,10 +1513,12 @@ public void translateJmlAssertCondition(final JmlAssert jmlAssert, final IProgra .exceptionVariable(pv.excVar) .atPres(pv.atPres) .atBefore(pv.atBefores); - JTerm expr = io.translateTerm(jmlAssert.getCondition()); + ImmutableList varsInProof = jmlAssert.collectVariablesInProof(io); + io.parameters(pv.paramVars.prepend(varsInProof)); + ImmutableList terms = jmlAssert.collectTerms().map(io::translateTerm); services.getSpecificationRepository().addStatementSpec( jmlAssert, - new SpecificationRepository.JmlStatementSpec(pv, ImmutableList.of(expr))); + new SpecificationRepository.JmlStatementSpec(pv, terms)); } public @Nullable String checkSetStatementAssignee(JTerm assignee) { diff --git a/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/ProgramVariableCollection.java b/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/ProgramVariableCollection.java index a431c2cef22..1c0ee8f2073 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/ProgramVariableCollection.java +++ b/key.core/src/main/java/de/uka/ilkd/key/speclang/jml/translation/ProgramVariableCollection.java @@ -79,7 +79,7 @@ public ProgramVariableCollection(LocationVariable selfVar, * * @param selfVar {@code self} * @param paramVars the list of method parameters if the textual specification case is a method - * contract. + * contract. May also contain the local variables visible at a statement. * @param resultVar {@code result} * @param excVar {@code exception} * @param atPreVars a map from every variable {@code var} to {@code \old(var)}. diff --git a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java index 80b93be0ce9..35fba582253 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java +++ b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlFacade.java @@ -3,16 +3,14 @@ * SPDX-License-Identifier: GPL-2.0-only */ package de.uka.ilkd.key.speclang.njml; +import java.io.IOException; import java.net.URI; import de.uka.ilkd.key.java.Position; import de.uka.ilkd.key.speclang.PositionedString; import de.uka.ilkd.key.util.parsing.SyntaxErrorReporter; -import org.antlr.v4.runtime.CharStream; -import org.antlr.v4.runtime.CharStreams; -import org.antlr.v4.runtime.CommonTokenStream; -import org.antlr.v4.runtime.ParserRuleContext; +import org.antlr.v4.runtime.*; import org.jspecify.annotations.NonNull; /** @@ -114,4 +112,17 @@ private static ParserRuleContext getExpressionContext(JmlLexer lexer) { p.getErrorReporter().throwException(); return ctx; } + + // FIXME Make sure this is removed. For testing only! + public static void main(String[] args) throws IOException { + String input = new String(System.in.readAllBytes()); + JmlLexer lexer = createLexer(input); + for (Token t : lexer.getAllTokens()) { + System.out.println(t.getText() + " " + t); + } + lexer = createLexer(input); + var parser = createParser(lexer); + var tree = parser.methodlevel_comment(); + System.out.println(tree.toStringTree(parser)); + } } diff --git a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlIO.java b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlIO.java index 7fce7c039aa..71c5b5f9097 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlIO.java +++ b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/JmlIO.java @@ -8,6 +8,7 @@ import de.uka.ilkd.key.java.Label; import de.uka.ilkd.key.java.Services; import de.uka.ilkd.key.java.abstraction.KeYJavaType; +import de.uka.ilkd.key.java.abstraction.Type; import de.uka.ilkd.key.logic.JTerm; import de.uka.ilkd.key.logic.label.OriginTermLabel; import de.uka.ilkd.key.logic.op.IObserverFunction; @@ -20,6 +21,7 @@ import de.uka.ilkd.key.util.InfFlowSpec; import de.uka.ilkd.key.util.mergerule.MergeParamsSpec; +import org.key_project.logic.sort.Sort; import org.key_project.util.collection.ImmutableList; import org.key_project.util.collection.ImmutableSLList; import org.key_project.util.collection.Pair; @@ -199,6 +201,22 @@ public JTerm translateTerm(ParserRuleContext expr) { } } + /** + * Interpret the given parse tree as an KeYJavaType in the current context. + * May return null if the KJT cannot be resolved. + */ + public @Nullable KeYJavaType translateType(JmlParser.TypespecContext ctx) { + Object interpreted = interpret(ctx); + return switch (interpreted) { + case SLExpression slExpression -> slExpression.getType(); + case Sort sort -> services.getJavaInfo().getKeYJavaType(sort); + case KeYJavaType kjt -> kjt; + case Type type -> services.getJavaInfo().getKeYJavaType(type); + default -> throw new IllegalArgumentException("Cannot translate to KeYJavaType: " + + interpreted + " of class " + interpreted.getClass()); + }; + } + /** * Interpret the given parse tree as an JML expression in the current context. Label is * attached. @@ -411,6 +429,4 @@ public void clearWarnings() { warnings = ImmutableSLList.nil(); } - // region - // endregion } diff --git a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/TextualTranslator.java b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/TextualTranslator.java index fa08e4cb385..2f57bcbb133 100644 --- a/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/TextualTranslator.java +++ b/key.core/src/main/java/de/uka/ilkd/key/speclang/njml/TextualTranslator.java @@ -520,8 +520,11 @@ public Object visitAssume_statement(JmlParser.Assume_statementContext ctx) { @Override public Object visitAssert_statement(JmlParser.Assert_statementContext ctx) { - TextualJMLAssertStatement b = new TextualJMLAssertStatement( - TextualJMLAssertStatement.Kind.ASSERT, new KeyAst.Expression(ctx.expression())); + TextualJMLAssertStatement b = + new TextualJMLAssertStatement(TextualJMLAssertStatement.Kind.ASSERT, + new KeyAst.Expression(ctx.expression()), + KeyAst.JMLProofScript.fromContext(ctx.assertionProof()), + ctx.label == null ? null : ctx.label.getText()); constructs = constructs.append(b); return null; } 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 c649de42ab2..a43cde3f0cd 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 @@ -436,9 +436,8 @@ public void write(Properties p) { } } - @Override - public synchronized Object clone() { + public synchronized StrategyProperties clone() { final Properties p = (Properties) super.clone(); final StrategyProperties sp = new StrategyProperties(); sp.putAll(p); diff --git a/key.core/src/main/java/de/uka/ilkd/key/util/ANTLRUtil.java b/key.core/src/main/java/de/uka/ilkd/key/util/ANTLRUtil.java new file mode 100644 index 00000000000..c4be36280ae --- /dev/null +++ b/key.core/src/main/java/de/uka/ilkd/key/util/ANTLRUtil.java @@ -0,0 +1,96 @@ +package de.uka.ilkd.key.util; + +import java.util.ArrayList; +import java.util.List; + +import org.antlr.v4.runtime.Token; +import org.antlr.v4.runtime.tree.ParseTree; +import org.antlr.v4.runtime.tree.TerminalNode; +import org.jspecify.annotations.NullMarked; + +/** + * Utility to reconstruct the original source text from a given ANTLR4 parse tree. + * + * It is not 100% accurate, but should be good enough for most use cases. + * + * @author Mattias Ulbrich, or rather ChatGPT 5 mini + */ +@NullMarked +public final class ANTLRUtil { + private ANTLRUtil() { } + + /** + * Reconstructs the original source text from a given ANTLR4 parse tree. + */ + public static String reconstructOriginal(ParseTree tree) { + List terminals = new ArrayList<>(); + collectTerminals(tree, terminals); + + StringBuilder sb = new StringBuilder(); + int curLine = -1; + int curCol = 0; + + for (TerminalNode tn : terminals) { + Token t = tn.getSymbol(); + if (t == null) continue; + if (t.getType() == Token.EOF) continue; + + if(curLine == -1) { + // use first token to initialize line and column + curCol = t.getCharPositionInLine(); + curLine = t.getLine(); + } + + int line = t.getLine(); + int col = t.getCharPositionInLine(); + + // add newlines to reach the desired line + while (curLine < line) { + sb.append('\n'); + curLine++; + curCol = 0; + } + + // add spaces to reach the desired column + int pad = col - curCol; + if (pad > 0) { + sb.append(" ".repeat(pad)); + curCol = col; + } + + String text = t.getText(); + if (text == null) text = ""; + + sb.append(text); + + // update current position + int newlines = countOccurrences(text, '\n'); + if (newlines > 0) { + curLine += newlines; + int lastNl = text.lastIndexOf('\n'); + curCol = text.length() - lastNl - 1; + } else { + curCol += text.length(); + } + } + + return sb.toString(); + } + + private static void collectTerminals(ParseTree node, List out) { + if (node == null) return; + if (node instanceof TerminalNode) { + out.add((TerminalNode) node); + return; + } + for (int i = 0; i < node.getChildCount(); i++) { + collectTerminals(node.getChild(i), out); + } + } + + private static int countOccurrences(String s, char c) { + int cnt = 0; + for (int i = 0; i < s.length(); i++) if (s.charAt(i) == c) cnt++; + return cnt; + } +} \ No newline at end of file diff --git a/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.macros.ProofMacro b/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.macros.ProofMacro index d8125e5c21f..cc3c7df0612 100644 --- a/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.macros.ProofMacro +++ b/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.macros.ProofMacro @@ -19,6 +19,7 @@ de.uka.ilkd.key.macros.PropositionalExpansionMacro # de.uka.ilkd.key.macros.PropositionalExpansionWithSimplificationMacro de.uka.ilkd.key.macros.FullPropositionalExpansionMacro de.uka.ilkd.key.macros.TryCloseMacro +de.uka.ilkd.key.macros.TryCloseSideBranchesMacro de.uka.ilkd.key.macros.FinishSymbolicExecutionMacro de.uka.ilkd.key.macros.AutoMacro #de.uka.ilkd.key.macros.FinishSymbolicExecutionUntilJoinPointMacro @@ -30,3 +31,6 @@ de.uka.ilkd.key.macros.OneStepProofMacro de.uka.ilkd.key.macros.WellDefinednessMacro de.uka.ilkd.key.macros.UpdateSimplificationMacro de.uka.ilkd.key.macros.TranscendentalFloatSMTMacro +de.uka.ilkd.key.macros.ScriptAwareMacro +de.uka.ilkd.key.macros.ScriptAwarePrepMacro +de.uka.ilkd.key.macros.SymbolicExecutionOnlyMacro diff --git a/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.scripts.ProofScriptCommand b/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.scripts.ProofScriptCommand index ad6e048bebc..1324c9d9224 100644 --- a/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.scripts.ProofScriptCommand +++ b/key.core/src/main/resources/META-INF/services/de.uka.ilkd.key.scripts.ProofScriptCommand @@ -7,6 +7,8 @@ de.uka.ilkd.key.scripts.MacroCommand de.uka.ilkd.key.scripts.FocusCommand de.uka.ilkd.key.scripts.AutoCommand de.uka.ilkd.key.scripts.CutCommand +de.uka.ilkd.key.scripts.ObtainCommand +# de.uka.ilkd.key.scripts.AssertCommand # it is an alias for CutCommand now de.uka.ilkd.key.scripts.SetCommand de.uka.ilkd.key.scripts.SetEchoCommand de.uka.ilkd.key.scripts.SetFailOnClosedCommand @@ -17,6 +19,7 @@ de.uka.ilkd.key.scripts.LeaveCommand de.uka.ilkd.key.scripts.TryCloseCommand de.uka.ilkd.key.scripts.ExitCommand de.uka.ilkd.key.scripts.InstantiateCommand +de.uka.ilkd.key.scripts.WitnessCommand de.uka.ilkd.key.scripts.SelectCommand de.uka.ilkd.key.scripts.ScriptCommand de.uka.ilkd.key.scripts.LetCommand @@ -25,11 +28,16 @@ de.uka.ilkd.key.scripts.SaveNewNameCommand de.uka.ilkd.key.scripts.SchemaVarCommand de.uka.ilkd.key.scripts.JavascriptCommand de.uka.ilkd.key.scripts.SkipCommand +de.uka.ilkd.key.scripts.OneStepSimplifierCommand +de.uka.ilkd.key.scripts.DependencyContractCommand de.uka.ilkd.key.scripts.AxiomCommand de.uka.ilkd.key.scripts.AssumeCommand # does not exist? # de.uka.ilkd.key.macros.scripts.SettingsCommand -de.uka.ilkd.key.scripts.AssertCommand +de.uka.ilkd.key.scripts.ExpandDefCommand +de.uka.ilkd.key.scripts.AssertOpenGoalsCommand de.uka.ilkd.key.scripts.RewriteCommand de.uka.ilkd.key.scripts.AllCommand de.uka.ilkd.key.scripts.HideCommand de.uka.ilkd.key.scripts.UnhideCommand +de.uka.ilkd.key.scripts.BranchesCommand +de.uka.ilkd.key.scripts.CheatCommand \ No newline at end of file diff --git a/key.core/src/main/resources/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.admittedRuleSets.txt b/key.core/src/main/resources/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.admittedRuleSets.txt new file mode 100644 index 00000000000..3288342e4c1 --- /dev/null +++ b/key.core/src/main/resources/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.admittedRuleSets.txt @@ -0,0 +1,8 @@ +rulesets +alpha +simplify_prog_subset +simplify_prog +simplify_autoname +executeIntegerAssignment +simplify_expression +loop_scope_inv_taclet diff --git a/key.core/src/main/resources/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.admittedRules.txt b/key.core/src/main/resources/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.admittedRules.txt new file mode 100644 index 00000000000..2acdd87a049 --- /dev/null +++ b/key.core/src/main/resources/de/uka/ilkd/key/macros/SymbolicExecutionOnlyMacro.admittedRules.txt @@ -0,0 +1,4 @@ +rules +ifUnfold +ifSplit +ifElseSplit diff --git a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/firstOrderRules.key b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/firstOrderRules.key index ef14af1896f..599a957014f 100644 --- a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/firstOrderRules.key +++ b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/firstOrderRules.key @@ -215,6 +215,11 @@ \heuristics(semantics_blasting) }; + intro { + \varcond(\newDependingOn(sk, t)) + \add(t = sk ==>) + }; + \lemma eqTermCut { \find(t) diff --git a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaHeader.key b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaHeader.key index 810c4692342..5b59a4205f2 100644 --- a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaHeader.key +++ b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaHeader.key @@ -28,4 +28,9 @@ alpha alpha::cast(any); boolean alpha::exactInstance(any); boolean alpha::instance(any); + // alpha alpha::_; // for matching in scripts +} + +\predicates { + // __; // for matching scripts } diff --git a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaRules.key b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaRules.key index d9633b05f09..13f4bc3fb39 100644 --- a/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaRules.key +++ b/key.core/src/main/resources/de/uka/ilkd/key/proof/rules/javaRules.key @@ -500,16 +500,26 @@ tryCatchThrow { \find(\modality{#allmodal}{.. try { throw #se; #slist } catch ( #t #v0 ) { #slist1 } ...}\endmodality (post)) - \replacewith(\modality{#allmodal}{.. if ( #se == null ) { - try { throw new java.lang.NullPointerException (); } - catch ( #t #v0 ) { #slist1 } - } else if ( #se instanceof #t ) { - #t #v0; - #v0 = (#t) #se; - #slist1 - } else { - throw #se; - } ...}\endmodality (post)) + + // Case NPE: throwing null fails: + "Null reference in throw": + \replacewith(\modality{#allmodal}{.. + try { throw new java.lang.NullPointerException (); } + catch ( #t #v0 ) { #slist1 } ... }\endmodality (post)) + \add( #se = null ==> ) ; + + // Case catch or throw: + "Normal execution" [main]: + \replacewith(\modality{#allmodal}{.. + if ( #se instanceof #t ) { + #t #v0; + #v0 = (#t) #se; + #slist1 + } else { + throw #se; + } ...}\endmodality (post)) + \add( ==> #se = null ) + \heuristics(simplify_prog) \displayname "tryCatchThrow" }; diff --git a/key.core/src/test/java/de/uka/ilkd/key/logic/TestLocalSymbols.java b/key.core/src/test/java/de/uka/ilkd/key/logic/TestLocalSymbols.java index 68e6f162503..31e2bfd7a85 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/logic/TestLocalSymbols.java +++ b/key.core/src/test/java/de/uka/ilkd/key/logic/TestLocalSymbols.java @@ -135,8 +135,8 @@ public void testDoubleInstantiation() throws Exception { Proof proof = env.getLoadedProof(); var script = env.getProofScript(); - ProofScriptEngine pse = new ProofScriptEngine(script); - pse.execute(null, proof); + ProofScriptEngine pse = new ProofScriptEngine(proof); + pse.execute(null, script); ImmutableList openGoals = proof.openGoals(); assert openGoals.size() == 1; diff --git a/key.core/src/test/java/de/uka/ilkd/key/proof/proverules/ProveRulesTest.java b/key.core/src/test/java/de/uka/ilkd/key/proof/proverules/ProveRulesTest.java index 88e87ee5f9e..71cbe36a283 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/proof/proverules/ProveRulesTest.java +++ b/key.core/src/test/java/de/uka/ilkd/key/proof/proverules/ProveRulesTest.java @@ -69,8 +69,8 @@ public void loadTacletProof(String tacletName, Taclet taclet, @Nullable Path pro KeyAst.ProofScript script = env.getProofScript(); if (script != null) { - ProofScriptEngine pse = new ProofScriptEngine(script); - pse.execute(env.getUi(), proof); + ProofScriptEngine pse = new ProofScriptEngine(proof); + pse.execute(env.getUi(), script); } assertTrue(proof.closed(), "Taclet proof of taclet " + tacletName + " did not close."); diff --git a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java index c255c3a1308..02a252f87b7 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java +++ b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProofCollections.java @@ -355,15 +355,12 @@ public static ProofCollection automaticJavaDL() throws IOException { g.provable("heap/removeDups/contains.key"); g.provable("heap/removeDups/removeDup.key"); g.provable("heap/saddleback_search/Saddleback_search.key"); - // TODO: Make BoyerMoore run automatically, not only loading proofs. Need proofs scripts for - // that. - g.loadable("heap/BoyerMoore/BM(BM__bm((I)).JML normal_behavior operation contract.0.proof"); - g.loadable( - "heap/BoyerMoore/BM(BM__count((I,_bigint,_bigint)).JML accessible clause.0.proof"); - g.loadable( - "heap/BoyerMoore/BM(BM__count((I,_bigint,_bigint)).JML model_behavior operation contract.0.proof"); - g.loadable( - "heap/BoyerMoore/BM(BM__monoLemma((I,int,int)).JML normal_behavior operation contract.0.proof"); + // DONE: Make BoyerMoore run automatically, not only loading proofs. Need proofs scripts for + // that. YESSS, it runs with scripts now ... + g.provable("heap/BoyerMoore/BM.bm.key"); + g.provable("heap/BoyerMoore/BM.count.accessible.key"); + g.provable("heap/BoyerMoore/BM.count.key"); + g.provable("heap/BoyerMoore/BM.monoLemma.key"); g = c.group("quicksort"); g.setLocalSettings("[Choice]DefaultChoices=moreSeqRules-moreSeqRules:on"); diff --git a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProveTest.java b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProveTest.java index 7ccecc800fb..712e3ea75cb 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProveTest.java +++ b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/ProveTest.java @@ -190,8 +190,8 @@ private void autoMode(KeYEnvironment env, Proof loa env.getProofControl().startAndWaitForAutoMode(loadedProof); } else { // ... script - ProofScriptEngine pse = new ProofScriptEngine(script); - pse.execute(env.getUi(), env.getLoadedProof()); + ProofScriptEngine pse = new ProofScriptEngine(env.getLoadedProof()); + pse.execute(env.getUi(), script); } } diff --git a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/proofcollection/TestFile.java b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/proofcollection/TestFile.java index 79df3f4b495..1cf3e073883 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/proofcollection/TestFile.java +++ b/key.core/src/test/java/de/uka/ilkd/key/proof/runallproofs/proofcollection/TestFile.java @@ -268,8 +268,8 @@ protected void autoMode(KeYEnvironment env, Proof l env.getProofControl().startAndWaitForAutoMode(loadedProof); } else { // ... script - ProofScriptEngine pse = new ProofScriptEngine(script); - pse.execute(env.getUi(), env.getLoadedProof()); + ProofScriptEngine pse = new ProofScriptEngine(env.getLoadedProof()); + pse.execute(env.getUi(), script); } } diff --git a/key.core/src/test/java/de/uka/ilkd/key/scripts/DocumentationGenerator.java b/key.core/src/test/java/de/uka/ilkd/key/scripts/DocumentationGenerator.java new file mode 100644 index 00000000000..a70ab5de2ed --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/scripts/DocumentationGenerator.java @@ -0,0 +1,98 @@ +package de.uka.ilkd.key.scripts; + +import de.uka.ilkd.key.util.KeYResourceManager; + +import java.io.FileNotFoundException; +import java.util.*; + +public class DocumentationGenerator { + + private static String branch; + private static String sha1; + + public static void main(String[] args) throws FileNotFoundException { + + if(args.length > 0) { + System.err.println("Redirecting output to " + args[0]); + System.setOut(new java.io.PrintStream(args[0])); + } + + printHeader(); + + Set> commands = ProofScriptEngine.loadCommands().entrySet(); + Map>> commandsByCategory = new TreeMap<>(); + + for (Map.Entry entry : commands) { + String category = entry.getValue().getCategory(); + if (category == null) { + category = "Uncategorized"; + } + commandsByCategory.computeIfAbsent(category, k -> new ArrayList<>()).add(entry); + } + + List categories = new ArrayList<>(commandsByCategory.keySet()); + categories.remove("Uncategorized"); + Collections.sort(categories); + + for (String category : categories) { + listCategory(category, commandsByCategory.get(category)); + } + + listCategory("Uncategorized", commandsByCategory.get("Uncategorized")); + + } + + private static void printHeader() { + branch = KeYResourceManager.getManager().getBranch(); + String version = KeYResourceManager.getManager().getVersion(); + sha1 = KeYResourceManager.getManager().getSHA1(); + + // This gets too technical. But this is for the key-docs repository. ... + System.out.printf(""" + + # Proof Script Commands + + This document lists all proof script commands available in the KeY system. + The general ideas of scripts, their syntax, and control flow are described + in the general documentation files on proof scripts. + + Field | Value + ----- | ----- + Generated on: | %s + Branch: | %s + Version: | %s + Commit: | %s + + The commands are organised into categories. Each command may have multiple aliases + under which it can be invoked. The first alias listed is the primary name of the command. + There *named* and *positional* arguments. Named arguments need to be prefixed by their name + and a colon. Positional arguments are given in the order defined by the command. + Optional arguments are enclosed in square brackets. + """, new Date(), branch, version, sha1); + } + + private static void listCategory(String category, List> proofScriptCommands) { + proofScriptCommands.sort(Map.Entry.comparingByKey()); + System.out.println("\n## Category *" + category + "*\n"); + for (Map.Entry entry : proofScriptCommands) { + System.out.println("
\n"); + if(entry.getKey().equals(entry.getValue().getName())) { + ProofScriptCommand command = entry.getValue(); + String link = "main".equals(branch) ? "main" : sha1; + System.out.println("### Command `" + command.getName() + "`\n\n"); + System.out.printf("[Source](https://github.com/KeYProject/key/blob/%s/key.core/src/main/java/%s.java)\n\n", + link, + command.getClass().getName().replace('.', '/') ); + System.out.println(command.getDocumentation() + "\n"); + if (command.getAliases().size() > 1) { + System.out.println("#### Aliases:\n" + String.join(", ", command.getAliases()) + "\n"); + } + } else { + System.out.println("### Command `" + entry.getKey() + "`\n"); + System.out.println("Alias for command [\u2192 `" + entry.getValue().getName() + "`](#command-" + entry.getValue().getName() + ")\n"); + } + } + } +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/scripts/FocusCommandTest.java b/key.core/src/test/java/de/uka/ilkd/key/scripts/FocusCommandTest.java index 612d69ac541..8d2469c4efb 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/scripts/FocusCommandTest.java +++ b/key.core/src/test/java/de/uka/ilkd/key/scripts/FocusCommandTest.java @@ -32,8 +32,8 @@ public void testSimpleSelection() throws Exception { KeYEnvironment env = KeYEnvironment.load(temp); Proof p = env.getLoadedProof(); var script = ParsingFacade.parseScript("macro \"nosplit-prop\"; focus (i=1 ==> i = 4);"); - ProofScriptEngine pse = new ProofScriptEngine(script); - pse.execute(env.getUi(), p); + ProofScriptEngine pse = new ProofScriptEngine(p); + pse.execute(env.getUi(), script); assertEquals(1, p.openGoals().size()); Goal g = p.openGoals().head(); @@ -53,8 +53,8 @@ public void testSelectionWithLabels() throws Exception { KeYEnvironment env = KeYEnvironment.load(temp); Proof p = env.getLoadedProof(); var script = ParsingFacade.parseScript("macro \"nosplit-prop\"; focus (i=1 ==> i = 3);"); - ProofScriptEngine pse = new ProofScriptEngine(script); - pse.execute(env.getUi(), p); + ProofScriptEngine pse = new ProofScriptEngine(p); + pse.execute(env.getUi(), script); assertEquals(1, p.openGoals().size()); Goal g = p.openGoals().head(); diff --git a/key.core/src/test/java/de/uka/ilkd/key/scripts/JmlScriptTest.java b/key.core/src/test/java/de/uka/ilkd/key/scripts/JmlScriptTest.java new file mode 100644 index 00000000000..fa53950a474 --- /dev/null +++ b/key.core/src/test/java/de/uka/ilkd/key/scripts/JmlScriptTest.java @@ -0,0 +1,132 @@ +package de.uka.ilkd.key.scripts; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; +import de.uka.ilkd.key.control.DefaultUserInterfaceControl; +import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.control.UserInterfaceControl; +import de.uka.ilkd.key.nparser.KeyAst; +import de.uka.ilkd.key.proof.io.ProblemLoaderControl; +import de.uka.ilkd.key.proof.io.ProofSaver; +import de.uka.ilkd.key.util.KeYConstants; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import recoder.util.Debug; + +import java.io.File; +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Comparator; +import java.util.Map; +import java.util.logging.LogManager; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class JmlScriptTest { + + private static final Path KEY_FILE; + private static final Logger LOGGER = LoggerFactory.getLogger(JmlScriptTest.class); + + // Set this to a specific case to only run that case for debugging + private static final String ONLY_CASE = System.getProperty("key.testJmlScript.only"); + // Set this to true to save the proof after running the script + private static final boolean SAVE_PROOF = false; + + static { + URL url = JmlScriptTest.class.getResource("jml/project.key"); + try { + KEY_FILE = Paths.get(url.toURI()); + } catch (URISyntaxException e) { + throw new RuntimeException(e); + } + } + + @ParameterizedTest(name = "{1}") + @MethodSource("filesProvider") + public void testJmlScript(Path path, String identifier) throws Exception { + + Parameters params = readParams(path); + + Path tmpDir = Files.createTempDirectory("key.jmltest."); + try { + Files.copy(path, tmpDir.resolve("Test.java")); + Path projectFile = tmpDir.resolve("project.key"); + Files.copy(KEY_FILE, projectFile); + KeYEnvironment env = KeYEnvironment.load(projectFile); + if(params.settings != null && !params.settings.isEmpty()) { + for (Map.Entry entry : params.settings.entrySet()) { + env.getLoadedProof().getSettings().getStrategySettings().getActiveStrategyProperties() + .setProperty(entry.getKey(), entry.getValue()); + } + } + KeyAst.ProofScript script = env.getProofScript(); + if (script != null) { + ProofScriptEngine pse = new ProofScriptEngine(env.getLoadedProof()); + pse.execute(env.getUi(), script); + } + + if(SAVE_PROOF) { + String filename = tmpDir.resolve("saved.proof").toString(); + ProofSaver saver = new ProofSaver(env.getLoadedProof(), filename, KeYConstants.INTERNAL_VERSION); + saver.save(); + LOGGER.info("Saved proof to {}", filename); + } + + if(params.shouldClose) { + Assertions.assertTrue(env.getLoadedProof().closed(), "Proof did not close."); + } else { + Assertions.assertFalse(env.getLoadedProof().closed(), "Proof closes unexpectedly."); + } + } finally { + // Uncomment the following line to delete the temporary directory after the test + if(params.deleteTmpDir && !SAVE_PROOF) { + LOGGER.info("Deleting temporary directory: {}", tmpDir); + Files.walk(tmpDir).sorted(Comparator.reverseOrder()).map(Path::toFile).forEach(File::delete); + } else { + LOGGER.info("Temporary directory retained for inspection: {}", tmpDir); + } + } + + } + + private static Parameters readParams(Path path) throws IOException { + String input = Files.lines(path).filter(l -> l.startsWith("//!")).map(l -> l.substring(3)) + .collect(Collectors.joining("\n")).trim(); + if(input.isEmpty()) { + return new Parameters(); + } + var objectMapper = new ObjectMapper(new YAMLFactory()); + objectMapper.findAndRegisterModules(); + return objectMapper.readValue(input, Parameters.class); + } + + public static Stream filesProvider() throws URISyntaxException, IOException { + URL jmlUrl = JmlScriptTest.class.getResource("jml"); + if (ONLY_CASE != null) { + return Stream.of(Arguments.of(Paths.get(jmlUrl.toURI()).resolve(ONLY_CASE), "single specified case: " + ONLY_CASE)); + } else { + return Files.list(Paths.get(jmlUrl.toURI())) + .filter(p -> p.toString().endsWith(".java")) + .map(p -> Arguments.of(p, p.getFileName().toString())); + } + } + + static class Parameters { + public boolean shouldClose = true; + public String method; + public String exception; + public boolean deleteTmpDir = true; + public Map settings; + } + + +} diff --git a/key.core/src/test/java/de/uka/ilkd/key/scripts/TestProofScriptCommand.java b/key.core/src/test/java/de/uka/ilkd/key/scripts/TestProofScriptCommand.java index 34ae12c0c6a..4eb88209a5c 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/scripts/TestProofScriptCommand.java +++ b/key.core/src/test/java/de/uka/ilkd/key/scripts/TestProofScriptCommand.java @@ -10,14 +10,21 @@ import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Stream; import de.uka.ilkd.key.control.DefaultUserInterfaceControl; import de.uka.ilkd.key.control.KeYEnvironment; +import de.uka.ilkd.key.nparser.KeyAst; import de.uka.ilkd.key.nparser.ParsingFacade; +import de.uka.ilkd.key.pp.LogicPrinter; +import de.uka.ilkd.key.pp.NotationInfo; import de.uka.ilkd.key.proof.Goal; import de.uka.ilkd.key.proof.Proof; import de.uka.ilkd.key.smt.newsmt2.MasterHandlerTest; +import org.jspecify.annotations.NonNull; import org.key_project.util.collection.ImmutableList; import com.fasterxml.jackson.databind.ObjectMapper; @@ -27,6 +34,8 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.MethodSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -35,15 +44,32 @@ * see {@link MasterHandlerTest} from where I copied quite a bit. */ public class TestProofScriptCommand { + + private static final String ONLY_CASES = System.getProperty("key.testProofScript.only"); + private static final Logger LOGGER = LoggerFactory.getLogger(TestProofScriptCommand.class); + public record TestInstance( String name, - String key, String script, @Nullable String exception, - String[] goals, Integer selectedGoal) { + String key, + String script, + @Nullable String exception, + String[] goals, + Integer selectedGoal) { } public static List data() throws IOException, URISyntaxException { var folder = Paths.get("src/test/resources/de/uka/ilkd/key/scripts/cases") .toAbsolutePath(); + + Predicate filter; + if(ONLY_CASES != null && !ONLY_CASES.isEmpty()) { + // if ONLY_CASES is set, only run those cases (comma separated) + Set only = Set.of(ONLY_CASES.split(" *, *")); + filter = p -> only.contains(p.getFileName().toString().substring(0, p.getFileName().toString().length() - 4)); + } else { + filter = p -> true; + } + try (var walker = Files.walk(folder)) { List files = walker.filter(it -> it.getFileName().toString().endsWith(".yml")).toList(); @@ -52,10 +78,16 @@ public static List data() throws IOException, URISyntaxException { List args = new ArrayList<>(files.size()); for (Path path : files) { + if(!filter.test(path)) { + continue; + } try { TestInstance instance = objectMapper.readValue(path.toFile(), TestInstance.class); - args.add(Arguments.of(instance)); + var name = instance.name == null ? + path.getFileName().toString().substring(0, path.getFileName().toString().length() - 4) : + instance.name; + args.add(Arguments.of(instance, name)); } catch (Exception e) { System.out.println(path); e.printStackTrace(); @@ -66,24 +98,25 @@ public static List data() throws IOException, URISyntaxException { } } - @ParameterizedTest + @ParameterizedTest(name = "{1}") @MethodSource("data") - void testProofScript(TestInstance data) throws Exception { - var name = data.name(); + void testProofScript(TestInstance data, String name) throws Exception { Path tmpKey = Files.createTempFile("proofscript_key_" + name, ".key"); + LOGGER.info("Testing {} using file", name, tmpKey); Files.writeString(tmpKey, data.key()); KeYEnvironment env = KeYEnvironment.load(tmpKey); Proof proof = env.getLoadedProof(); - var script = ParsingFacade.parseScript(data.script()); - ProofScriptEngine pse = new ProofScriptEngine(script); + KeyAst.ProofScript script = ParsingFacade.parseScript(data.script()); + ProofScriptEngine pse = new ProofScriptEngine(proof); boolean hasException = data.exception() != null; try { - pse.execute(env.getUi(), proof); + pse.execute(env.getUi(), script); } catch (ScriptException ex) { + ex.printStackTrace(); assertTrue(data.exception != null && !data.exception.isEmpty(), "An exception was not expected, but got " + ex.getMessage()); // weigl: fix spurious error on Windows machine due to different file endings. @@ -104,15 +137,20 @@ void testProofScript(TestInstance data) throws Exception { Assertions.assertEquals(expected, goals.size()); for (String expectedGoal : data.goals()) { - assertThat(goals.head().toString().trim()).isEqualTo(expectedGoal); + assertThat(normaliseSpace(goals.head().toString())).isEqualTo(expectedGoal); goals = goals.tail(); } if (data.selectedGoal() != null) { Goal goal = pse.getStateMap().getFirstOpenAutomaticGoal(); - assertThat(goal.toString().trim()).isEqualTo(data.goals()[data.selectedGoal()]); + assertThat(normaliseSpace(goal.toString())).isEqualTo(data.goals()[data.selectedGoal()]); } } } + // For some layout reasons the toString may add linebreaks and spaces + private static String normaliseSpace(String str) { + return str.replaceAll("\\s+", " ").trim(); + } + } diff --git a/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/RewriteTest.java b/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/RewriteTest.java index 19f094bfbc4..4c2e621c087 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/RewriteTest.java +++ b/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/RewriteTest.java @@ -42,8 +42,8 @@ public void testTransitive() assertNotNull(env); Proof p = env.getLoadedProof(); - ProofScriptEngine engine = new ProofScriptEngine(script); - engine.execute(env.getUi(), p); + ProofScriptEngine engine = new ProofScriptEngine(p); + engine.execute(env.getUi(), script); String firstOpenGoal = p.openGoals().head().sequent().toString(); String expectedSequent = "[equals(x,f),equals(x,z)]==>[equals(z,f)]"; @@ -69,8 +69,8 @@ public void testLessTransitive() KeYEnvironment env = KeYEnvironment.load(keyFile); Proof proof = env.getLoadedProof(); - ProofScriptEngine engine = new ProofScriptEngine(script); - engine.execute(env.getUi(), proof); + ProofScriptEngine engine = new ProofScriptEngine(proof); + engine.execute(env.getUi(), script); String firstOpenGoal = proof.openGoals().head().sequent().toString(); String expectedSequent = "[]==>[imp(and(gt(x,f),lt(x,z)),lt(f,z))]"; diff --git a/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/ValueInjectorTest.java b/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/ValueInjectorTest.java index cbb384d3b48..c2949ca1b59 100644 --- a/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/ValueInjectorTest.java +++ b/key.core/src/test/java/de/uka/ilkd/key/scripts/meta/ValueInjectorTest.java @@ -58,10 +58,43 @@ public void testRequired() { () -> ValueInjector.injection(new PPCommand(), pp, ast)); } + // copied from old jmlScript branch ... possibly needs adaptation + @Test + public void testUnknownArguments() { + PP pp = new PP(); + Map args = new HashMap<>(); + ScriptCommandAst ast = new ScriptCommandAst("pp", args, new LinkedList<>(), + null); + args.put("i", "42"); + args.put("b", "true"); + args.put("q", "requiredValue"); + args.put("unknownParameter", "unknownValue"); + assertThrows(UnknownArgumentException.class, + () -> ValueInjector.injection(new PPCommand(), pp, ast)); + } + + // copied from old jmlScript branch ... possibly needs adaptation + @Test + public void testVarargsOld() throws Exception { + PP pp = new PP(); + Map args = new HashMap<>(); + ScriptCommandAst ast = new ScriptCommandAst("pp", args, new LinkedList<>(), + null); + args.put("i", "42"); + args.put("b", "true"); + args.put("var_21", "21"); + args.put("q", "requiredValue"); + args.put("var_other", "otherString"); + ValueInjector.injection(new PPCommand(), pp, ast); + assertEquals("21", pp.varargs.get("var_21")); + assertEquals("otherString", pp.varargs.get("var_other")); + assertEquals(2, pp.varargs.size()); + } + @Test public void testInferScriptArguments() throws NoSuchFieldException { List meta = ArgumentsLifter.inferScriptArguments(PP.class); - assertEquals(4, meta.size()); + assertEquals(5, meta.size()); { ProofScriptArgument b = meta.getFirst(); @@ -80,18 +113,35 @@ public void testInferScriptArguments() throws NoSuchFieldException { } { - ProofScriptArgument i = meta.get(2); - assertEquals("s", i.getName()); - assertEquals(PP.class.getDeclaredField("s"), i.getField()); - assertEquals(String.class, i.getType()); - assertFalse(i.isRequired()); + ProofScriptArgument s = meta.get(2); + assertEquals("s", s.getName()); + assertEquals(PP.class.getDeclaredField("s"), s.getField()); + assertEquals(String.class, s.getType()); + assertFalse(s.isRequired()); + } + + { + ProofScriptArgument q = meta.get(3); + assertEquals("q", q.getName()); + assertEquals(PP.class.getDeclaredField("required"), q.getField()); + assertEquals(String.class, q.getType()); + assertTrue(q.isRequired()); + } + + { + ProofScriptArgument vars = meta.get(4); + assertEquals("varargs", vars.getName()); + assertEquals(PP.class.getDeclaredField("varargs"), vars.getField()); + assertEquals(Map.class, vars.getType()); + assertEquals("var_", vars.getOptionalVarArgs().prefix()); + assertSame(String.class, vars.getOptionalVarArgs().as()); + assertTrue(vars.isOptionalVarArgs()); } } @Test - public void testFlag() throws ConversionException, ArgumentRequiredException, - InjectionReflectionException, NoSpecifiedConverterException { + public void testFlag() throws Exception { class Options { @Flag boolean a; @@ -110,8 +160,7 @@ class Options { @Test - public void testVarargs() throws ConversionException, ArgumentRequiredException, - InjectionReflectionException, NoSpecifiedConverterException { + public void testVarargs() throws InjectionException { class Varargs { @OptionalVarargs(prefix = "a", as = Boolean.class) Map a; @@ -149,6 +198,9 @@ public static class PP { @Option("q") @MonotonicNonNull String required; + + @OptionalVarargs(prefix = "var_") + Map varargs; } @NullMarked diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/assumes.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/assumes.yml new file mode 100644 index 00000000000..6374ed61d06 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/assumes.yml @@ -0,0 +1,7 @@ +key: | + \programVariables { int a,b,c,d,e,f; } + \problem { a=b, a=d ==> a=c } +script: | + rule "applyEq" on:(?focus(a) = c) assumes:( ? = b ==> ) ; +goals: + - a = b, a = d ==> b = c diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/autoOnly.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/autoOnly.yml new file mode 100644 index 00000000000..24eef5e7d10 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/autoOnly.yml @@ -0,0 +1,7 @@ +key: | + \predicates { a; b; c;} + \problem { a & b & 1=0 -> a | c } +script: | + auto only:alpha; +goals: + - a, b, 1 = 0 ==> a, c \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/bugWithFunctions.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/bugWithFunctions.yml new file mode 100644 index 00000000000..5f4b6929552 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/bugWithFunctions.yml @@ -0,0 +1,12 @@ +# Was a bug: b+0 would also match here ... +key: | + \programVariables { int a; int b; } + + \problem { a + 0 = a & b + 0 = b } + +script: | + rule add_zero_right on:(a+0); + +goals: + - "==> a = a & b + 0 = b" + diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/find1.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/find1.yml new file mode 100644 index 00000000000..26ea900fee9 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/find1.yml @@ -0,0 +1,7 @@ +key: | + \programVariables { int a; } + \problem { ==> ((1 + 1) + 5) + 0 = 7 & ((0 + 2) + 5) + 0 = 7 } +script: | + rule add_zero_right on:(?find(1+1)); +goals: + - ==> 1 + 1 + 5 = 7 & 0 + 2 + 5 + 0 = 7 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/find2.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/find2.yml new file mode 100644 index 00000000000..498842ca2e8 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/find2.yml @@ -0,0 +1,6 @@ +key: | + \problem { ==> 1 + 0 = 1 & 2 + 0 = 2 & 3 + 0 = 3 } +script: | + rule add_zero_right on:(?find(2)); +goals: + - ==> 1 + 0 = 1 & 2 = 2 & 3 + 0 = 3 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/findOfFocus.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/findOfFocus.yml new file mode 100644 index 00000000000..3c9e122033b --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/findOfFocus.yml @@ -0,0 +1,7 @@ +key: | + \programVariables { int a; } + \problem { ==> 3 = (3+0)+0 & 3+0 = (3+0)+0 } +script: | + rule add_zero_right on:(? & ?find(?focus(3+0)+0)); +goals: + - ==> 3 = 3 + 0 + 0 & 3 + 0 = 3 + 0 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus1.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus1.yml new file mode 100644 index 00000000000..3387b6cd31e --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus1.yml @@ -0,0 +1,7 @@ +key: | + \programVariables { int a; } + \problem { ==> a + 0 = a + 0 & 1 = 1 } +script: | + rule add_zero_right on:(a+0 = ?focus(a+0)); +goals: + - ==> a + 0 = a & 1 = 1 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus2.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus2.yml new file mode 100644 index 00000000000..f776bc20567 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus2.yml @@ -0,0 +1,8 @@ +# Used to identify and hunt a bug. +# Raised a -1 illegal index exception +key: | + \problem { ==> 1 + 0 + 0 + 0 + 0 >= 0 } +script: | + rule add_zero_right on:(?focus(?) + ? + ? + ?); +goals: + - ==> 1 + 0 + 0 + 0 >= 0 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focusOfFind.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focusOfFind.yml new file mode 100644 index 00000000000..267325d1c12 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focusOfFind.yml @@ -0,0 +1,7 @@ +key: | + \programVariables { int a; } + \problem { ==> (0+3)+0 = (0+2+1)+0 & (0+1+1+1)+0 = (0+3)+0 } +script: | + rule add_zero_right on:(? = ?focus(?find(3))); +goals: + - ==> 0 + 3 + 0 = 0 + 2 + 1 + 0 & 0 + 1 + 1 + 1 + 0 = 0 + 3 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus_rule.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus_rule.yml new file mode 100644 index 00000000000..f7ad12aab7d --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/focus_rule.yml @@ -0,0 +1,6 @@ +key: | + \problem { 1 > 0, 2 > 1 ==> 3 > 2, 4 > 2, 5 > 1 } +script: | + focus (? > 1 ==> ? > 2); +goals: + - 2 > 1 ==> 3 > 2, 4 > 2 diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/holes1.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/holes1.yml new file mode 100644 index 00000000000..c73fcf31c8d --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/holes1.yml @@ -0,0 +1,6 @@ +key: | + \problem { ==> \forall int x; x=x, \forall int x; (x > 0 | x < 2) } +script: | + witness (\forall int x; (? | x < ?)) as="y"; +goals: + - ==> \forall int x; x = x, y > 0 | y < 2 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/holes2.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/holes2.yml new file mode 100644 index 00000000000..00bbff5a86b --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/holes2.yml @@ -0,0 +1,7 @@ +# Holes in strings ... requires a bit of care +key: | + \problem { ==> \forall int x; x=x, \forall int x; (x > 0 | x < 2) } +script: | + witness "(\forall int x; (? | x < ?))" as="y"; +goals: + - ==> \forall int x; x = x, y > 0 | y < 2 \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/instantiate1.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/instantiate1.yml new file mode 100644 index 00000000000..66bebb3403e --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/instantiate1.yml @@ -0,0 +1,6 @@ +key: | + \problem { \exists int x; x>0 } +script: | + instantiate var="x" with=3 hide:true; +goals: + - ==> 3 > 0 diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/let.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/let.yml new file mode 100644 index 00000000000..990640ac6a1 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/let.yml @@ -0,0 +1,7 @@ +key: | + \problem { 1+0 = 2+0 } +script: | + let @p1: "1+0"; + rule add_zero_right on: @p1; +goals: + - ==> 1 = 2 + 0 diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/termsAsStrings.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/termsAsStrings.yml new file mode 100644 index 00000000000..5d36b39c5db --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/termsAsStrings.yml @@ -0,0 +1,8 @@ +# Give arguments as strings ... requires a bit of care +key: | + \problem { ==> true } +script: | + cut "1 > 0"; +goals: + - ==> 1 > 0, true + - 1 > 0 ==> true \ No newline at end of file diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/witness1.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/witness1.yml new file mode 100644 index 00000000000..da74c45bd51 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/witness1.yml @@ -0,0 +1,6 @@ +key: | + \problem { ==> \forall int x; x=x, \forall int x; (x > 0 | x < 2) } +script: | + witness (\forall int x; (x>0 | x<2)) as="y"; +goals: + - ==> \forall int x; x = x, y > 0 | y < 2 diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/witness2.yml b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/witness2.yml new file mode 100644 index 00000000000..69e2d245925 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/cases/witness2.yml @@ -0,0 +1,8 @@ +key: | + \functions { int x; int x1; } + \predicates { x2; } + \programVariables { int x3; } + \problem { ==> \forall int x; x=x, \forall int x; (x > 0 | x < x1) } +script: | + witness (\forall int x; (x > 0 | x < x1)) as="x1"; +exception: "Name already used as function or predicate: x1" diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AssertedModelMethod.java b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AssertedModelMethod.java new file mode 100644 index 00000000000..f10551b4dc7 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AssertedModelMethod.java @@ -0,0 +1,23 @@ +//! settings: +//! CLASS_AXIOM_OPTIONS_KEY: CLASS_AXIOM_OFF + +// Was a bug: +// Instantiation Test::pred(heap,self,int::select(heap,self,Test::$f)) of cutFormula (formula) does not satisfy the variable conditions + +class Test { + + //@ model boolean pred(int x) { return x > 20; } + + int f; + + //@ requires pred(f); + //@ ensures f > 2; + void test() { + int x; + /*@ assert f > 2 \by { + assert pred(f) \by { auto classAxioms:true; } + auto; // should not be necessary ... eventually removed + } */ + } + +} diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AutoOnly.java b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AutoOnly.java new file mode 100644 index 00000000000..445fb66b9a5 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AutoOnly.java @@ -0,0 +1,19 @@ +//! settings: +//! CLASS_AXIOM_OPTIONS_KEY: CLASS_AXIOM_OFF + +class Test { + + //@ model int f(int arg) { return arg + arg; } + + boolean b,c,d; + + //@ ensures true; + void test() { + + /*@ assert b && c ==> c || d \by { + auto only:"alpha"; + rule "close" occ:"1"; + } */ + } + +} diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AutoOnly2.java b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AutoOnly2.java new file mode 100644 index 00000000000..8be9e20db47 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/AutoOnly2.java @@ -0,0 +1,17 @@ +//! shouldClose: false + +class Test { + + //@ model int f(int arg) { return arg + arg; } + + boolean b,c,d; + + //@ ensures true; + void test() { + + /*@ assert b && c ==> c || d \by { + auto only:"beta"; + } */ + } + +} diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/NestedAssert.java b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/NestedAssert.java new file mode 100644 index 00000000000..76a6686eddd --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/NestedAssert.java @@ -0,0 +1,11 @@ +class Test { + //@ ensures true; + void test() { + int x; + /*@ assert x > 2 \by { + assert x == 7 \by { cheat; } + auto; // should not be necessary ... eventually removed + } */ + } + +} diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/Obtain1.java b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/Obtain1.java new file mode 100644 index 00000000000..92bd352ddf2 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/Obtain1.java @@ -0,0 +1,15 @@ +//! deleteTmpDir : false + +class Test { + //@ ensures true; + void test() { + int x = 42; + /*@ assert x == 42 \by { + obtain int y = 41; + assert y+1 == 42 \by auto; + auto; + // Still too verbose on auto + } */ + } + +} diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/ObtainFromGoal.java b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/ObtainFromGoal.java new file mode 100644 index 00000000000..4f7350b56f1 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/ObtainFromGoal.java @@ -0,0 +1,18 @@ +//! deleteTmpDir : false + +class Test { + + //@ model int f(int arg); + + //@ ensures true; + void test() { + int x = 42; + /*@ assert (\forall int x; f(x) > 40) \by { + obtain int y \from_goal; + assert f(y) == 42 \by cheat; + auto; + // Still too verbose on auto + } */ + } + +} diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/ObtainWithUpdates.java b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/ObtainWithUpdates.java new file mode 100644 index 00000000000..c85eebaee8b --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/ObtainWithUpdates.java @@ -0,0 +1,23 @@ +//! settings: +//! CLASS_AXIOM_OPTIONS_KEY: CLASS_AXIOM_OFF + +class Test { + + //@ model int f(int arg) { return arg + arg; } + + int field; + + //@ ensures true; + void test() { + + field = 21; + int local = 42; + + /*@ assert f(field) == local \by { + expand on: f(field); + auto; + // Still too verbose on auto + } */ + } + +} diff --git a/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/project.key b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/project.key new file mode 100644 index 00000000000..dad8dd3de51 --- /dev/null +++ b/key.core/src/test/resources/de/uka/ilkd/key/scripts/jml/project.key @@ -0,0 +1,14 @@ +\profile "Java Profile"; + + +\javaSource "."; + +\proofObligation { + "class" : "de.uka.ilkd.key.proof.init.FunctionalOperationContractPO", + "contract" : "Test[Test::test()].JML operation contract.0", + "name" : "Test[Test::test()].JML operation contract.0" + } + +\proofScript { + macro "script-auto"; +} diff --git a/key.ncore.calculus/src/main/java/org/key_project/prover/sequent/Semisequent.java b/key.ncore.calculus/src/main/java/org/key_project/prover/sequent/Semisequent.java index 88b152e1c46..64a099b7bd7 100644 --- a/key.ncore.calculus/src/main/java/org/key_project/prover/sequent/Semisequent.java +++ b/key.ncore.calculus/src/main/java/org/key_project/prover/sequent/Semisequent.java @@ -190,6 +190,12 @@ public SequentFormula getFirst() { return seqList.head(); } + /// @return the last [SequentFormula] of this [Semisequent] + public SequentFormula getLast() { + return seqList.last(); + // or return seqList.take(seqList.size() - 1).head(); + } + /// Returns iterator about the formulas contained in this [Semisequent] /// /// @return iterator about the formulas contained in this [Semisequent] diff --git a/key.ncore/src/main/java/org/key_project/logic/PosInTerm.java b/key.ncore/src/main/java/org/key_project/logic/PosInTerm.java index b705e697bf2..326e1697c63 100644 --- a/key.ncore/src/main/java/org/key_project/logic/PosInTerm.java +++ b/key.ncore/src/main/java/org/key_project/logic/PosInTerm.java @@ -82,6 +82,13 @@ public static PosInTerm parseReverseString(String s) { : new PosInTerm(positions, (char) positions.length, true); } + public PosInTerm prepend(char index) { + var newPositions = new char[size + 1]; + System.arraycopy(positions, 0, newPositions, 1, size); + newPositions[0] = index; + return new PosInTerm(newPositions, (char) (size + 1), false); + } + /// returns the instance representing the top level position /// /// @return the top level position @@ -130,6 +137,25 @@ public PosInTerm firstN(int n) { return new PosInTerm(positions, (char) n, true); } + + /// returns the position of the suffix of length n + /// @param n the length of the suffix + /// @return the suffix of this position of length n + /// @throws IndexOutOfBoundsException if n is greater than the depth of this + /// position + public PosInTerm lastN(int n) { + if (n > size) { + throw new IndexOutOfBoundsException("Position is shorter than " + n); + } else if (n == 0) { + return getTopLevel(); + } else if (n == size) { + return this; + } + final char[] newPositions = new char[n]; + System.arraycopy(positions, size - n, newPositions, 0, n); + return new PosInTerm(newPositions, (char) n, false); + } + /// returns the position for the i-th subterm of the subterm described by this /// position /// @@ -237,7 +263,7 @@ public boolean equals(@Nullable Object o) { /// /// @param it the iterator /// @return the String with the list of integers - public String integerList(IntIterator it) { + public static String integerList(IntIterator it) { final StringBuilder list = new StringBuilder("["); while (it.hasNext()) { list.append(it.next()); diff --git a/key.ui/examples/heap/BoyerMoore/BM.bm.key b/key.ui/examples/heap/BoyerMoore/BM.bm.key new file mode 100644 index 00000000000..d9bd59e4d27 --- /dev/null +++ b/key.ui/examples/heap/BoyerMoore/BM.bm.key @@ -0,0 +1,86 @@ +\profile "Java Profile"; + +\settings // Proof-Settings-Config-File +{ + "Choice" : { + "JavaCard" : "JavaCard:on", + "Strings" : "Strings:on", + "assertions" : "assertions:on", + "bigint" : "bigint:on", + "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:off", + "programRules" : "programRules:Java", + "reach" : "reach:on", + "runtimeExceptions" : "runtimeExceptions:ban", + "sequences" : "sequences:on", + "wdChecks" : "wdChecks:off", + "wdOperator" : "wdOperator:L" + }, + "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" : "JavaCardDLStrategy", + "MaximumNumberOfAutomaticApplications" : 10000, + "Timeout" : -1, + "options" : { + "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", + "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", + "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_DELAYED", + "DEP_OPTIONS_KEY" : "DEP_ON", + "INF_FLOW_CHECK_PROPERTY" : "INF_FLOW_CHECK_FALSE", + "LOOP_OPTIONS_KEY" : "LOOP_INVARIANT", + "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", + "MPS_OPTIONS_KEY" : "MPS_MERGE", + "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_DEF_OPS", + "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" + } + } + } + + +\javaSource "src"; + +\proofObligation { + "class" : "de.uka.ilkd.key.proof.init.FunctionalOperationContractPO", + "contract" : "BoyerMoore[BoyerMoore::bm([I)].JML normal_behavior operation contract.0", + "name" : "BoyerMoore[BoyerMoore::bm([I)].JML normal_behavior operation contract.0" + } + +\proofScript { macro "script-auto"; } diff --git a/key.ui/examples/heap/BoyerMoore/BM.count.accessible.key b/key.ui/examples/heap/BoyerMoore/BM.count.accessible.key new file mode 100644 index 00000000000..0c3fdb72aa0 --- /dev/null +++ b/key.ui/examples/heap/BoyerMoore/BM.count.accessible.key @@ -0,0 +1,85 @@ +\profile "Java Profile"; + +\settings // Proof-Settings-Config-File +{ + "Choice" : { + "JavaCard" : "JavaCard:on", + "Strings" : "Strings:on", + "assertions" : "assertions:on", + "bigint" : "bigint:on", + "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:off", + "programRules" : "programRules:Java", + "reach" : "reach:on", + "runtimeExceptions" : "runtimeExceptions:ban", + "sequences" : "sequences:on", + "wdChecks" : "wdChecks:off", + "wdOperator" : "wdOperator:L" + }, + "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" : "JavaCardDLStrategy", + "MaximumNumberOfAutomaticApplications" : 10000, + "Timeout" : -1, + "options" : { + "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", + "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", + "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_DELAYED", + "DEP_OPTIONS_KEY" : "DEP_ON", + "INF_FLOW_CHECK_PROPERTY" : "INF_FLOW_CHECK_FALSE", + "LOOP_OPTIONS_KEY" : "LOOP_INVARIANT", + "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", + "MPS_OPTIONS_KEY" : "MPS_MERGE", + "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_DEF_OPS", + "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" + } + } + } + + +\javaSource "src"; + +\proofObligation { + "class" : "de.uka.ilkd.key.proof.init.DependencyContractPO", + "contract" : "BoyerMoore[BoyerMoore::count([I,\bigint,\bigint)].JML accessible clause.0", + "name" : "BoyerMoore[BoyerMoore::count([I,\bigint,\bigint)].JML accessible clause.0" + } + diff --git a/key.ui/examples/heap/BoyerMoore/BM.count.key b/key.ui/examples/heap/BoyerMoore/BM.count.key new file mode 100644 index 00000000000..52ef9b4383b --- /dev/null +++ b/key.ui/examples/heap/BoyerMoore/BM.count.key @@ -0,0 +1,84 @@ +\profile "Java Profile"; + +\settings // Proof-Settings-Config-File +{ + "Choice" : { + "JavaCard" : "JavaCard:on", + "Strings" : "Strings:on", + "assertions" : "assertions:on", + "bigint" : "bigint:on", + "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:off", + "programRules" : "programRules:Java", + "reach" : "reach:on", + "runtimeExceptions" : "runtimeExceptions:ban", + "sequences" : "sequences:on", + "wdChecks" : "wdChecks:off", + "wdOperator" : "wdOperator:L" + }, + "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" : "JavaCardDLStrategy", + "MaximumNumberOfAutomaticApplications" : 10000, + "Timeout" : -1, + "options" : { + "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", + "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", + "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_DELAYED", + "DEP_OPTIONS_KEY" : "DEP_ON", + "INF_FLOW_CHECK_PROPERTY" : "INF_FLOW_CHECK_FALSE", + "LOOP_OPTIONS_KEY" : "LOOP_INVARIANT", + "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", + "MPS_OPTIONS_KEY" : "MPS_MERGE", + "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_DEF_OPS", + "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" + } + } + } + + +\javaSource "src"; + +\proofObligation { + "class" : "de.uka.ilkd.key.proof.init.FunctionalOperationContractPO", + "contract" : "BoyerMoore[BoyerMoore::count([I,\bigint,\bigint)].JML model_behavior operation contract.0", + "name" : "BoyerMoore[BoyerMoore::count([I,\bigint,\bigint)].JML model_behavior operation contract.0" + } diff --git a/key.ui/examples/heap/BoyerMoore/BM.monoLemma.key b/key.ui/examples/heap/BoyerMoore/BM.monoLemma.key new file mode 100644 index 00000000000..e4d85019478 --- /dev/null +++ b/key.ui/examples/heap/BoyerMoore/BM.monoLemma.key @@ -0,0 +1,83 @@ +\profile "Java Profile"; + +\settings // Proof-Settings-Config-File +{ + "Choice" : { + "JavaCard" : "JavaCard:on", + "Strings" : "Strings:on", + "assertions" : "assertions:on", + "bigint" : "bigint:on", + "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:off", + "programRules" : "programRules:Java", + "reach" : "reach:on", + "runtimeExceptions" : "runtimeExceptions:ban", + "sequences" : "sequences:on", + "wdChecks" : "wdChecks:off", + "wdOperator" : "wdOperator:L" + }, + "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" : "JavaCardDLStrategy", + "MaximumNumberOfAutomaticApplications" : 10000, + "Timeout" : -1, + "options" : { + "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", + "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", + "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_DELAYED", + "DEP_OPTIONS_KEY" : "DEP_ON", + "INF_FLOW_CHECK_PROPERTY" : "INF_FLOW_CHECK_FALSE", + "LOOP_OPTIONS_KEY" : "LOOP_INVARIANT", + "METHOD_OPTIONS_KEY" : "METHOD_CONTRACT", + "MPS_OPTIONS_KEY" : "MPS_MERGE", + "NON_LIN_ARITH_OPTIONS_KEY" : "NON_LIN_ARITH_DEF_OPS", + "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" + } + } + } + +\javaSource "src"; + +\proofObligation { + "class" : "de.uka.ilkd.key.proof.init.FunctionalOperationContractPO", + "contract" : "BoyerMoore[BoyerMoore::monoLemma([I,int,int)].JML normal_behavior operation contract.0", + "name" : "BoyerMoore[BoyerMoore::monoLemma([I,int,int)].JML normal_behavior operation contract.0" + } diff --git a/key.ui/examples/heap/BoyerMoore/BoyerMoore.key b/key.ui/examples/heap/BoyerMoore/BoyerMoore.key index 5aa881eb14a..b54ea73969f 100644 --- a/key.ui/examples/heap/BoyerMoore/BoyerMoore.key +++ b/key.ui/examples/heap/BoyerMoore/BoyerMoore.key @@ -51,7 +51,7 @@ "options" : { "AUTO_INDUCTION_OPTIONS_KEY" : "AUTO_INDUCTION_OFF", "BLOCK_OPTIONS_KEY" : "BLOCK_CONTRACT_INTERNAL", - "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_OFF", + "CLASS_AXIOM_OPTIONS_KEY" : "CLASS_AXIOM_DELAYED", "DEP_OPTIONS_KEY" : "DEP_ON", "INF_FLOW_CHECK_PROPERTY" : "INF_FLOW_CHECK_FALSE", "LOOP_OPTIONS_KEY" : "LOOP_INVARIANT", diff --git a/key.ui/examples/heap/BoyerMoore/README.txt b/key.ui/examples/heap/BoyerMoore/README.txt index f56ae2a608a..9c748ddbecd 100644 --- a/key.ui/examples/heap/BoyerMoore/README.txt +++ b/key.ui/examples/heap/BoyerMoore/README.txt @@ -15,8 +15,9 @@ entries in the array hold m. Suggested by J.C. Filliâtre as an example during VerifyThis 24. -Currently the proofs do not go through automatically, the proof -files are checked in with the example. +Originally, the proofs did not go through automatically, with JML +proof scripts they now succeed. + @see https://en.wikipedia.org/wiki/Boyer-Moore_majority_vote_algorithm @author Mattias Ulbrich diff --git a/key.ui/examples/heap/BoyerMoore/src/BoyerMoore.java b/key.ui/examples/heap/BoyerMoore/src/BoyerMoore.java index dc8a350ff1a..abe99b428b1 100644 --- a/key.ui/examples/heap/BoyerMoore/src/BoyerMoore.java +++ b/key.ui/examples/heap/BoyerMoore/src/BoyerMoore.java @@ -59,11 +59,23 @@ public IntOpt bm(int[] a) { if(mc == 0) { mc = 1; mx = a[k]; + /*@ assert count(a, k+1, a[k]) <= count(a, k, a[k]) + 1 \by { + @ oss; + @ expand on: "self.count(a, k_0 + 1, a[k_0])"; + @ auto classAxioms:false; + @ }*/ } else if(mx == a[k]) { mc++; } else { mc--; } + /*@ assert (\forall int x; x != mx; 2 * count(a, k+1, x) <= k+1 - mc) \by { + @ oss; + @ obtain int x \from_goal; + @ expand on: "self.count(a, k_0 + 1, x)"; + @ instantiate var:"x" with: x; + @ auto classAxioms:false; + @ }*/ } if(mc == 0) return IntOpt.NONE; diff --git a/key.ui/examples/heap/quicksort/Quicksort.java b/key.ui/examples/heap/quicksort/Quicksort.java index 23d7bb6f0ff..431eaf04726 100644 --- a/key.ui/examples/heap/quicksort/Quicksort.java +++ b/key.ui/examples/heap/quicksort/Quicksort.java @@ -28,7 +28,10 @@ * The example has been added to show the power of proof * scripts. * - * @author Mattias Ulbrich, 2015 + * Translated to the use of JML proof scripts in 2025. Currently, + * this still increases the size of proof. + * + * @author Mattias Ulbrich, 2015, 2025 */ class Quicksort { @@ -58,9 +61,18 @@ public void sort(int[] array) { @*/ private void sort(int[] array, int from, int to) { if(from < to) { + //@ ghost \seq seq0 = \dl_array2seq(array); int splitPoint = split(array, from, to); + //@ ghost \seq seq1 = \dl_array2seq(array); sort(array, from, splitPoint-1); + //@ ghost \seq seq2 = \dl_array2seq(array); sort(array, splitPoint+1, to); + //@ ghost \seq seq3 = \dl_array2seq(array); + /*@ assert \dl_seqPerm(seq3, seq0) \by { + @ assert \dl_seqPerm(seq1, seq0) \by auto; + @ assert \dl_seqPerm(seq2, seq0) \by auto; + @ auto; + @ } */ } } @@ -97,6 +109,18 @@ private int split(int[] array, int from, int to) { int t = array[i]; array[i] = array[j]; array[j] = t; + /*@ assert \dl_seqPerm(\dl_array2seq(array), \old(\dl_array2seq(array))) \by { + @ oss; + @ rule "seqPermFromSwap"; + @ rule "andRight" \by { + @ case "Case 1": // the first of the two conjuncts is easy + @ auto; + @ case "Case 2": // the 2nd requires instantiations: + @ instantiate hide:true var:"iv" with:i; + @ instantiate hide:true var:"jv" with:j; + @ auto; + @ } + @ }; */ i++; } } @@ -104,6 +128,19 @@ private int split(int[] array, int from, int to) { array[to] = array[i]; array[i] = pivot; + /*@ assert \dl_seqPerm(\dl_array2seq(array), \old(\dl_array2seq(array))) \by { + @ oss; + @ rule "seqPermFromSwap"; + @ rule "andRight" \by { + @ case "Case 1": // the first of the two conjuncts is easy + @ auto; + @ case "Case 2": // the 2nd requires instantiations: + @ instantiate hide:true var:"iv" with:i; + @ instantiate hide:true var:"jv" with:to; + @ auto; + @ } + @ }; */ + return i; } diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/IssueDialog.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/IssueDialog.java index a882eb57afd..a90b89338b4 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/IssueDialog.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/IssueDialog.java @@ -736,6 +736,9 @@ private void addHighlights(DefaultHighlighter dh, PositionedString ps) { } String source = txtSource.getText(); int offset = getOffsetFromLineColumn(source, pos); + while (offset < source.length() && Character.isWhitespace(source.charAt(offset))) { + offset++; + } int end = offset; while (end < source.length() && !Character.isWhitespace(source.charAt(end))) { end++; diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofScriptWorker.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofScriptWorker.java index ccb6b52727a..e218efc1623 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofScriptWorker.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/ProofScriptWorker.java @@ -5,6 +5,7 @@ import java.awt.*; import java.awt.Dialog.ModalityType; +import java.io.IOException; import java.net.URI; import java.util.List; import java.util.concurrent.CancellationException; @@ -22,6 +23,7 @@ import de.uka.ilkd.key.scripts.ProofScriptEngine; import de.uka.ilkd.key.scripts.ScriptException; +import org.checkerframework.checker.nullness.qual.MonotonicNonNull; import org.jspecify.annotations.NullMarked; import org.jspecify.annotations.Nullable; import org.slf4j.Logger; @@ -46,7 +48,8 @@ public class ProofScriptWorker extends SwingWorker<@Nullable Object, ProofScript /** * The proof script engine. */ - private final ProofScriptEngine engine; + private @MonotonicNonNull ProofScriptEngine engine; + private final JDialog monitor = new JDialog(MainWindow.getInstance(), "Running Script ...", ModalityType.MODELESS); private final JTextArea logArea = new JTextArea(); @@ -75,14 +78,15 @@ public ProofScriptWorker(KeYMediator mediator, KeyAst.ProofScript script, this.mediator = mediator; this.script = script; this.initiallySelectedGoal = initiallySelectedGoal; - engine = new ProofScriptEngine(script, initiallySelectedGoal); } @Override protected @Nullable Object doInBackground() throws Exception { try { + engine = new ProofScriptEngine(mediator.getSelectedProof()); + engine.setInitiallySelectedGoal(initiallySelectedGoal); engine.setCommandMonitor(observer); - engine.execute(mediator.getUI(), mediator.getSelectedProof()); + engine.execute(mediator.getUI(), script); } catch (InterruptedException ex) { LOGGER.debug("Proof macro has been interrupted:", ex); } @@ -171,7 +175,8 @@ public void done() { private void selectGoalOrNode() { final KeYSelectionModel selectionModel = mediator.getSelectionModel(); - if (!mediator.getSelectedProof().closed()) { + final Proof proof = mediator.getSelectedProof(); + if (proof != null && !proof.closed() && engine != null) { try { selectionModel .setSelectedGoal(engine.getStateMap().getFirstOpenAutomaticGoal()); diff --git a/key.ui/src/main/java/de/uka/ilkd/key/gui/StrategySelectionView.java b/key.ui/src/main/java/de/uka/ilkd/key/gui/StrategySelectionView.java index 37141d6090e..442e098cc4d 100644 --- a/key.ui/src/main/java/de/uka/ilkd/key/gui/StrategySelectionView.java +++ b/key.ui/src/main/java/de/uka/ilkd/key/gui/StrategySelectionView.java @@ -63,7 +63,7 @@ public final class StrategySelectionView extends JPanel implements TabPanel { /** * The always used {@link StrategyFactory}. */ - private static final StrategyFactory FACTORY = JavaProfile.DEFAULT; + private static final StrategyFactory FACTORY = JavaProfile.getDefault(); /** * The {@link StrategySettingsDefinition} of {@link #FACTORY} which defines the UI controls to 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 4b33d65c79b..64554e31174 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 @@ -166,10 +166,10 @@ public void taskFinished(TaskFinishedInfo info) { var script = problemLoader.getProofScript(); if (script != null) { ProofScriptEngine pse = - new ProofScriptEngine(script); + new ProofScriptEngine(proof); this.taskStarted( new DefaultTaskStartedInfo(TaskKind.Macro, "Script started", 0)); - pse.execute(this, proof); + pse.execute(this, script); // The start and end messages are fake to persuade the system ... // All this here should refactored anyway ... this.taskFinished(new ProofMacroFinishedInfo(new SkipMacro(), proof)); diff --git a/key.util/src/main/java/org/key_project/util/collection/ImmutableList.java b/key.util/src/main/java/org/key_project/util/collection/ImmutableList.java index d83c99b9e90..1f10fb345e7 100644 --- a/key.util/src/main/java/org/key_project/util/collection/ImmutableList.java +++ b/key.util/src/main/java/org/key_project/util/collection/ImmutableList.java @@ -356,20 +356,25 @@ default T last() { remainder = remainder.tail(); } T result = remainder.head(); + // MU: I wonder why this is required. T may be a nullable type ... assert result != null : "@AssumeAssertion(nullness): this should never be null"; return result; } /** - * Get the n-th element of this list. + * Returns the element at the specified position in this list. * - * @param idx the 0-based index of the element - * @return the element at index idx. - * @throws IndexOutOfBoundsException if idx is less than 0 or at - * least {@link #size()}. - */ - default T get(int idx) { - return take(idx).head(); + * @param index index of the element to return + * @return the element at the specified position in this list + * @throws IndexOutOfBoundsException if the index is out of range + * ({@code index < 0 || index >= size()}) + */ + default T get(int index) { + if (index < 0 || index >= size()) { + throw new IndexOutOfBoundsException(); + } else { + return take(index).head(); + } } } diff --git a/key.util/src/main/java/org/key_project/util/collection/ImmutableSet.java b/key.util/src/main/java/org/key_project/util/collection/ImmutableSet.java index 7eab97f1b7a..2d633534012 100644 --- a/key.util/src/main/java/org/key_project/util/collection/ImmutableSet.java +++ b/key.util/src/main/java/org/key_project/util/collection/ImmutableSet.java @@ -9,6 +9,7 @@ import java.util.stream.Collector.Characteristics; import java.util.stream.Stream; +import org.jspecify.annotations.NonNull; import org.jspecify.annotations.Nullable; /** @@ -43,6 +44,14 @@ public interface ImmutableSet return DefaultImmutableSet.nil(); } + static ImmutableSet from(Iterable ts) { + ImmutableSet result = DefaultImmutableSet.nil(); + for (T t : ts) { + result = result.add(t); + } + return result; + } + /** * @return a {@code Set} containing the same elements as this {@code ImmutableSet} */ diff --git a/key.util/src/main/java/org/key_project/util/java/IOUtil.java b/key.util/src/main/java/org/key_project/util/java/IOUtil.java index 9a4666f7189..6ddcb062fa5 100644 --- a/key.util/src/main/java/org/key_project/util/java/IOUtil.java +++ b/key.util/src/main/java/org/key_project/util/java/IOUtil.java @@ -700,6 +700,40 @@ public static boolean copy(InputStream source, OutputStream target) throws IOExc } } + public static URL makeMemoryURL(String data) { + try { + return new URL("memory", "", 0, String.format("/%x", System.identityHashCode(data)), + new MemoryDataHandler(data)); + } catch (MalformedURLException e) { + throw new RuntimeException(e); + } + } + + private static final class MemoryDataHandler extends URLStreamHandler { + private final String data; + + public MemoryDataHandler(String data) { + this.data = data; + } + + @Override + protected URLConnection openConnection(URL u) throws IOException { + // perhaps check the hash code too? + if (!u.getProtocol().equals("memory")) { + throw new IOException("Unsupported protocol"); + } + return new URLConnection(u) { + @Override + public void connect() {} + + @Override + public InputStream getInputStream() throws IOException { + return new ByteArrayInputStream(data.getBytes()); + } + }; + } + } + /** * Returns the current directory. * diff --git a/key.util/src/main/java/org/key_project/util/java/IntegerUtil.java b/key.util/src/main/java/org/key_project/util/java/IntegerUtil.java index 240bc59f135..bd6e6306659 100644 --- a/key.util/src/main/java/org/key_project/util/java/IntegerUtil.java +++ b/key.util/src/main/java/org/key_project/util/java/IntegerUtil.java @@ -4,6 +4,9 @@ package org.key_project.util.java; +import java.util.Collection; +import java.util.List; + public final class IntegerUtil { /** * Forbid instances. @@ -28,4 +31,27 @@ public static int factorial(int n) { return factorial; } } + + /** + * Creates a list of integers from {@code 0} (inclusive) to the size of the given collection + * (exclusive). + */ + public static List indexRangeOf(Collection coll) { + return rangeUntil(coll.size()); + } + + /** + * Creates a list of integers from {@code 0} (inclusive) to {@code size} (exclusive). + */ + private static List rangeUntil(int size) { + return range(0, size); + } + + /** + * Creates a list of integers from {@code from} (inclusive) to {@code untilExclusive} + * (exclusive). + */ + private static List range(int from, int untilExclusive) { + return java.util.stream.IntStream.range(from, untilExclusive).boxed().toList(); + } } diff --git a/key.util/src/main/java/org/key_project/util/java/StringUtil.java b/key.util/src/main/java/org/key_project/util/java/StringUtil.java index 3d1f6ed4212..31a1b57785f 100644 --- a/key.util/src/main/java/org/key_project/util/java/StringUtil.java +++ b/key.util/src/main/java/org/key_project/util/java/StringUtil.java @@ -554,4 +554,20 @@ public static String removeEmptyLines(String string) { return string.replaceAll("(?m)^[ \t]*\r?\n|\n$", ""); } + /** + * If the given text starts and ends with quotes (single or double), they will be stripped. + * + * @param text The text to check. + * @return The text without leading and trailing quotes or the original text if no quotes were + * present. + */ + public static String stripQuotes(String text) { + if (text.length() >= 2 && text.startsWith("\"") && text.endsWith("\"")) { + return text.substring(1, text.length() - 1); + } + if (text.length() >= 2 && text.startsWith("'") && text.endsWith("'")) { + return text.substring(1, text.length() - 1); + } + return text; + } }