Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Added enum type support with tests
  • Loading branch information
cheestree committed Mar 14, 2026
commit c70cc5a8ea91399409e80388ae2a6f6606d4c4b6
33 changes: 33 additions & 0 deletions liquidjava-example/src/main/java/testSuite/CorrectEnumUsage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package testSuite;

import liquidjava.specification.StateRefinement;
import liquidjava.specification.StateSet;

@SuppressWarnings("unused")
@StateSet({"photoMode", "videoMode", "noMode"})
class CorrectEnumUsage {
enum Mode {
Photo, Video, Unknown
}

Mode mode;
@StateRefinement(to="noMode(this)")
public CorrectEnumUsage() {}

@StateRefinement(from="noMode(this) && mode == Mode.Photo", to="photoMode(this)")
@StateRefinement(from="noMode(this) && mode == Mode.Video", to="videoMode(this)")
public void setMode(Mode mode) {
this.mode = mode;
}

@StateRefinement(from="photoMode(this)")
public void takePhoto() {}


public static void main(String[] args) {
// Correct
CorrectEnumUsage st = new CorrectEnumUsage();
st.setMode(Mode.Photo);
st.takePhoto();
}
}
35 changes: 35 additions & 0 deletions liquidjava-example/src/main/java/testSuite/ErrorEnumUsage.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// State Refinement Error
package testSuite;

import liquidjava.specification.StateRefinement;
import liquidjava.specification.StateSet;


@SuppressWarnings("unused")
@StateSet({"photoMode", "videoMode", "noMode"})
class ErrorEnumUsage {
enum Mode {
Photo, Video, Unknown
}

Mode mode;
@StateRefinement(to="noMode(this)")
public ErrorEnumUsage() {}

@StateRefinement(from="noMode(this) && mode == Mode.Photo", to="photoMode(this)")
@StateRefinement(from="noMode(this) && mode == Mode.Video", to="videoMode(this)")
public void setMode(Mode mode) {
this.mode = mode;
}

@StateRefinement(from="photoMode(this)")
public void takePhoto() {}


public static void main(String[] args) {
// Correct
ErrorEnumUsage st = new ErrorEnumUsage();
st.setMode(Mode.Video);
st.takePhoto(); //error
}
}
4 changes: 4 additions & 0 deletions liquidjava-verifier/src/main/antlr4/rj/grammar/RJ.g4
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ literalExpression:
| literal #lit
| ID #var
| functionCall #invocation
| enumCall #enum
;

functionCall:
Expand All @@ -55,6 +56,9 @@ ghostCall:
aliasCall:
ID_UPPER '(' args? ')';

enumCall:
OBJECT_TYPE;
Copy link
Copy Markdown
Collaborator

@rcosta358 rcosta358 Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand you used OBJECT_TYPE because of the rule ambiguity. However, this raises a concern, because it allows for multiple chaining of ids (e.g., Mode.photo.photo, Mode.photo.photo.photo, etc.) which should not be syntactically valid.

We could avoid the ambiguity by forcing the OBJECT_TYPE to always start with a lowercase letter and declare enumCall as ID '.' ID, but Java packages are allowed start with an uppercase letter (even though it's highly discouraged).

So, we can introduce another terminal, just for enums:

enumCall: 
	ENUM_CALL;

// ...

ENUM_CALL: [a-zA-Z_][a-zA-Z0-9_]* '.' [a-zA-Z_][a-zA-Z0-9_]*;

This way, you guarantee the enums are always represented by two ids separated by a dot, without ambiguity.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's understandable, my issue with that, at the time of development, was if ID and ID_UPPER wouldn't, even if they are slightly different, lead to ambiguity due to other grammar rules. I should've explored more formats.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have support for enums that live in another package, via qualified name. I agree with @rcosta358 that we want a first version working, and then we can think about this. To achieve this, we need a name resolution (à lá Spoon).


args: pred (',' pred)* ;


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,13 @@
import liquidjava.diagnostics.errors.LJError;
import liquidjava.processor.context.Context;
import liquidjava.processor.refinement_checker.general_checkers.MethodsFunctionsChecker;
import liquidjava.rj_language.Predicate;
import liquidjava.utils.constants.Formats;
import liquidjava.utils.constants.Types;
import spoon.reflect.declaration.CtClass;
import spoon.reflect.declaration.CtConstructor;
import spoon.reflect.declaration.CtEnum;
import spoon.reflect.declaration.CtEnumValue;
import spoon.reflect.declaration.CtInterface;
import spoon.reflect.declaration.CtMethod;
import spoon.reflect.declaration.CtType;
Expand Down Expand Up @@ -116,4 +121,19 @@ public <R> void visitCtMethod(CtMethod<R> method) {
}
context.exitContext();
}

