Skip to content

Commit 2c1cfbc

Browse files
Address review comments on subclass spec inheritance
- Log unresolvable supertypes in debug mode (new DebugLog.warn) - Walk supertypes in a single loop - Split inheritSuperConstructorState into documented helpers and fix its misplaced Javadoc Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 553c53c commit 2c1cfbc

3 files changed

Lines changed: 76 additions & 40 deletions

File tree

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/DebugLog.java‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ public final class DebugLog {
2929
private static final String SMT_TAG = Colors.BLUE + "[SMT]" + Colors.RESET;
3030
private static final String SMT_CHECK = Colors.SALMON + "[SMT CHECK]" + Colors.RESET;
3131
private static final String SMP_TAG = Colors.YELLOW + "[SMP]" + Colors.RESET;
32+
private static final String WARN_TAG = Colors.SALMON + "[WARN]" + Colors.RESET;
3233

3334
private DebugLog() {
3435
}
@@ -37,6 +38,17 @@ public static boolean enabled() {
3738
return CommandLineLauncher.cmdArgs.debugMode;
3839
}
3940

41+
/**
42+
* A non-fatal problem the verifier worked around (e.g. a supertype it could not resolve), which may explain a
43+
* missing or unexpected result.
44+
*/
45+
public static void warn(String message) {
46+
if (!enabled()) {
47+
return;
48+
}
49+
System.out.println(WARN_TAG + " " + message);
50+
}
51+
4052
/**
4153
* One-line header for a verification check: emits the absolute file path + line so terminals (iTerm2, VS Code,
4254
* WezTerm, …) make it ⌘/Ctrl-clickable. Replaces the older two-line {@code info()} prints.

‎liquidjava-verifier/src/main/java/liquidjava/processor/context/RefinedVariable.java‎

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
package liquidjava.processor.context;
22

3-
import java.util.ArrayDeque;
43
import java.util.ArrayList;
54
import java.util.Deque;
5+
import java.util.LinkedList;
66
import java.util.List;
77
import java.util.Set;
8+
import liquidjava.diagnostics.DebugLog;
89
import liquidjava.rj_language.Predicate;
910
import spoon.reflect.declaration.CtElement;
1011
import spoon.reflect.reference.CtTypeReference;
@@ -35,26 +36,21 @@ public List<CtTypeReference<?>> getSuperTypes() {
3536
* direct superclass is {@code Exception}).
3637
*/
3738
public void addSuperTypes(CtTypeReference<?> ts, Set<CtTypeReference<?>> sts) {
38-
Deque<CtTypeReference<?>> todo = new ArrayDeque<>();
39-
if (ts != null)
40-
todo.add(ts);
41-
for (CtTypeReference<?> ct : sts)
42-
if (ct != null)
43-
todo.add(ct);
39+
// LinkedList, unlike ArrayDeque, accepts the nulls of a missing superclass, skipped when polled
40+
Deque<CtTypeReference<?>> todo = new LinkedList<>(sts);
41+
todo.addFirst(ts);
4442
while (!todo.isEmpty()) {
4543
CtTypeReference<?> t = todo.poll();
46-
if (supertypes.contains(t))
44+
if (t == null || supertypes.contains(t))
4745
continue;
4846
supertypes.add(t);
4947
try {
50-
CtTypeReference<?> sup = t.getSuperclass();
51-
if (sup != null)
52-
todo.add(sup);
53-
for (CtTypeReference<?> i : t.getSuperInterfaces())
54-
if (i != null)
55-
todo.add(i);
56-
} catch (RuntimeException | LinkageError ignored) {
48+
todo.add(t.getSuperclass());
49+
todo.addAll(t.getSuperInterfaces());
50+
} catch (RuntimeException | LinkageError e) {
5751
// a supertype that cannot be resolved (no source, not on the classpath) ends the walk on that branch
52+
DebugLog.warn("Could not resolve the supertypes of " + t.getQualifiedName()
53+
+ "; specs declared above it will not apply to " + getName() + " (" + e + ")");
5854
}
5955
}
6056
}

‎liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/object_checkers/AuxStateHandler.java‎

Lines changed: 53 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,6 @@ public static void handleConstructorState(CtConstructor<?> c, RefinedFunction f,
4949
}
5050
}
5151

