Skip to content

Commit 03a15b1

Browse files
Treat null literals as carrying no information instead of failing (#311)
Null literals (`x = null`, `x == null`, `x != null`, `f(null)`) no longer raise `Null literals are not supported` and stop verification of the rest of the file. In `OperationsChecker`, a null literal now yields `new Predicate()` (true), like String literals already do, and a binary operation with a null operand also yields `true` (otherwise e.g. `name == null` became `name == true` and failed with a Z3 sort mismatch). No null reasoning is added. Fixes #301 Testing: - New `CorrectNullLiterals.java`: null-initialised local, `== null` / `!= null` in `if`, `null` passed to an unrefined method, the `X s = null; try { ... } finally { if (s != null) ... }` shape, plus an unrelated int refinement that still verifies. - Manually checked that a failing refinement inside an `if (name == null)` branch is still reported as a Refinement Error. - `mvn test`: 339 tests, 0 failures (existing `ErrorEnumNull` still fails as expected). 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 3a2df08 commit 03a15b1

3 files changed

Lines changed: 116 additions & 6 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package testSuite;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
5+
6+
import liquidjava.specification.Refinement;
7+
8+
@SuppressWarnings("unused")
9+
public class CorrectNullLiterals {
10+
11+
static void describe(String label, Object value) {
12+
}
13+
14+
public static void main(String[] args) throws IOException {
15+
String name = null;
16+
if (name == null) {
17+
name = "default";
18+
}
19+
if (name != null) {
20+
describe(name, null);
21+
}
22+
23+
ByteArrayOutputStream out = null;
24+
try {
25+
out = new ByteArrayOutputStream();
26+
out.write(1);
27+
} finally {
28+
if (out != null) {
29+
out.close();
30+
}
31+
}
32+
33+
// refinements unrelated to the null literals are still checked
34+
@Refinement("x > 0")
35+
int x = 1;
36+
if (name != null) {
37+
@Refinement("y > 1")
38+
int y = x + 1;
39+
}
40+
}
41+
42+
// facts next to a null comparison are kept
43+
static void conjunction(String s, int y) {
44+
if (s == null && y > 0) {
45+
@Refinement("_ > 0")
46+
int z = y;
47+
}
48+
}
49+
50+
static void ternary(Object o) {
51+
@Refinement("_ == -1 || _ == 1")
52+
int x = (o == null) ? -1 : 1;
53+
}
54+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
@SuppressWarnings("unused")
6+
public class ErrorNullLiterals {
7+
8+
// comparisons with null are unknown booleans, so no branch may be considered unreachable
9+
10+
static void elseBranch(String name) {
11+
if (name == null) {
12+
System.out.println("none");
13+
} else {
14+
@Refinement("_ > 0")
15+
int x = -1; // Expect: Refinement Error
16+
}
17+
}
18+
19+
static void elseBranchOfDisjunction(String name, int y) {
20+
if (name == null || y > 0) {
21+
System.out.println("some");
22+
} else {
23+
@Refinement("_ > 0")
24+
int x = -1; // Expect: Refinement Error
25+
}
26+
}
27+
28+
static void elseBranchOfConjunction(String name, int y) {
29+
if (name != null && y > 0) {
30+
System.out.println("some");
31+
} else {
32+
@Refinement("_ <= 0")
33+
int z = y; // Expect: Refinement Error
34+
}
35+
}
36+
37+
static void ternary(Object o) {
38+
@Refinement("_ < 0")
39+
int x = (o == null) ? -1 : 1; // Expect: Refinement Error
40+
}
41+
42+
static void booleanValue(Object o) {
43+
boolean isNull = o == null;
44+
@Refinement("_ == true")
45+
boolean b = isNull; // Expect: Refinement Error
46+
}
47+
}

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/general_checkers/OperationsChecker.java‎

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
import java.util.List;
55
import java.util.Optional;
66

7-
import liquidjava.diagnostics.errors.CustomError;
87
import liquidjava.diagnostics.errors.LJError;
98
import liquidjava.processor.context.RefinedFunction;
109
import liquidjava.processor.context.RefinedVariable;
@@ -80,6 +79,8 @@ public <T> void getBinaryOpRefinements(CtBinaryOperator<T> operator) throws LJEr
8079
&& ((CtAssignment<?, ?>) parent).getAssigned()instanceof CtVariableWrite<?> parentVar) {
8180
oper = getOperationRefinements(operator, parentVar, operator);
8281

82+
} else if (hasNullOperand(operator)) {
83+
oper = createFreshValue(operator, new Predicate()); // null comparisons are not supported yet: unknown value
8384
} else {
8485
Predicate varLeft = getOperationRefinements(operator, left);
8586
Predicate varRight = getOperationRefinements(operator, right);
@@ -224,6 +225,8 @@ private Predicate getOperationRefinements(CtBinaryOperator<?> operator, CtVariab
224225
rtc.getContext().addVarToContext(elemName, elemVar.getType(), e, elemVar);
225226
return Predicate.createVar(returnName);
226227
} else if (element instanceof CtBinaryOperator<?> binop) {
228+
if (hasNullOperand(binop)) // null comparisons are not supported yet: unknown boolean value
229+
return createFreshValue(binop, new Predicate());
227230
Predicate right = getOperationRefinements(operator, parentVar, binop.getRightHandOperand());
228231
Predicate left = getOperationRefinements(operator, parentVar, binop.getLeftHandOperand());
229232
return Predicate.createOperation(left, getOperatorFromKind(binop.getKind()), right);
@@ -235,12 +238,10 @@ private Predicate getOperationRefinements(CtBinaryOperator<?> operator, CtVariab
235238
return new Predicate(String.format("(%s)", s), element);
236239

237240
} else if (element instanceof CtLiteral<?> l) {
238-
if (l.getType().getQualifiedName().equals("java.lang.String")) {
239-
// skip strings
241+
if (l.getType().getQualifiedName().equals("java.lang.String") || l.getValue() == null) {
242+
// skip strings and null literals (not supported yet, carry no information)
240243
return new Predicate();
241244
}
242-
if (l.getValue() == null)
243-
throw new CustomError("Null literals are not supported", l.getPosition());
244245

245246
return new Predicate(l.getValue().toString(), element);
246247

@@ -299,6 +300,14 @@ private Predicate getOperationRefinementFromExternalLib(CtInvocation<?> inv) thr
299300
return new Predicate();
300301
}
301302

303+
private static boolean hasNullOperand(CtBinaryOperator<?> binop) {
304+
return isNullLiteral(binop.getLeftHandOperand()) || isNullLiteral(binop.getRightHandOperand());
305+
}
306+
307+
private static boolean isNullLiteral(CtExpression<?> e) {
308+
return e instanceof CtLiteral<?> l && l.getValue() == null;
309+
}
310+
302311
/**
303312
* Returns the latest symbolic value for a variable
304313
*/
@@ -327,7 +336,7 @@ private Predicate getOperatorAssignmentRefinement(CtExpression<?> element) throw
327336
return Predicate.createITE(condition, thenExpression, elseExpression);
328337
} else if (element instanceof CtLiteral<?> literal) {
329338
if (literal.getValue() == null)
330-
throw new CustomError("Null literals are not supported", literal.getPosition());
339+
return new Predicate(); // null literals are not supported yet, carry no information
331340
return new Predicate(literal.getValue().toString(), element);
332341
} else if (element instanceof CtInvocation<?>) {
333342
VariableInstance invocationValue = (VariableInstance) element.getMetadata(Keys.TARGET);

0 commit comments

Comments
 (0)