@Override
public <T extends Enum<?>> void visitCtEnum(CtEnum<T> enumRead) {
String enumName = enumRead.getSimpleName();
String qualifiedEnumName = enumRead.getQualifiedName();
int ordinal = 0;
for (CtEnumValue ev : enumRead.getEnumValues()) {
Comment thread
rcosta358 marked this conversation as resolved.
Outdated
String varName = String.format(Formats.ENUM_VALUE, enumName, ev.getSimpleName());
Predicate refinement = Predicate.createEquals(Predicate.createVar(varName),
Predicate.createLit(String.valueOf(ordinal), Types.INT));
context.addGlobalVariableToContext(varName, qualifiedEnumName, enumRead.getReference(), refinement);
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why global? Shouldn't it only be visible within the class or package where it was declared?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there is an enum that is used across methods, and a shared package is needed, when the context is reinitialized, the local context is cleaned. If it's kept in the global context, the placement is irrelevant.
For example, the following common package a layer above the testSuite.

image

When line 134 is changed to context.addVarToContext(varName, enumRead.getReference(), refinement, enumRead), the following occurs:

image

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's not really a shared package because it's not even included in the verification, each test is ran individually from its file or folder.

Copy link
Copy Markdown
Collaborator

@rcosta358 rcosta358 Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is: if I declare an enum X in class A, should I be able to use it in class B? It might be fine, just raising some questions about how it should work.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, if we can use enums from different classes, we should make sure they have their fully qualified name to distinguish enums with the same name from different classes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't realize the testing was that restrictive. I can try and create some test scenarios that take into consideration global and local context. The other option would be to restrict enum verification to only in the same file, but that feels like a cop-out instead of an actual fix/intended functionality. 🤔

ordinal++;
}
super.visitCtEnum(enumRead);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -269,7 +269,11 @@ public <T> void visitCtFieldRead(CtFieldRead<T> fieldRead) {
String targetName = fieldRead.getTarget().toString();
fieldRead.putMetadata(Keys.REFINEMENT, Predicate.createEquals(Predicate.createVar(Keys.WILDCARD),
BuiltinFunctionPredicate.length(targetName, fieldRead)));

} else if (fieldRead.getVariable().getDeclaringType().isEnum()) {
String target = fieldRead.getVariable().getDeclaringType().getSimpleName();
String enumLiteral = String.format(Formats.ENUM_VALUE, target, fieldName);
fieldRead.putMetadata(Keys.REFINEMENT,
Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), Predicate.createVar(enumLiteral)));
} else {
fieldRead.putMetadata(Keys.REFINEMENT, new Predicate());
// TODO DO WE WANT THIS OR TO SHOW ERROR MESSAGE?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,15 @@
import liquidjava.rj_language.ast.UnaryExpression;
import liquidjava.rj_language.ast.Var;
import liquidjava.utils.Utils;
import liquidjava.utils.constants.Formats;
import liquidjava.utils.constants.Keys;

import org.antlr.v4.runtime.tree.ParseTree;
import org.apache.commons.lang3.NotImplementedException;
import rj.grammar.RJParser.AliasCallContext;
import rj.grammar.RJParser.ArgsContext;
import rj.grammar.RJParser.DotCallContext;
import rj.grammar.RJParser.EnumContext;
import rj.grammar.RJParser.ExpBoolContext;
import rj.grammar.RJParser.ExpContext;
import rj.grammar.RJParser.ExpGroupContext;
Expand Down Expand Up @@ -156,9 +158,10 @@ private Expression literalExpressionCreate(ParseTree rc) throws LJError {
return new GroupExpression(create(((LitGroupContext) rc).literalExpression()));
else if (rc instanceof LitContext)
return create(((LitContext) rc).literal());
else if (rc instanceof VarContext) {
else if (rc instanceof VarContext)
return new Var(((VarContext) rc).ID().getText());

else if (rc instanceof EnumContext) {
return new Var(enumCreate((EnumContext) rc));
Comment thread
rcosta358 marked this conversation as resolved.
Outdated
} else {
return create(((InvocationContext) rc).functionCall());
}
Expand Down Expand Up @@ -234,6 +237,15 @@ private List<Expression> getArgs(ArgsContext args) throws LJError {
return le;
}

private String enumCreate(EnumContext rc) {
String enumText = rc.enumCall().getText();
int lastDot = enumText.lastIndexOf('.');
String enumClass = enumText.substring(0, lastDot);
String enumConst = enumText.substring(lastDot + 1);
String varName = String.format(Formats.ENUM_VALUE, enumClass, enumConst);
return varName;
Copy link
Copy Markdown
Collaborator

@rcosta358 rcosta358 Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't like the "enum#%s#%s" format because it's hard to read in the error messages. Here you can simply return the enumText, and everywhere else still use this format but change it to "%s.%s" instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I should've gone with a better format, I admit. I wasn't sure due to the rest of the existing types.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We might want to "decode" enums when they come back in the error messages back to their Refinement version. Two approaches: 1) having a translation map. 2) storing the source of each variable in the Verification Condition (I'm doing this in aeon, and I think we discussed it for LJ).

This is not specific for Enums, we might want to do this for other types.

}

private Expression literalCreate(LiteralContext literalContext) throws LJError {
if (literalContext.BOOL() != null)
return new LiteralBoolean(literalContext.BOOL().getText());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ private static Expr<?> getExpr(Context z3, String name, CtTypeReference<?> type)
case "long", "java.lang.Long" -> z3.mkRealConst(name);
case "float", "double", "java.lang.Float", "java.lang.Double" -> (FPExpr) z3.mkConst(name, z3.mkFPSort64());
case "int[]" -> z3.mkArrayConst(name, z3.mkIntSort(), z3.mkIntSort());
default -> z3.mkConst(name, z3.mkUninterpretedSort(typeName));
case "java.lang.Enum" -> z3.mkIntConst(name);
default -> {
if (type.isEnum())
yield z3.mkIntConst(name);
yield z3.mkConst(name, z3.mkUninterpretedSort(typeName));
}
Comment thread
rcosta358 marked this conversation as resolved.
Outdated
};
}

Expand Down Expand Up @@ -88,6 +93,7 @@ static Sort getSort(Context z3, String sort) {
case "float", "java.lang.Float" -> z3.mkFPSort32();
case "double", "java.lang.Double" -> z3.mkFPSortDouble();
case "int[]" -> z3.mkArraySort(z3.mkIntSort(), z3.mkIntSort());
case "java.lang.Enum" -> z3.getIntSort();
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is there a reason for this to be an IntSort if we created the EnumSorts before?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was part of a case I was trying to cover, but I ended up learning that it could only use a sort with an actual enum type, not the abstract class java.lang.Enum. I think I forgot to remove it 💫

case "String" -> z3.getStringSort();
case "void" -> z3.mkUninterpretedSort("void");
default -> z3.mkUninterpretedSort(sort);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ public final class Formats {
public static final String INSTANCE = "#%s_%d";
public static final String THIS = "this#%s";
public static final String RET = "#ret_%d";
public static final String ENUM_VALUE = "enum#%s#%s";
Comment thread
rcosta358 marked this conversation as resolved.
Outdated
}