Skip to content

Commit a415821

Browse files
Propagate typestate changes to must-aliases (#337)
A typestate transition creates a new instance of the variable it was made through (`b#2` with `closed(b#2)`), but other variables bound to the same object (`Res b = a;` gives `b#1 == a#1`) kept their old instance, so `a` was still considered open after `b.close()`. After a transition, every other reference variable of a related type whose current instance is provably equal to the target's previous instance gets a new instance equal to the transitioned one, so the new state is seen through all must-aliases (including aliases of aliases and aliases created by a later assignment). Reassigning the alias (`b = new Res()`) breaks the proved equality, so later changes through it no longer affect the original. Variables that may, but need not, alias the target keep their state. Fixes #337 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent ad055b4 commit a415821

3 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"open", "closed"})
7+
class SeparateResource {
8+
@StateRefinement(to = "open(this)")
9+
public SeparateResource() {}
10+
11+
@StateRefinement(from = "open(this)")
12+
public void read() {}
13+
14+
@StateRefinement(from = "open(this)", to = "closed(this)")
15+
public void close() {}
16+
}
17+
18+
public class CorrectStateChangeThroughAlias {
19+
static void distinctObjects() {
20+
SeparateResource a = new SeparateResource();
21+
SeparateResource b = new SeparateResource();
22+
b.close();
23+
a.read();
24+
}
25+
26+
static void reassignmentBreaksAlias() {
27+
SeparateResource a = new SeparateResource();
28+
SeparateResource b = a;
29+
b = new SeparateResource();
30+
b.close();
31+
a.read();
32+
}
33+
34+
static void aliasSeesOpenState() {
35+
SeparateResource a = new SeparateResource();
36+
SeparateResource b = a;
37+
b.read();
38+
a.read();
39+
a.close();
40+
}
41+
42+
static void aliasClosedInBothBranches(boolean done) {
43+
SeparateResource a = new SeparateResource();
44+
SeparateResource b = a;
45+
if (done) {
46+
b.close();
47+
} else {
48+
a.close();
49+
}
50+
SeparateResource c = new SeparateResource();
51+
c.read();
52+
}
53+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.StateRefinement;
4+
import liquidjava.specification.StateSet;
5+
6+
@StateSet({"open", "closed"})
7+
class AliasedResource {
8+
@StateRefinement(to = "open(this)")
9+
public AliasedResource() {}
10+
11+
@StateRefinement(from = "open(this)")
12+
public void read() {}
13+
14+
@StateRefinement(from = "open(this)", to = "closed(this)")
15+
public void close() {}
16+
}
17+
18+
public class ErrorStateChangeThroughAlias {
19+
static void localAlias() {
20+
AliasedResource a = new AliasedResource();
21+
AliasedResource b = a;
22+
b.close();
23+
a.read(); // Expect: State Refinement Error
24+
}
25+
26+
static void aliasOfAlias() {
27+
AliasedResource a = new AliasedResource();
28+
AliasedResource b = a;
29+
AliasedResource c = b;
30+
a.close();
31+
c.read(); // Expect: State Refinement Error
32+
}
33+
34+
static void aliasAssignedLater() {
35+
AliasedResource a = new AliasedResource();
36+
AliasedResource b = new AliasedResource();
37+
b = a;
38+
b.close();
39+
a.read(); // Expect: State Refinement Error
40+
}
41+
42+
static void aliasClosedInBranch(boolean done) {
43+
AliasedResource a = new AliasedResource();
44+
AliasedResource b = a;
45+
if (done) {
46+
b.close();
47+
}
48+
a.read(); // Expect: State Refinement Error
49+
}
50+
}

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

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -618,6 +618,7 @@ private static void changeState(TypeChecker tc, VariableInstance vi, RefinedFunc
618618
transitionedState = checkOldMentions(transitionedState, instanceName, newInstanceName);
619619
// update of state of new instance of this#n#(whatever it was + 1)
620620
addInstanceWithState(tc, name, newInstanceName, vi, transitionedState, invocation);
621+
updateAliases(tc, name, vi, newInstanceName, invocation);
621622
}
622623
}
623624
if (!found) { // Reaches the end of stateChange no matching states
@@ -699,6 +700,54 @@ private static void addInstanceWithState(TypeChecker tc, String superName, Strin
699700
invocation.putMetadata(Keys.TARGET, vi2);
700701
}
701702

703+
/**
704+
* A state change made through one reference changes the state of the object, so every other variable that must
705+
* refer to the same object (e.g. {@code b} after {@code Res b = a;}) gets a new instance equal to the transitioned
706+
* one. Variables that may, but need not, alias the target keep their state.
707+
*
708+
* @param tc
709+
* @param name
710+
* the variable the state change was made through
711+
* @param previous
712+
* the target instance before the state change
713+
* @param transitioned
714+
* the name of the target instance after the state change
715+
* @param invocation
716+
*/
717+
private static void updateAliases(TypeChecker tc, String name, VariableInstance previous, String transitioned,
718+
CtElement invocation) {
719+
Context ctx = tc.getContext();
720+
for (RefinedVariable rv : ctx.getCtxVars()) {
721+
if (!(rv instanceof Variable alias) || alias.getName().equals(name) || !relatedTypes(alias, previous))
722+
continue;
723+
String current = alias.getLastInstance().map(Refined::getName).orElse(alias.getName());
724+
Predicate identity = Predicate.createEquals(Predicate.createVar(current),
725+
Predicate.createVar(previous.getName()));
726+
try {
727+
if (!tc.checkStateSMT(new Predicate(), identity, invocation.getPosition(), true))
728+
continue;
729+
} catch (LJError e) {
730+
continue; // identity not provable: not a must-alias
731+
}
732+
String aliasInstanceName = String.format(Formats.INSTANCE, alias.getName(), ctx.getCounter());
733+
RefinedVariable aliasInstance = ctx.addInstanceToContext(aliasInstanceName, alias.getType(),
734+
Predicate.createEquals(Predicate.createVar(aliasInstanceName), Predicate.createVar(transitioned)),
735+
invocation);
736+
alias.getSuperTypes().forEach(aliasInstance::addSuperType);
737+
ctx.addRefinementInstanceToVariable(alias.getName(), aliasInstanceName);
738+
}
739+
}
740+
741+
/** Whether one variable's type is the other's type or one of its recorded supertypes. */
742+
private static boolean relatedTypes(RefinedVariable v1, RefinedVariable v2) {
743+
if (v1.getType() == null || v2.getType() == null || v1.getType().isPrimitive() || v2.getType().isPrimitive())
744+
return false;
745+
String t1 = v1.getType().getQualifiedName();
746+
String t2 = v2.getType().getQualifiedName();
747+
return t1.equals(t2) || v1.getSuperTypes().stream().anyMatch(t -> t.getQualifiedName().equals(t2))
748+
|| v2.getSuperTypes().stream().anyMatch(t -> t.getQualifiedName().equals(t1));
749+
}
750+
702751
/** Whether the access denotes the receiver of the enclosing type, including unqualified super. */
703752
public static boolean isCurrentReceiver(CtElement target) {
704753
CtType<?> owner = target.getParent(CtType.class);

0 commit comments

Comments
 (0)