Skip to content

Commit 87c0a99

Browse files
get value of final static vars
1 parent 40e105f commit 87c0a99

4 files changed

Lines changed: 167 additions & 0 deletions

File tree

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: 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
package testSuite;
2+
3+
import liquidjava.specification.Refinement;
4+
5+
@SuppressWarnings("unused")
6+
public class ErrorStaticFinalConstant {
7+
8+
static void requirePositive(@Refinement("_ > 0") int x) {
9+
}
10+
11+
public static void main(String[] args) {
12+
requirePositive(Integer.MIN_VALUE); // Refinement Error
13+
}
14+
}

liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
import liquidjava.processor.refinement_checker.object_checkers.AuxStateHandler;
1414
import liquidjava.rj_language.BuiltinFunctionPredicate;
1515
import liquidjava.rj_language.Predicate;
16+
import liquidjava.rj_language.ast.LiteralString;
1617
import liquidjava.utils.constants.Formats;
1718
import liquidjava.utils.constants.Keys;
1819
import liquidjava.utils.constants.Types;
@@ -274,13 +275,61 @@ public <T> void visitCtFieldRead(CtFieldRead<T> fieldRead) {
274275
String enumLiteral = String.format(Formats.ENUM, target, fieldName);
275276
fieldRead.putMetadata(Keys.REFINEMENT,
276277
Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), Predicate.createVar(enumLiteral)));
278+
} else if (tryStaticFinalConstantRefinement(fieldRead)) {
279+
// refinement metadata set by helper
277280
} else {
278281
fieldRead.putMetadata(Keys.REFINEMENT, new Predicate());
279282
// TODO DO WE WANT THIS OR TO SHOW ERROR MESSAGE?
280283
}
281284
super.visitCtFieldRead(fieldRead);
282285
}
283286

287+
/** Resolve a {@code static final} primitive/String constant to {@code #wild == <literal>}. */
288+
private <T> boolean tryStaticFinalConstantRefinement(CtFieldRead<T> fieldRead) {
289+
CtFieldReference<T> ref = fieldRead.getVariable();
290+
if (!ref.isStatic() || !ref.isFinal())
291+
return false;
292+
293+
Object value = null;
294+
CtField<?> decl = ref.getFieldDeclaration();
295+
if (decl != null && decl.getDefaultExpression()instanceof CtLiteral<?> lit)
296+
value = lit.getValue();
297+
if (value == null) {
298+
try {
299+
if (ref.getActualField()instanceof java.lang.reflect.Field jf) {
300+
jf.setAccessible(true);
301+
value = jf.get(null);
302+
}
303+
} catch (Throwable ignored) {
304+
// ClassNotFound / IllegalAccess / ExceptionInInitializerError — fall through.
305+
}
306+
}
307+
308+
Predicate literal = literalPredicateFor(value);
309+
if (literal == null)
310+
return false;
311+
fieldRead.putMetadata(Keys.REFINEMENT, Predicate.createEquals(Predicate.createVar(Keys.WILDCARD), literal));
312+
return true;
313+
}
314+
315+
private static Predicate literalPredicateFor(Object value) {
316+
if (value instanceof Boolean)
317+
return Predicate.createLit(value.toString(), Types.BOOLEAN);
318+
if (value instanceof Integer || value instanceof Short || value instanceof Byte)
319+
return Predicate.createLit(value.toString(), Types.INT);
320+
if (value instanceof Long)
321+
return Predicate.createLit(value.toString(), Types.LONG);
322+
if (value instanceof Float)
323+
return Predicate.createLit(value.toString(), Types.FLOAT);
324+
if (value instanceof Double)
325+
return Predicate.createLit(value.toString(), Types.DOUBLE);
326+
if (value instanceof Character)
327+
return Predicate.createLit("'" + value + "'", Types.CHAR);
328+
if (value instanceof String s)
329+
return new Predicate(new LiteralString("\"" + s + "\""));
330+
return null;
331+
}
332+
284333
@Override
285334
public <T> void visitCtVariableRead(CtVariableRead<T> variableRead) {
286335
super.visitCtVariableRead(variableRead);

0 commit comments

Comments
 (0)