diff --git a/liquidjava-example/src/main/java/testSuite/classes/imagewrite_correct/ImageWriteParamsRefinements.java b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_correct/ImageWriteParamsRefinements.java new file mode 100644 index 00000000..32491265 --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_correct/ImageWriteParamsRefinements.java @@ -0,0 +1,88 @@ +package testSuite.classes.imagewrite_correct; + +import java.util.Locale; + +import javax.imageio.ImageWriteParam; + +import liquidjava.specification.ExternalRefinementsFor; +import liquidjava.specification.Refinement; +import liquidjava.specification.StateRefinement; +import liquidjava.specification.StateSet; + +/** + * External typestate specification for {@code javax.imageio.ImageWriteParam}. + * + *
+ * The class is modelled as two independent ghost-state dimensions — tiling and compression — so a configuration error + * in one dimension does not mask the other. The conditional {@code setTilingMode} / {@code setCompressionMode} + * transitions only reach the {@code *Explicit} state when called with {@code MODE_EXPLICIT}; any other mode leaves the + * param in its {@code start*} state, which the dimension-specific setters reject. + */ +@StateSet({ "startTiling", "tilingExplicit", "tilingSet" }) +@StateSet({ "startCompression", "compressionExplicit", "compressionSet" }) +@ExternalRefinementsFor("javax.imageio.ImageWriteParam") +public interface ImageWriteParamsRefinements { + + // Constructor + @StateRefinement(to = "startTiling(this) && startCompression(this)") + void ImageWriteParam(Locale locale); + + // Tiling related methods + + @StateRefinement(to = "(mode == ImageWriteParam.MODE_EXPLICIT)? tilingExplicit(this) : startTiling(this)") + void setTilingMode(int mode); + + @StateRefinement(from = "tilingExplicit(this)", to = "tilingSet(this)") + @StateRefinement(from = "tilingSet(this)", to = "tilingSet(this)") + void setTiling(@Refinement("_ > 0") int tileWidth, @Refinement("_ > 0") int tileHeight, int tileGridXOffset, + int tileGridYOffset); + + @StateRefinement(from = "tilingSet(this)") + int getTileGridXOffset(); + + @StateRefinement(from = "tilingSet(this)") + int getTileGridYOffset(); + + @StateRefinement(from = "tilingSet(this)") + int getTileHeight(); + + @StateRefinement(from = "tilingSet(this)") + int getTileWidth(); + + @StateRefinement(from = "tilingExplicit(this)") + @StateRefinement(from = "tilingSet(this)", to = "tilingExplicit(this)") + void unsetTiling(); + + void setProgressiveMode(@Refinement("ImageWriteParam.MODE_DISABLED == mode || mode == ImageWriteParam.MODE_DEFAULT || mode == ImageWriteParam.MODE_COPY_FROM_METADATA") int mode); + + // Compression related methods + + @StateRefinement(to = "mode == ImageWriteParam.MODE_EXPLICIT? compressionExplicit(this) : startCompression(this)") + void setCompressionMode(int mode); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + void setCompressionQuality(@Refinement("_ >= 0.0 && _ <= 1.0") float quality); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + String getCompressionType(); + + @StateRefinement(from = "compressionExplicit(this)", to = "compressionSet(this)") + void setCompressionType(String compressionType); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)", to = "compressionExplicit(this)") + void unsetCompression(); + + @StateRefinement(from = "compressionSet(this)") + String getLocalizedCompressionTypeName(); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + boolean isCompressionLossless(); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + float getCompressionQuality(); +} diff --git a/liquidjava-example/src/main/java/testSuite/classes/imagewrite_correct/JpegExporter.java b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_correct/JpegExporter.java new file mode 100644 index 00000000..e7d6b657 --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_correct/JpegExporter.java @@ -0,0 +1,32 @@ +package testSuite.classes.imagewrite_correct; + +import java.util.Locale; + +import javax.imageio.ImageWriteParam; + +/** + * A JPEG export pipeline configured correctly against {@link ImageWriteParamsRefinements}. + * + *
+ * Both ghost-state dimensions are driven through their full transition path: each dimension's mode is set to + * {@code MODE_EXPLICIT} before the dimension-specific setters run, and {@code getTileWidth} is reached only after + * {@code setTiling} has moved the param into {@code tilingSet}. No state refinement is violated. + */ +class JpegExporter { + + ImageWriteParam buildJpegParam() { + ImageWriteParam param = new ImageWriteParam(Locale.getDefault()); + param.setTilingMode(ImageWriteParam.MODE_EXPLICIT); + param.setTiling(10, 30, 10, 30); + param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT); + param.setCompressionQuality(0.85f); + return param; + } + + int firstTileWidth() { + ImageWriteParam param = new ImageWriteParam(Locale.getDefault()); + param.setTilingMode(ImageWriteParam.MODE_EXPLICIT); + param.setTiling(8, 8, 0, 0); + return param.getTileWidth(); + } +} diff --git a/liquidjava-example/src/main/java/testSuite/classes/imagewrite_error/ImageWriteParamsRefinements.java b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_error/ImageWriteParamsRefinements.java new file mode 100644 index 00000000..23f3055b --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_error/ImageWriteParamsRefinements.java @@ -0,0 +1,88 @@ +package testSuite.classes.imagewrite_error; + +import java.util.Locale; + +import javax.imageio.ImageWriteParam; + +import liquidjava.specification.ExternalRefinementsFor; +import liquidjava.specification.Refinement; +import liquidjava.specification.StateRefinement; +import liquidjava.specification.StateSet; + +/** + * External typestate specification for {@code javax.imageio.ImageWriteParam}. + * + *
+ * The class is modelled as two independent ghost-state dimensions — tiling and compression — so a configuration error + * in one dimension does not mask the other. The conditional {@code setTilingMode} / {@code setCompressionMode} + * transitions only reach the {@code *Explicit} state when called with {@code MODE_EXPLICIT}; any other mode leaves the + * param in its {@code start*} state, which the dimension-specific setters reject. + */ +@StateSet({ "startTiling", "tilingExplicit", "tilingSet" }) +@StateSet({ "startCompression", "compressionExplicit", "compressionSet" }) +@ExternalRefinementsFor("javax.imageio.ImageWriteParam") +public interface ImageWriteParamsRefinements { + + // Constructor + @StateRefinement(to = "startTiling(this) && startCompression(this)") + void ImageWriteParam(Locale locale); + + // Tiling related methods + + @StateRefinement(to = "(mode == ImageWriteParam.MODE_EXPLICIT)? tilingExplicit(this) : startTiling(this)") + void setTilingMode(int mode); + + @StateRefinement(from = "tilingExplicit(this)", to = "tilingSet(this)") + @StateRefinement(from = "tilingSet(this)", to = "tilingSet(this)") + void setTiling(@Refinement("_ > 0") int tileWidth, @Refinement("_ > 0") int tileHeight, int tileGridXOffset, + int tileGridYOffset); + + @StateRefinement(from = "tilingSet(this)") + int getTileGridXOffset(); + + @StateRefinement(from = "tilingSet(this)") + int getTileGridYOffset(); + + @StateRefinement(from = "tilingSet(this)") + int getTileHeight(); + + @StateRefinement(from = "tilingSet(this)") + int getTileWidth(); + + @StateRefinement(from = "tilingExplicit(this)") + @StateRefinement(from = "tilingSet(this)", to = "tilingExplicit(this)") + void unsetTiling(); + + void setProgressiveMode(@Refinement("ImageWriteParam.MODE_DISABLED == mode || mode == ImageWriteParam.MODE_DEFAULT || mode == ImageWriteParam.MODE_COPY_FROM_METADATA") int mode); + + // Compression related methods + + @StateRefinement(to = "mode == ImageWriteParam.MODE_EXPLICIT? compressionExplicit(this) : startCompression(this)") + void setCompressionMode(int mode); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + void setCompressionQuality(@Refinement("_ >= 0.0 && _ <= 1.0") float quality); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + String getCompressionType(); + + @StateRefinement(from = "compressionExplicit(this)", to = "compressionSet(this)") + void setCompressionType(String compressionType); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)", to = "compressionExplicit(this)") + void unsetCompression(); + + @StateRefinement(from = "compressionSet(this)") + String getLocalizedCompressionTypeName(); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + boolean isCompressionLossless(); + + @StateRefinement(from = "compressionExplicit(this)") + @StateRefinement(from = "compressionSet(this)") + float getCompressionQuality(); +} diff --git a/liquidjava-example/src/main/java/testSuite/classes/imagewrite_error/JpegExporter.java b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_error/JpegExporter.java new file mode 100644 index 00000000..c5dae5e3 --- /dev/null +++ b/liquidjava-example/src/main/java/testSuite/classes/imagewrite_error/JpegExporter.java @@ -0,0 +1,29 @@ +package testSuite.classes.imagewrite_error; + +import java.util.Locale; + +import javax.imageio.ImageWriteParam; + +/** + * A JPEG export pipeline configured against {@link ImageWriteParamsRefinements}. + * + *
+ * The author did configure a tiling mode — but passed {@code MODE_DEFAULT} instead of {@code MODE_EXPLICIT}. The spec's + * conditional transition leaves the param in {@code startTiling} for any non-explicit mode, so {@code setTiling} + * (which requires {@code tilingExplicit} or {@code tilingSet}) violates its from-state. + * + *
+ * The found-state threads the same {@code param} across SSA versions joined by internal {@code stateN(x) == stateN(y)}
+ * equalities; state derivation rewrites those into developer-facing typestate names for the diagnostic.
+ */
+class JpegExporter {
+
+ ImageWriteParam buildJpegParam() {
+ ImageWriteParam param = new ImageWriteParam(Locale.getDefault());
+ param.setTilingMode(ImageWriteParam.MODE_DEFAULT);
+ param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+ param.setCompressionQuality(0.85f);
+ param.setTiling(10, 30, 10, 30); // State Refinement Error
+ return param;
+ }
+}
diff --git a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java
index d01b03e5..f3f23460 100644
--- a/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java
+++ b/liquidjava-verifier/src/main/java/liquidjava/processor/refinement_checker/VCChecker.java
@@ -391,6 +391,9 @@ protected void throwStateRefinementError(SourcePosition position, Predicate foun
gatherVariables(found, lrv, mainVars);
TranslationTable map = new TranslationTable();
VCImplication foundState = joinPredicates(found, mainVars, lrv, map);
+ // simplify(context) folds the found-state predicate and, in the same pass, rewrites internal
+ // ghost-state equalities into developer-facing state predicates (see ExpressionSimplifier /
+ // StateDerivation). The resulting ValDerivationNode keeps the provenance of that rewrite.
throw new StateRefinementError(position, expected.simplify(context),
foundState.toConjunctions().simplify(context), map, customMessage);
}
diff --git a/liquidjava-verifier/src/main/java/liquidjava/rj_language/Predicate.java b/liquidjava-verifier/src/main/java/liquidjava/rj_language/Predicate.java
index 64da35b2..095dcded 100644
--- a/liquidjava-verifier/src/main/java/liquidjava/rj_language/Predicate.java
+++ b/liquidjava-verifier/src/main/java/liquidjava/rj_language/Predicate.java
@@ -257,8 +257,9 @@ public ValDerivationNode simplify(Context context) {
for (AliasWrapper aw : context.getAliases()) {
aliases.put(aw.getName(), aw.createAliasDTO());
}
- // simplify expression
- ValDerivationNode result = ExpressionSimplifier.simplify(exp.clone(), aliases);
+ // simplify expression — ghost states let the simplifier rewrite internal state equalities into
+ // developer-facing state predicates for error messages
+ ValDerivationNode result = ExpressionSimplifier.simplify(exp.clone(), aliases, context.getGhostStates());
DebugLog.simplification(this.getExpression(), result.getValue());
return result;
}
diff --git a/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/ExpressionSimplifier.java b/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/ExpressionSimplifier.java
index e94d2574..be4da893 100644
--- a/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/ExpressionSimplifier.java
+++ b/liquidjava-verifier/src/main/java/liquidjava/rj_language/opt/ExpressionSimplifier.java
@@ -2,7 +2,9 @@
import liquidjava.diagnostics.DebugLog;
import liquidjava.processor.context.Context;
+import liquidjava.processor.context.GhostState;
import liquidjava.rj_language.Predicate;
+import java.util.List;
import java.util.Map;
import liquidjava.processor.facade.AliasDTO;
@@ -23,9 +25,10 @@ public class ExpressionSimplifier {
* Simplifies an expression by applying constant propagation, constant folding, removing redundant conjuncts and
* expanding aliases Returns a derivation node representing the tree of simplifications applied
*/
- public static ValDerivationNode simplify(Expression exp, Map
+ * Typestate found-state conjunctions mix developer state invocations ({@code startTiling(p)}) with internal ghost-state
+ * equalities ({@code state1(a) == state1(b)}). The equalities are an SMT artifact, meaningless to the developer. When a
+ * known developer state holds for one side of such an equality, the same state must hold for the other side; this pass
+ * derives that state predicate and drops the equality.
+ *
+ *
+ * It is a single pass: it only consults the conjuncts it is given. Chain resolution
+ * ({@code state1(a)==state1(b) && state1(b)==state1(c)}) falls out of the surrounding {@code simplifyToFixedPoint} loop
+ * re-running the pass until stable.
+ *
+ *
+ * Derivation only matches plain {@link FunctionInvocation} conjuncts and plain {@code stateN(x) == stateN(y)}
+ * equalities. It deliberately does not traverse into {@code Ite} / disjunction / negation — a conditional branch
+ * is not an asserted fact, so deriving from it would be unsound.
+ */
+public class StateDerivation {
+
+ /**
+ * Rewrites ghost-state equalities in {@code node} into derived developer-state conjuncts. Returns {@code node}
+ * unchanged when nothing can be derived, so the enclosing fixed-point loop terminates.
+ */
+ public static ValDerivationNode derive(ValDerivationNode node, List
+ * Substitutions of the form {@code stateN(x) -> stateN(y)} (both sides invocations of a ghost-state function named
+ * in {@code protectedStateFunctions}) are dropped before propagation. Collapsing them would erase the shared middle
+ * term of an equality chain ({@code state1(a)==state1(b) && state1(b)==state1(c)}), which state-equality derivation
+ * needs whole. These equalities are an SMT artifact consumed only by error-message derivation.
+ */
+ public static ValDerivationNode propagate(Expression exp, ValDerivationNode previousOrigin,
+ Set
+ * When a found-state conjunction holds a known developer state for one object ({@code knownState(a)}) together with an
+ * internal ghost-state equality ({@code state1(a) == state1(b)}), the same state must hold for the other object. The
+ * derivation pass rewrites this into {@code knownState(b)} and attaches a {@code StateDerivationNode} so the step is
+ * explainable: {@code knownState(b)} was derived through {@link #getSourceEquality()} starting from
+ * {@link #getSourceState()}.
+ */
+public class StateDerivationNode extends DerivationNode {
+
+ private final ValDerivationNode sourceEquality;
+ private final ValDerivationNode sourceState;
+
+ public StateDerivationNode(ValDerivationNode sourceEquality, ValDerivationNode sourceState) {
+ this.sourceEquality = sourceEquality;
+ this.sourceState = sourceState;
+ }
+
+ /** The ghost-state equality ({@code stateN(x) == stateN(y)}) the derivation went through. */
+ public ValDerivationNode getSourceEquality() {
+ return sourceEquality;
+ }
+
+ /** The known developer state the derivation started from. */
+ public ValDerivationNode getSourceState() {
+ return sourceState;
+ }
+}
diff --git a/liquidjava-verifier/src/test/java/liquidjava/api/tests/DerivedStateErrorMessageTest.java b/liquidjava-verifier/src/test/java/liquidjava/api/tests/DerivedStateErrorMessageTest.java
new file mode 100644
index 00000000..7a321caf
--- /dev/null
+++ b/liquidjava-verifier/src/test/java/liquidjava/api/tests/DerivedStateErrorMessageTest.java
@@ -0,0 +1,48 @@
+package liquidjava.api.tests;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.junit.jupiter.api.Test;
+
+import liquidjava.api.CommandLineLauncher;
+import liquidjava.diagnostics.Diagnostics;
+import liquidjava.diagnostics.errors.StateRefinementError;
+
+/**
+ * End-to-end check that {@link StateRefinementError} diagnostics present the found-state conjunction with
+ * developer-facing typestate names — not the internal {@code stateN(x) == stateN(y)} equalities the SMT layer threads
+ * across call boundaries.
+ *
+ *
+ * {@code TestExamples} only matches an error's title and line, never its message body, so the state-equality derivation
+ * rewrite needs a dedicated assertion on the rendered text. The {@code imagewrite_error} scenario is a two-dimension
+ * external-typestate spec whose found-state relates the same {@code param} across several SSA versions — exactly the
+ * shape that produces those equalities.
+ */
+class DerivedStateErrorMessageTest {
+
+ private static final String IMAGEWRITE_ERROR = "../liquidjava-example/src/main/java/testSuite/classes/imagewrite_error";
+
+ @Test
+ void stateRefinementErrorShowsDeveloperStatesNotInternalEqualities() {
+ CommandLineLauncher.launch(IMAGEWRITE_ERROR);
+
+ StateRefinementError error = Diagnostics.getInstance().getErrors().stream()
+ .filter(StateRefinementError.class::isInstance).map(StateRefinementError.class::cast).findFirst()
+ .orElseThrow(() -> new AssertionError("expected a StateRefinementError from imagewrite_error"));
+
+ // the rendered, developer-visible message string ("Expected state ... but found ...")
+ String message = error.getMessage();
+
+ // derivation rewrote every cross-version ghost-state equality into a named typestate
+ assertTrue(message.contains("startTiling("), "message should name the tiling typestate, got: " + message);
+ assertTrue(message.contains("compressionExplicit("),
+ "message should name the compression typestate, got: " + message);
+
+ // the internal ghost-state functions and their equalities must not leak into the diagnostic
+ assertFalse(message.contains("state1("), "internal state1(...) leaked into the message: " + message);
+ assertFalse(message.contains("state2("), "internal state2(...) leaked into the message: " + message);
+ assertFalse(message.contains("=="), "a raw ghost-state equality leaked into the message: " + message);
+ }
+}
diff --git a/liquidjava-verifier/src/test/java/liquidjava/rj_language/opt/DerivedStateEqualitiesTest.java b/liquidjava-verifier/src/test/java/liquidjava/rj_language/opt/DerivedStateEqualitiesTest.java
new file mode 100644
index 00000000..b27f2f47
--- /dev/null
+++ b/liquidjava-verifier/src/test/java/liquidjava/rj_language/opt/DerivedStateEqualitiesTest.java
@@ -0,0 +1,183 @@
+package liquidjava.rj_language.opt;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.util.List;
+import java.util.Map;
+
+import org.junit.jupiter.api.Test;
+
+import liquidjava.processor.context.GhostState;
+import liquidjava.processor.facade.AliasDTO;
+import liquidjava.rj_language.Predicate;
+import liquidjava.rj_language.ast.Expression;
+import liquidjava.rj_language.opt.derivation_node.BinaryDerivationNode;
+import liquidjava.rj_language.opt.derivation_node.StateDerivationNode;
+import liquidjava.rj_language.opt.derivation_node.ValDerivationNode;
+import liquidjava.rj_language.parsing.RefinementsParser;
+
+/**
+ * Test suite for state-equality derivation inside the expression simplifier.
+ *
+ * Derivation rewrites internal ghost-state equalities (e.g. {@code state1(a) == state1(b)}) into developer-facing state
+ * predicates (e.g. {@code knownState(b)}) so {@code StateRefinementError} diagnostics show meaningful typestate names.
+ */
+class DerivedStateEqualitiesTest {
+
+ private static Expression parse(String sut) {
+ return RefinementsParser.createAST(sut, "");
+ }
+
+ /**
+ * Builds a developer state named {@code stateName} whose refinement is backed by the internal ghost-state function
+ * {@code internalName} (the {@code stateN} dimension).
+ */
+ private static GhostState ghostState(String stateName, String internalName) {
+ GhostState gs = new GhostState(stateName, null, null, "", "");
+ gs.setRefinement(new Predicate(parse(internalName + "(wild) == 0")));
+ return gs;
+ }
+
+ @Test
+ void testBasicDerivationDerivesKnownStateForOtherOperand() {
+ Expression expression = parse("state1(a) == state1(b) && knownState(a)");
+ List