52-
/**
53-
* An unannotated constructor whose body starts with {@code super(...)} leaves the object in the state that the
54-
* called superclass constructor's specification gives it (e.g. {@code MyException(String m, Throwable c) { super(m,
55-
* c); }} with the {@code Throwable(String, Throwable)} spec: {@code withThrowable(this)}). The super constructor's
56-
* parameters are renamed to the arguments passed, which must be parameters of this constructor or literals.
57-
*
58-
* @return whether a state was inherited (otherwise the caller falls back to the default state)
59-
*/
6052
/**
6153
* JDK classes whose constructors all pass their arguments unchanged to the superclass constructor with the same
6254
* parameter types (documented in their Javadoc), so a spec on that superclass constructor describes them too. Only
@@ -66,6 +58,43 @@ public static void handleConstructorState(CtConstructor<?> c, RefinedFunction f,
6658
private static final Set<String> DELEGATING_JDK_CLASSES = Set.of("java.lang.Exception",
6759
"java.lang.RuntimeException", "java.lang.Error");
6860

61+
/**
62+
* An unannotated constructor whose body starts with {@code super(...)} leaves the object in the state that the
63+
* called superclass constructor's specification gives it (e.g. {@code MyException(String m, Throwable c) { super(m,
64+
* c); }} with the {@code Throwable(String, Throwable)} spec: {@code withThrowable(this)}). The super constructor's
65+
* parameters are renamed to the arguments passed, which must be parameters of this constructor or literals.
66+
*
67+
* @return whether a state was inherited (otherwise the caller falls back to the default state)
68+
*/
69+
private static boolean inheritSuperConstructorState(CtConstructor<?> c, RefinedFunction f, TypeChecker tc) {
70+
CtInvocation<?> call = superConstructorCall(c);
71+
if (call == null)
72+
return false;
73+
var exe = call.getExecutable();
74+
RefinedFunction superF = specifiedConstructor(tc, exe.getSimpleName(), exe.getDeclaringType(),
75+
exe.getParameters());
76+
if (superF == null || superF.getToStates().isEmpty()
77+
|| superF.getArguments().size() != call.getArguments().size())
78+
return false;
79+
List<ObjectState> states = renameStates(superF.getToStates(), superParamsToArguments(superF, call));
80+
if (states == null)
81+
return false;
82+
f.setAllStates(states);
83+
return true;
84+
}
85+
86+
/** The {@code super(...)} call that starts the body of {@code c}, or null if it does not start with one. */
87+
private static CtInvocation<?> superConstructorCall(CtConstructor<?> c) {
88+
if (c.getBody() == null || c.getBody().getStatements().isEmpty()
89+
|| !(c.getBody().getStatement(0)instanceof CtInvocation<?> call))
90+
return null;
91+
var exe = call.getExecutable();
92+
if (exe == null || !exe.isConstructor() || exe.getDeclaringType() == null || c.getDeclaringType() == null
93+
|| exe.getDeclaringType().equals(c.getDeclaringType().getReference()))
94+
return null; // not a super(...) call (this(...) delegates within the class)
95+
return call;
96+
}
97+
6998
/** The specified constructor that {@code name(params)} of {@code type} ends up running, if one is known. */
7099
private static RefinedFunction specifiedConstructor(TypeChecker tc, String name, CtTypeReference<?> type,
71100
List<CtTypeReference<?>> params) {
@@ -82,19 +111,11 @@ private static RefinedFunction specifiedConstructor(TypeChecker tc, String name,
82111
return null;
83112
}
84113

85-
private static boolean inheritSuperConstructorState(CtConstructor<?> c, RefinedFunction f, TypeChecker tc) {
86-
if (c.getBody() == null || c.getBody().getStatements().isEmpty()
87-
|| !(c.getBody().getStatement(0)instanceof CtInvocation<?> call))
88-
return false;
89-
var exe = call.getExecutable();
90-
if (exe == null || !exe.isConstructor() || exe.getDeclaringType() == null || c.getDeclaringType() == null
91-
|| exe.getDeclaringType().equals(c.getDeclaringType().getReference()))
92-
return false; // not a super(...) call (this(...) delegates within the class)
93-
RefinedFunction superF = specifiedConstructor(tc, exe.getSimpleName(), exe.getDeclaringType(),
94-
exe.getParameters());
95-
if (superF == null || superF.getToStates().isEmpty()
96-
|| superF.getArguments().size() != call.getArguments().size())
97-
return false;
114+
/**
115+
* Maps each parameter of the super constructor to the name of the argument {@code call} passes for it: a parameter
116+
* of the calling constructor or a non-string literal, or null for any other expression.
117+
*/
118+
private static Map<String, String> superParamsToArguments(RefinedFunction superF, CtInvocation<?> call) {
98119
Map<String, String> rename = new HashMap<>();
99120
for (int i = 0; i < call.getArguments().size(); i++) {
100121
CtExpression<?> arg = call.getArguments().get(i);
@@ -107,12 +128,20 @@ else if (arg instanceof CtLiteral<?> lit && lit.getValue() != null && !(lit.getV
107128
else
108129
rename.put(param, null); // an argument we cannot name: usable only if the state ignores it
109130
}
131+
return rename;
132+
}
133+
134+
/**
135+
* The given states with the super constructor's parameters renamed, or null if one of them mentions a parameter
136+
* whose argument has no name.
137+
*/
138+
private static List<ObjectState> renameStates(List<Predicate> toStates, Map<String, String> rename) {
110139
List<ObjectState> states = new ArrayList<>();
111-
for (Predicate to : superF.getToStates()) {
140+
for (Predicate to : toStates) {
112141
for (Map.Entry<String, String> r : rename.entrySet()) {
113142
if (r.getValue() == null) {
114143
if (to.getVariableNames().contains(r.getKey()))
115-
return false;
144+
return null;
116145
} else {
117146
to = to.substituteVariable(r.getKey(), r.getValue());
118147
}
@@ -121,8 +150,7 @@ else if (arg instanceof CtLiteral<?> lit && lit.getValue() != null && !(lit.getV
121150
os.setTo(to);
122151
states.add(os);
123152
}
124-
f.setAllStates(states);
125-
return true;
153+
return states;
126154
}
127155

128156
/**

0 commit comments

Comments
 (0)