From 821cc7eaa8eb8f7449c41c2aef3bb07f954fd220 Mon Sep 17 00:00:00 2001 From: Ricardo Costa Date: Sat, 9 May 2026 18:31:42 +0100 Subject: [PATCH] Operator Assignment Support --- .../testSuite/CorrectOperatorAssignments.java | 47 +++++++++++++++ .../RefinementTypeChecker.java | 31 +++++++++- .../general_checkers/OperationsChecker.java | 58 +++++++++++++++++++ 3 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 liquidjava-example/src/main/java/testSuite/CorrectOperatorAssignments.java diff --git a/liquidjava-example/src/main/java/testSuite/CorrectOperatorAssignments.java b/liquidjava-example/src/main/java/testSuite/CorrectOperatorAssignments.java new file mode 100644 index 00000000..fb466ee8 --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/CorrectOperatorAssignments.java @@ -0,0 +1,47 @@ +package testSuite; + +import liquidjava.specification.Refinement; + +public class CorrectOperatorAssignments { + + @Refinement("_ > 0") + int plus(@Refinement("_ >= 0") int x) { + x += 1; // x is now > 0 + return x; + } + + @Refinement("_ > 0") + int plusInvocation(@Refinement("_ >= 0") int x) { + x += one(); // x is now > 0 + return x; + } + + @Refinement("_ == 1") + int one() { + return 1; + } + + @Refinement("_ < 0") + int minus(@Refinement("_ <= 0") int x) { + x -= 1; // x is now < 0 + return x; + } + + @Refinement("_ >= 0") + int multiply(int x) { + x *= x; // x is now >= 0 + return x; + } + + @Refinement("_ <= 1") + int divide(@Refinement("0 <= _ && _ <= 3") int x) { + x /= 2; // x is now <= 1 + return x; + } + + @Refinement("_ == 0 || _ == 1") + int remainder(@Refinement("_ >= 0") int x) { + x %= 2; // x is now == 0 || x is now == 1 + return x; + } +} diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java index 09095f35..9fd9bd0f 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java +++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java @@ -38,6 +38,7 @@ import spoon.reflect.code.CtLocalVariable; import spoon.reflect.code.CtNewArray; import spoon.reflect.code.CtNewClass; +import spoon.reflect.code.CtOperatorAssignment; import spoon.reflect.code.CtReturn; import spoon.reflect.code.CtStatement; import spoon.reflect.code.CtThisAccess; @@ -186,10 +187,23 @@ public void visitCtThisAccess(CtThisAccess thisAccess) { } } - @SuppressWarnings("unchecked") @Override public void visitCtAssignment(CtAssignment assignment) throws LJError { super.visitCtAssignment(assignment); + visitAssignment(assignment); + } + + @Override + public void visitCtOperatorAssignment(CtOperatorAssignment assignment) { + super.visitCtOperatorAssignment(assignment); + visitAssignment(assignment); + } + + /** + * Handles simple and operator assignments after Spoon has visited their children + */ + @SuppressWarnings("unchecked") + private void visitAssignment(CtAssignment assignment) throws LJError { CtExpression ex = assignment.getAssigned(); if (ex instanceof CtVariableWriteImpl) { @@ -379,7 +393,7 @@ public void visitCtIf(CtIf ifElement) { thenRefs = Predicate.createConjunction(expRefs, freshIsTrue); elseRefs = Predicate.createConjunction(expRefs, freshIsFalse); } - + freshRV = context.addInstanceToContext(pathVarName, factory.Type().BOOLEAN_PRIMITIVE, thenRefs, exp); } vcChecker.addPathVariable(freshRV); @@ -495,7 +509,7 @@ private void checkAssignment(String name, CtTypeReference type, CtExpression< CtElement parentElem, CtElement varDecl) throws LJError { getPutVariableMetadata(ex, name); - Predicate refinementFound = getRefinement(assignment); + Predicate refinementFound = getAssignmentRefinement(name, assignment, parentElem); if (refinementFound == null) { RefinedVariable rv = context.getVariableByName(name); if (rv instanceof Variable) { @@ -512,6 +526,17 @@ private void checkAssignment(String name, CtTypeReference type, CtExpression< checkVariableRefinements(refinementFound, name, type, parentElem, varDecl); } + /** + * Get the refinement for operator assignments (e.g. x += 1) + */ + private Predicate getAssignmentRefinement(String name, CtExpression assignment, CtElement parentElem) + throws LJError { + if (parentElem instanceof CtOperatorAssignment operatorAssignment) { + return otc.getOperatorAssignmentRefinement(name, operatorAssignment); + } + return getRefinement(assignment); + } + private Predicate getExpressionRefinements(CtExpression element) throws LJError { if (element instanceof CtVariableRead) { // CtVariableRead elemVar = (CtVariableRead) element; diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/OperationsChecker.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/OperationsChecker.java index de64ceb1..b8c86bfa 100644 --- a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/OperationsChecker.java +++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/OperationsChecker.java @@ -16,6 +16,9 @@ import liquidjava.utils.constants.Ops; import liquidjava.utils.constants.Types; import liquidjava.rj_language.Predicate; +import liquidjava.rj_language.ast.BinaryExpression; +import liquidjava.rj_language.ast.Expression; +import liquidjava.rj_language.ast.GroupExpression; import org.apache.commons.lang3.NotImplementedException; import spoon.reflect.code.BinaryOperatorKind; import spoon.reflect.code.CtAssignment; @@ -26,6 +29,7 @@ import spoon.reflect.code.CtInvocation; import spoon.reflect.code.CtLiteral; import spoon.reflect.code.CtLocalVariable; +import spoon.reflect.code.CtOperatorAssignment; import spoon.reflect.code.CtReturn; import spoon.reflect.code.CtUnaryOperator; import spoon.reflect.code.CtVariableRead; @@ -94,6 +98,18 @@ public void getBinaryOpRefinements(CtBinaryOperator operator) throws LJEr // TODO ADD TYPES } + /** + * Builds the refinement for a operator assignment. Java operator assignments such as {@code x += y} are modeled as + * {@code x = x + y}; the returned predicate refines the assigned value as {@code _ == current(x) rhs}. + */ + public Predicate getOperatorAssignmentRefinement(String assignedName, CtOperatorAssignment assignment) + throws LJError { + Predicate left = getCurrentVariableValue(assignedName); + Predicate right = getOperatorAssignmentRefinement(assignment.getAssignment()); + Predicate operation = Predicate.createOperation(left, getOperatorFromKind(assignment.getKind()), right); + return Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), operation); + } + /** * Finds and adds refinement metadata to the unary operation * @@ -280,6 +296,48 @@ private Predicate getOperationRefinementFromExternalLib(CtInvocation inv) thr return new Predicate(); } + /** + * Returns the latest symbolic value for a variable + */ + private Predicate getCurrentVariableValue(String name) { + Optional variableInstance = rtc.getContext().getLastVariableInstance(name); + return Predicate.createVar(variableInstance.map(VariableInstance::getName).orElse(name)); + } + + /** + * Converts a operator assignment into an arithmetic predicate operand + */ + private Predicate getOperatorAssignmentRefinement(CtExpression element) throws LJError { + if (element instanceof CtVariableRead variableRead) { + String name = variableRead.getVariable().getSimpleName(); + if (variableRead instanceof CtFieldRead) + name = String.format(Formats.THIS, name); + return getCurrentVariableValue(name); + } else if (element instanceof CtBinaryOperator binaryOperator) { + Predicate left = getOperatorAssignmentRefinement(binaryOperator.getLeftHandOperand()); + Predicate right = getOperatorAssignmentRefinement(binaryOperator.getRightHandOperand()); + return Predicate.createOperation(left, getOperatorFromKind(binaryOperator.getKind()), right); + } else if (element instanceof CtLiteral literal) { + if (literal.getValue() == null) + throw new CustomError("Null literals are not supported", literal.getPosition()); + return new Predicate(literal.getValue().toString(), element); + } + // unwrap wildcard equality: _ == expr -> expr + Predicate refinement = rtc.getRefinement(element); + Expression expression = unwrapGroupExpression(refinement.getExpression()); + if (expression instanceof BinaryExpression binaryExpression && Ops.EQ.equals(binaryExpression.getOperator()) + && Keys.WILDCARD.equals(binaryExpression.getFirstOperand().toString())) { + return new Predicate(binaryExpression.getSecondOperand()); + } + return refinement; + } + + private Expression unwrapGroupExpression(Expression expression) { + while (expression instanceof GroupExpression groupExpression) + expression = groupExpression.getExpression(); + return expression; + } + /** * Retrieves the refinements for the variable write inside unary operation *