Skip to content

Commit 46f6922

Browse files
CatarinaGamboaclaudercosta358
authored
Support for class Constants (#207)
We did not support using static final fields from classes in predicates or using the values in Java code. For example, both these cases would not have been possible before, and now are acceptable ```java static void clampInt(@refinement("_ >= Integer.MIN_VALUE && _ <= Integer.MAX_VALUE") int x) { } public static void main(String[] args) { clampInt(0); clampInt(Integer.MAX_VALUE); } ``` - Resolution follows Java scoping rules: fully-qualified name → compilation-unit imports (single + on-demand) → implicit java.lang, with reflection as the source-of-truth. User-defined enums/classes are left untouched for the existing SMT path. - Based on reflection, find the declaration and get the value assigned to it, and add it as a value. --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Co-authored-by: Ricardo Costa <rcosta.ms358@gmail.com>
1 parent fbc69c3 commit 46f6922

24 files changed

Lines changed: 666 additions & 3 deletions

CLAUDE.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
## Project Overview
6+
7+
LiquidJava is an additional type checker for Java that adds **liquid types** (refinements) and **typestates** on top of standard Java. Users annotate Java code with `@Refinement`, `@StateRefinement`, `@StateSet` etc. (from `liquidjava-api`); the verifier parses the program with [Spoon](https://spoon.gforge.inria.fr/), translates refinement predicates to SMT, and discharges verification conditions with **Z3**.
8+
9+
Requires **Java 20+** and **Maven 3.6+** (the parent POM declares 1.8 source/target, but the verifier module overrides to 20).
10+
11+
## Module Layout
12+
13+
This is a Maven multi-module build (`pom.xml` is the umbrella):
14+
15+
- `liquidjava-api` — published annotations (`@Refinement`, `@RefinementAlias`, `@StateRefinement`, `@StateSet`, ghost functions). Stable artifact users depend on.
16+
- `liquidjava-verifier` — the actual checker (Spoon processor + RJ AST + SMT translator). Published as `io.github.liquid-java:liquidjava-verifier`.
17+
- `liquidjava-example` — sample programs **and the test suite** under `src/main/java/testSuite/`. The verifier's tests scan this directory.
18+
19+
Verifier package map (`liquidjava-verifier/src/main/java/liquidjava/`):
20+
- `api/` — entrypoints; `CommandLineLauncher` is the CLI main.
21+
- `processor/` — Spoon processors. `RefinementProcessor` orchestrates; `refinement_checker/` contains `RefinementTypeChecker`, `MethodsFirstChecker`, `ExternalRefinementTypeChecker`, plus `general_checkers/` and `object_checkers/` for typestate.
22+
- `ast/` — AST of the Refinements Language (RJ).
23+
- `rj_language/` — parser from refinement strings to RJ AST.
24+
- `smt/` — Z3 translation (`TranslatorToZ3`, `ExpressionToZ3Visitor`, `SMTEvaluator`, `Counterexample`).
25+
- `errors/`, `utils/`, `diagnostics/`.
26+
27+
## Commands
28+
29+
Build / install everything:
30+
```bash
31+
mvn clean install
32+
```
33+
34+
Run the test suite (verifier module, runs whole `testSuite/` dir):
35+
```bash
36+
mvn test
37+
```
38+
39+
Run a single test method (JUnit 4/5 mix — both work via Surefire):
40+
```bash
41+
mvn -pl liquidjava-verifier -Dtest=TestExamples test
42+
mvn -pl liquidjava-verifier -Dtest=TestExamples#testMultiplePaths test
43+
```
44+
45+
Verify a specific file/directory from CLI (uses the `liquidjava` script in repo root, macOS/Linux):
46+
```bash
47+
./liquidjava liquidjava-example/src/main/java/testSuite/CorrectSimpleAssignment.java
48+
```
49+
Equivalent raw form:
50+
```bash
51+
mvn exec:java -pl liquidjava-verifier \
52+
-Dexec.mainClass="liquidjava.api.CommandLineLauncher" \
53+
-Dexec.args="/path/to/file_or_dir"
54+
```
55+
56+
Code formatting runs automatically in the `validate` phase via `formatter-maven-plugin` (configured for Java 20 in `liquidjava-verifier/pom.xml`); no separate lint command.
57+
58+
## Test Suite Conventions
59+
60+
Tests are discovered by `TestExamples#testPath` (parameterized) under `liquidjava-example/src/main/java/testSuite/`:
61+
62+
- Single-file cases: filename starts with `Correct…` or `Error…`.
63+
- Directory cases: directory name contains the substring `correct` or `error`.
64+
- Anything else is **ignored** (so helper sources can live alongside).
65+
- Expected error for a failing case:
66+
- Single file: write the expected error title in a comment on the **first line** of the file.
67+
- Directory: place a `.expected` file in that directory containing the expected error title.
68+
69+
When adding new test cases, place them under `liquidjava-example/src/main/java/testSuite/` following the naming rules above — that is the only way they get picked up.
70+
71+
## Architecture Notes That Span Files
72+
73+
- **Two-pass typechecking.** `MethodsFirstChecker` collects method signatures and refinement contracts before `RefinementTypeChecker` walks bodies, so forward references and recursion resolve. Edits to one usually need a matching change in the other.
74+
- **Refinement string → AST → Z3.** A `@Refinement("a > 0")` string flows: `rj_language` parser → `ast` nodes → `smt/TranslatorToZ3` / `ExpressionToZ3Visitor`. New predicate forms generally require touching all three.
75+
- **External refinements.** `ExternalRefinementTypeChecker` plus `*Refinements.java` companion files specify contracts for third-party APIs without modifying their sources. The `co-specifying-liquidjava` skill covers this workflow.
76+
- **Typestate** lives in `processor/refinement_checker/object_checkers/` and uses `@StateRefinement` / `@StateSet` from the API. Ghost-state predicates flow through the same SMT pipeline as value refinements.
77+
- **Z3 dependency.** The verifier shells out to Z3 via JNI bindings; failures often surface as `SMTResult` errors or counterexamples, not Java exceptions.
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package testSuite;
2+
3+
import javax.imageio.ImageWriteParam;
4+
5+
import liquidjava.specification.Refinement;
6+
7+
@SuppressWarnings("unused")
8+
public class CorrectImageWriteParamModes {
9+
10+
static void requireExplicit(
11+
@Refinement("_ == ImageWriteParam.MODE_EXPLICIT") int mode) {
12+
}
13+
14+
static void requireKnownMode(
15+
@Refinement("_ == ImageWriteParam.MODE_DISABLED || _ == ImageWriteParam.MODE_DEFAULT "
16+
+ "|| _ == ImageWriteParam.MODE_EXPLICIT || _ == ImageWriteParam.MODE_COPY_FROM_METADATA") int mode) {
17+
}
18+
19+
public static void main(String[] args) {
20+
requireExplicit(ImageWriteParam.MODE_EXPLICIT);
21+
requireKnownMode(ImageWriteParam.MODE_DEFAULT);
22+
requireKnownMode(ImageWriteParam.MODE_DISABLED);
23+
requireKnownMode(2);
24+
}
25+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class CorrectSourceStaticFinalInPredicate {
6+
7+
static void requireBelowLimit(@Refinement("_ < LIMITS.MAX") double x) {
8+
}
9+
10+
public static void main(String[] args) {
11+
requireBelowLimit(5.0);
12+
}
13+
14+
static class LIMITS {
15+
static final double MAX = 10.0;
16+
}
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class CorrectStaticFinalCharInPredicate {
6+
7+
static void requireMaxChar(@Refinement("_ == Character.MAX_VALUE") char c) {
8+
}
9+
10+
public static void main(String[] args) {
11+
requireMaxChar(Character.MAX_VALUE);
12+
}
13+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package testSuite;
2+
3+
import javax.imageio.ImageWriteParam;
4+
5+
import liquidjava.specification.Refinement;
6+
7+
@SuppressWarnings("unused")
8+
public class CorrectStaticFinalConstant {
9+
10+
static void requirePositive(@Refinement("_ > 0") int x) {
11+
}
12+
13+
static void requireAtLeast(@Refinement("_ >= 1024") int x) {
14+
}
15+
16+
public static void main(String[] args) {
17+
// Reflective resolution of a JDK static final int constant.
18+
requirePositive(Integer.MAX_VALUE);
19+
20+
// Reflective resolution of a JDK static final int with a known concrete value.
21+
requireAtLeast(Short.MAX_VALUE);
22+
}
23+
24+
void other(){
25+
@Refinement("_ > 0 && _ <= 1") int x = ImageWriteParam.MODE_DEFAULT;
26+
}
27+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
@SuppressWarnings("unused")
6+
public class CorrectStaticFinalInPredicate {
7+
8+
// Constants resolved inside the predicate string itself.
9+
static void clampInt(@Refinement("_ >= Integer.MIN_VALUE && _ <= Integer.MAX_VALUE") int x) {
10+
}
11+
12+
static void belowMaxLong(@Refinement("_ <= Long.MAX_VALUE") long x) {
13+
}
14+
15+
static void notMaxByte(@Refinement("_ != Byte.MAX_VALUE") int x) {
16+
}
17+
18+
public static void main(String[] args) {
19+
clampInt(0);
20+
clampInt(Integer.MAX_VALUE);
21+
clampInt(Integer.MIN_VALUE);
22+
belowMaxLong(123L);
23+
notMaxByte(0);
24+
}
25+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package testSuite;
2+
3+
import static java.lang.Math.PI;
4+
5+
import liquidjava.specification.Refinement;
6+
7+
/**
8+
* Locks in that the import walk ignores {@code import static} (kind {@code FIELD}) without confusing the resolver:
9+
* the file has a static import for an unrelated symbol, and a refinement that uses a regular {@code Type.CONST}
10+
* reference. The verifier must skip the static import and resolve {@code Math.PI} via the implicit {@code java.lang}
11+
* fallback (or the static-import target's class — either path is correct here).
12+
*/
13+
@SuppressWarnings("unused")
14+
public class CorrectStaticImportInPredicate {
15+
16+
static double useUnused() {
17+
return PI;
18+
}
19+
20+
static void requireBelowFour(@Refinement("_ < 4.0") double x) {
21+
}
22+
23+
public static void main(String[] args) {
24+
requireBelowFour(Math.PI);
25+
}
26+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package testSuite;
2+
3+
import javax.imageio.ImageWriteParam;
4+
5+
import liquidjava.specification.Refinement;
6+
7+
@SuppressWarnings("unused")
8+
public class ErrorImageWriteParamModes {
9+
10+
static void requireExplicit(@Refinement("_ == ImageWriteParam.MODE_EXPLICIT") int mode) {
11+
}
12+
13+
public static void main(String[] args) {
14+
// MODE_DEFAULT is 1, not 2 (MODE_EXPLICIT).
15+
requireExplicit(ImageWriteParam.MODE_DEFAULT); // Refinement Error
16+
}
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class ErrorSourceStaticFinalInPredicate {
6+
7+
static void requireBelowLimit(@Refinement("_ < LIMITS.MAX") double x) {
8+
}
9+
10+
public static void main(String[] args) {
11+
requireBelowLimit(15.0); // Refinement Error
12+
}
13+
14+
static class LIMITS {
15+
static final double MAX = 10.0;
16+
}
17+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
public class ErrorStaticFinalCharInPredicate {
6+
7+
static void requireMaxChar(@Refinement("_ == Character.MAX_VALUE") char c) {
8+
}
9+
10+
public static void main(String[] args) {
11+
requireMaxChar('\''); // Refinement Error
12+
}
13+
}

0 commit comments

Comments
 (0)