GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Keep the values expected depends on in the counterexample by CatarinaGamboa · Pull Request #460 · liquid-java/liquidjava Conversation
|
| Fixture | main | this PR |
|---|---|---|
UnconstrainedArgument (issue reproducer) |
port == 0 |
timeout == -1 |
CombinedGuard (if (port<0 || … || window<1 || timeout>60000) return;) |
port == 0 && retries == 0 && backoff == 0 && window == 1 |
timeout == -1 |
MixedGuard (port<0 || (retries>3 && backoff>10) || timeout>60000) |
port == 0 && retries == 0 |
timeout == -1 |
GuardRelatesArguments (retries > timeout + 5) |
port == 0 && retries == 0 |
timeout == -1 && retries == 0 |
NothingKnown (Found is true) |
(none) | millis == -1 |
GuardedArgument |
port == 0 |
timeout == -1 |
AliasedArgument |
configured, port, timeout¹¹ |
timeout¹¹ == -1 && configured == -1 |
FieldArgument |
this#…timeout, port, timeout⁶ |
timeout⁶ == -1 && this#…timeout == -1 |
CallArgument (connect(read(key) - 1)) |
#fresh⁶, port, timeout⁷ |
timeout⁷ == -1 && #fresh⁶ == 0 |
BranchArgument (nested if/else) |
secure == false && #fresh⁰ == false && port == 1025 |
timeout == -1 |
SeveralArguments |
port == 0 && lo == 1 |
hi == 0 && lo == 1 |
KnownArgument |
port == 0 |
capacity == 0 |
LinkedPathCondition (connect(port - 1)) |
port, retries, timeout¹⁰ |
timeout¹⁰ == -1 && port == 0 |
ReturnPath |
configured, port, #ret⁵ |
#ret⁵ == -1 && configured == -1 |
ReturnConstant (return 0 under port < 0) |
port == -1 |
(none): Found fixes #ret == 0 |
ReturnFromPath (return port + 1 under port < 0) |
port == -1 && #ret² == 0 |
#ret² == 0 && port == -1 |
KnownGuardedArgument (int value = 0; if (port > 0) take(value);) |
port == 1 |
(none) |
KnownTwiceGuardedArgument (same, under two guards) |
port == 1 && retries == 0 |
(none) |
KnownAliasedArgument (int base = 0; int value = base;) |
port == 1 |
(none) |
RefinementAlias |
port == 0 |
timeout == -1 |
GhostArgument |
port == 0 |
size(bag) == 0 |
KnownGhost (Found states size(s⁹) == 1) |
port == 0 |
(none) |
KnownEnum (format == Format.JPG) |
(none) | (none); format == JPG stays hidden |
ArrayArgument (get(a, 3)) |
port == 0 |
length(a) == 0 |
ArrayIndex (int[] a = new int[4]; get(a, k)) |
port == 0 && a⁵ == ((as const (Array Int Int)) 4) |
k == -1 |
15 unit tests in RefinementErrorCounterexampleTest. Each builds a RefinementError from a VC chain and a hand-written model. They cover:
VariableFormatterTest also covers nested applications.
The review fixtures fail on the commit before them: CombinedGuard, MixedGuard, GuardRelatesArguments, KnownEnum, ArrayArgument and ArrayIndex on 491d297e; KnownGuardedArgument, KnownTwiceGuardedArgument and KnownAliasedArgument on 9e037b6b, which showed port == 1. So do their unit tests. Four tests guard behaviour that should not change and pass on both: ReturnFromPath, keepsAPathConditionTheExpectedValueIsComputedFrom, hasNoCounterexampleWhenFoundStatesEveryValue and keepsGhostValuesWithSeveralArgumentsFoundSaysNothingAbout.
mvn test (verifier): 599 tests, 0 failures, 0 errors (534 on main).
Issue reproducer:
Refinement Error: !(port < 0 || port > 65535) is not a subtype of timeout >= 0
- --> Counterexample: port == 0
+ --> Counterexample: timeout == -1
OpenNMS study exercise (E_OSS_A2_04_VmwareCimProbe):
- --> Counterexample: #fresh³¹⁶ == false && port³¹³ == 0
+ --> Counterexample: timeout == -1
Combined guard:
Refinement Error: !(port < 0 || port > 65535 || retries < 0 || backoff < 0 || window < 1 || timeout > 60000) is not a subtype of timeout >= 0
- --> Counterexample: port == 0 && retries == 0 && backoff == 0 && window == 1
+ --> Counterexample: timeout == -1
Array index:
Refinement Error: !(port < 0) && length(a⁵) == 4 is not a subtype of k >= 0 && k < length(a⁵)
- --> Counterexample: port == 0 && a⁵ == ((as const (Array Int Int)) 4)
+ --> Counterexample: k == -1
Ghost state:
Refinement Error: !(port < 0) is not a subtype of size(bag) > 0
- --> Counterexample: port == 0
+ --> Counterexample: size(bag) == 0
Known enum value (ErrorEnumFunctionRefinement):
Refinement Error: newColor¹¹ == Blue is not a subtype of newColor¹¹ == Color.Red || newColor¹¹ == Color.Green
- --> Counterexample: newColor¹¹ == Blue
+ (no counterexample: Found already states it)
ErrorEnumNull (Color c = null) now shows c³ == Blue. Found is true because the verifier does not model null for enums; the assignment is lost before the error is built. Blue is a real counterexample to what Found states, but not the value the program has. Hiding values whenever Found is true would also hide millis == -1 in the issue's own case. The fix belongs in how null enum assignments are translated, which is a separate issue.@Refinement("open(f)")): RefinementError receives Expected before states are expanded, while the model names the state ghost state1(#f_6). Here Found already states that ghost (closed(f⁶) is state1 == 0), so nothing new would be shown even after expansion, and state1(f⁶) == 0 would be harder to read than the state name. With nothing new to show, there is no counterexample (main showed the unrelated port == 0). Showing values in state names needs the state mapping in the error, which this PR does not add.value == 0 would only restate Found, and it is the rule ErrorLiteralZero and ErrorStaticFinalConstant already pin. The cost is ten(port) with return 0; under port < 0 (ReturnConstant): main showed port == -1, which says which return failed. The VC cannot tell that case apart from take(value) under port > 0, where port == 1 misleads. In both, a guard is unlinked to an Expected term that Found fixes. When the return value is computed from the path (return port + 1;), the path value is linked and shown.size(old(this))) are not matched. The model names them by Z3's rendering of the argument.!(x != v) is not rewritten to x == v. Hiding that value would hide the breaking value itself (t == -3 against t >= 0).static class makes Z3 fail with Sort mismatch … supplied sort is …$Bag. This is an unrelated, existing bug, so the ghost fixtures use top-level classes.Fixes #453
🤖 Generated with Claude Code
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
[Codex review] The fallback still produces a counterexample that points at the wrong value when Found already states the value that violates Expected. Reproducer: |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
[Claude] Fixed in 712759f: the fallback is gone. The counterexample now keeps only the values linked to Expected that Found does not already state. So when Found fixes every value Expected depends on, there is no counterexample. Your reproducer now shows none instead of Covered by:
The trade-off for |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
[Codex re-review] Resolved. The fallback is removed, and the new tests cover the guarded argument case, aliases, multiple guards, and return values computed from path conditions. The PR’s CI is green. My |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
The counterexample kept only the binders of the simplified found, so a variable found knows nothing about (simplified to true) lost its value, while unrelated path conditions and unmentioned path binders stayed. It now keeps the variables and ghost applications of the expected refinement plus those of the found conjuncts linked to them, still hiding values found already states. Terms only expected mentions come first, generated names last, the rest in the order of the VC binders. Co-Authored-By: Claude Opus 5.5 <[email protected]>
… read Review follow-ups for the counterexample filter: - Negated disjunctions and double negations are split into separate facts before linking, so one combined guard no longer links every variable in it to expected. - The terms of expected come before the terms linked to them. - Known values are compared structurally, so enum constants (Format.JPG against the model's JPG) and ghost applications with several arguments are recognised. - Array values are left out of the model; their ghosts (length(a)) remain. - When every linked value is known, the counterexample falls back to the other terms found shows, such as the path condition of a constant return. - The fresh name prefix lives in Formats, and ghost application names are formatted with a nesting-aware scanner. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The fallback to the other terms found shows could point at an unrelated path condition (port == 1 for take(value) with value == 0 already in found). Only terms linked to expected are now kept, so when found states all of them the counterexample is empty, as for a literal argument. Co-Authored-By: Claude Opus 5.5 <[email protected]>
712759f to
b9d7e9b
Compare
There was an error while loading. Please reload this page.
…#459) ## Problem When a method reads a field, Found shows a row for it under its internal name, and the row says only `true`. Issue reproducer (`ArrayList.size()` refined `_ == size(this) && _ >= 0`, `Random.nextInt(bound)` refined `bound > 0`): ``` ∀this#shop.Basket.items true → ∀bound⁶ bound⁶ == size(items) && bound⁶ >= 0 ``` In the IDE the first row wraps over several lines, above the row that matters. The message and the later row also write the field as `items`, so it reads like the local it was copied into.  ## Cause 1. `VCBinderSimplification` dropped a `true` binder only when no later row mentioned its variable. Here `size(this#shop.Basket.items)` mentions it. Every field, parameter, call result or local that is only passed along keeps its `true` row this way. The `∀getUndoManager⁸⁵⁶ true` and `∀#fresh… true` rows of #375 come from the same rule. 2. `VariableFormatter.format` left `this#<package>.<Class>.<field>` unchanged. The binder, the counterexample and the extension's context and translation-table keys therefore showed the internal name, while `ExpressionNameResolver` cut it down to the bare `items` inside expressions. ## Fix ### Which `true` rows stay A binder refined only by `true` states no fact. It stays in Found only when the error needs it: - **Linked to Expected.** Some fact of the chain constrains the binder's *value* and is linked to Expected, meaning it shares a term with Expected directly or through other linked facts. - Terms are variables and ghost or state applications such as `size(x)`. - Facts are the rows' conjuncts. A negated disjunction is split first: `!(p || q)` becomes `!p` and `!q`, so one combined guard does not link everything. - A variable of an **object** type that a fact names only as a ghost argument (`x` in `b == size(x)`) is not constrained by that fact. Its row would say nothing, so it goes. A primitive argument (`k` in `y == f(k)`) has a value of its own and is kept. - **Expected constrains its value, in a refinement error only.** The counterexample then shows that value; `filterCounterexample` lists only binders. A state error has no counterexample, so there this alone keeps nothing. The rule depends only on the chain and Expected, not on Z3's model, and it works the same way for both error kinds. A kept `true` binder that comes after a row using its variable is moved to just before that row. This is a separate `Binder` step. Examples: | Case | Rows (after) | |---|---| | issue reproducer | `∀bound⁶ bound⁶ == size(this.items) && bound⁶ >= 0` (field only inside `size(...)`) | | `if (port < 0) return; positive(t);` | `∀#fresh !(port < 0)`, `∀t true`: `port` dropped (unlinked guard), `t` kept (Expected, counterexample `t == 0`) | | `if (p \|\| q) return; positive(t);` | `∀#fresh !(p \|\| q)`, `∀t true`: `p` and `q` dropped | | `int y = x + 1` refined `_ > 0` | `∀x true`, `∀y y == x + 1`: `x` kept, linked through `y` | | `ErrorStateHintPathDependent` (state) | `∀ready true`, `∀f ready ? open(f) : created(f)`: `ready` kept | | state errors whose Found is otherwise `true` | unchanged from `main`: no `∀it true` / `∀this true` row is added | | `for (int i = start; i < served; i++) positive(i);` | `∀Basket.served true`, `∀i true`, `∀#fresh i < Basket.served`: `i` now before its first use | The linking is `VCSimplificationUtils.linkedConjuncts(conjuncts, termsOf, seed)`. It does what `linkedTerms` in `RefinementError` does on #460 (branch `fix/counterexample-expected-vars`, issue #453): the same closure over shared terms, with the term function passed in. Once both are merged, `linkedTerms` can call this helper with its `termsOf`, and `splitConjuncts` can replace `getConjuncts` there, so the counterexample and the rows are linked in the same way. I didn't touch `filterCounterexample`. ### Simplification history (the VS Code arrows) Dropping a `true` row is an ordinary simplification step of the `Binder` pass, one binder per step. It goes into `VCSimplificationResult`'s origin chain, which the extension's server sends whole (`VCSimplificationResultDTO.origin`). The panel steps back through that chain, and its last step is the untouched `Original` with every binder. What the panel shows for the issue reproducer: | Step | `main` (2 steps) | this branch (3 steps) | |---|---|---| | 1 Original | `∀this#shop.Basket.items true`<br>`∀items⁵ items⁵ == this#shop.Basket.items`<br>`∀bound⁶ bound⁶ == size(items⁵) && bound⁶ >= 0` | `∀this.items true`<br>`∀items⁵ items⁵ == this.items`<br>`∀bound⁶ bound⁶ == size(items⁵) && bound⁶ >= 0` | | 2 after Substitution | (Simplified) `∀this#shop.Basket.items true`<br>`∀bound⁶ bound⁶ == size(items) && bound⁶ >= 0` | `∀this.items true`<br>`∀bound⁶ bound⁶ == size(this.items) && bound⁶ >= 0` | | 3 after Binder | | (Simplified) `∀bound⁶ bound⁶ == size(this.items) && bound⁶ >= 0` | The field display name is applied at every step, including Original, because the server formats every step with `VariableFormatter.format`. The internal names are untouched in the history itself. `TestFoundBinders.steppingBackReachesTheOriginalWithTheDroppedRow` checks that the original's binders are `this#testSuite.classes.found_true_binders_error.Basket.items`, `#items_…`, `#bound_…`. Showing the Original raw would be a one-line change in `VCImplicationDTO` (skip `format` for the oldest step), but I left that for the PM to decide. ### Source-like field names `this#pkg.Class.field` is written `this.field` in the message, the Found rows, the counterexample and the extension's context and translation table. This holds for both `this.x` and bare `x`. A static field is written `Class.field`. Instance counters stay (`this.interrupts¹¹³`), nested classes and long package names reduce the same way, and the **internal SMT name is unchanged**. Static-ness is not in the name, so `Utils.qualifyFieldName` records static fields as it builds their names, and `CommandLineLauncher.launch` clears the record at the start of each run. Every error comes from code verified in that run, which also built the names of the fields it shows, so the record is complete for that run's errors. Verifying a static `total`, then an instance `total` of the same class, then the static one again in one JVM shows `C.total`, `this.total`, `C.total`. There is no incremental verification that skips classes. If one is added, the record would need to be filled from the field declarations instead. I removed the earlier class-based disambiguation (`Basket.this.count`). Real code could not reach it, it was not valid Java for inherited fields, and it was not applied to binder rows or translation-table keys. Two different fields with the same simple name in one expression (a shadowed field) now both read `this.x`. ### Writes to another object's field (display only) This PR does not change what the verifier accepts or rejects for any write. A write to another object's field (`other.level = …`, `other.level++`, `other.level--`, `other.level += …`) is still checked and tracked exactly as on `main`, against the current object's field variable. Only its label changes. When `TypeChecker.checkVariableRefinements` creates the new instance for a write that `Utils.accessesTrackedField` says is not to the current object, it records the receiver's variable name for that instance in `VariableFormatter` (a bare field name when the receiver is not a variable). The instance is then written `other.level¹` instead of `this.level¹`: ``` main: level¹ == -1 is not a subtype of level¹ >= 0 now: other.level¹ == -1 is not a subtype of other.level¹ >= 0 ``` Because tracking is unchanged, a decrement shows what the verifier actually uses as the old value: `other.level⁹ == this.level⁸ - 1`. Writes through another object leak into `this.level`, and aliasing with `this` is not handled. Both are left for their own issue. Parity with `main`, checked on the reviewers' reproducers and `ErrorOtherObjectFieldWrite.java`: - `other.level++`, `other.level += 1` and `other.level = other.level + 1` verify. - `other.level--` and `other.level = -1` are reported. - The write through `W other = this` and `maybeSelf(this)` are reported. - The `other.level = 5; int k = this.level` (`_ == 5`) case verifies as on `main`. ### #375 This covers part of #375, not all of it. The `true` rows go, and in the JabRef `EditAction` exercise all four are removed. The path conditions over now-undeclared fresh variables (`∀#fresh¹²² !#fresh¹²¹`) are not `true` rows and stay; they also tie in with #373. #375 stays open. ## Before / after (main vs this branch, every error) I ran the harness that dumps what the IDE gets (message, rows, counterexample, translation-table keys) over 321 test-suite errors and 117 errors from the 98 study exercises, with `main` and this branch on identical inputs: - The same errors are reported at the same lines on both sides (the harness requires it), so nothing that `main` accepts or rejects changes. - 58 test-suite errors and 57 exercise errors differ in what they show. - The differences are renames (`this#…` to `this.x` or `Class.x`), dropped `true` rows, the `Binder` reordering, and in refinement errors a kept `true` row for an Expected variable. In those refinement errors `main` showed only `true`; now the variable's counterexample value also shows, e.g. `port == 65536`. - `true` rows: 110 → 87 (test suite), 78 → 46 (exercises). Raw `this#` names: 30 → 0 and 72 → 0. - No state error gains a `true` row. - **Instance numbers do not move**: 0 of the 438 messages change their superscripts. I could not reproduce the `transactionCommitted²⁷→²⁸` shift (A1_50 shows `²⁸` on both). I think it came from different inputs between the two runs. **Several fields** (`pickAny`) ``` before: ∀this#testSuite.classes.found_true_binders_error.Basket.items³⁷ true ∀first³⁸ first³⁸ == size(items³⁷) && first³⁸ >= 0 ∀this#testSuite.classes.found_true_binders_error.Basket.extras⁴¹ true ∀second⁴² second⁴² == size(extras⁴¹) && second⁴² >= 0 ∀bound⁴⁵ bound⁴⁵ == first³⁸ + second⁴² after: ∀first³⁸ first³⁸ == size(this.items³⁷) && first³⁸ >= 0 ∀second⁴² second⁴² == size(this.extras⁴¹) && second⁴² >= 0 ∀bound⁴⁵ bound⁴⁵ == first³⁸ + second⁴² ``` **Static field, linked to Expected** (`serve`) ``` before: count⁵⁴ == served + 1 is not a subtype of count⁵⁴ > 0 ∀this#testSuite.classes.found_true_binders_error.Basket.served true ∀count⁵⁴ count⁵⁴ == served + 1 after: count⁵⁴ == Basket.served + 1 is not a subtype of count⁵⁴ > 0 ∀Basket.served true ∀count⁵⁴ count⁵⁴ == Basket.served + 1 Counterexample: Basket.served == -1 && count⁵⁴ == 0 ``` **Field holding a state kept, unrelated `true` parameter dropped** (`classes/found_field_state_error`, after Jpcsp's `IntrManager`) ``` before: ∀intrNumber true ∀#fresh¹⁶ !(intrNumber < 0 || intrNumber >= 67) ∀this#testSuite.classes.found_field_state_error.InterruptManager.interrupts¹³ empty(interrupts¹³) after: ∀#fresh¹⁶ !(intrNumber < 0 || intrNumber >= 67) ∀this.interrupts¹³ empty(this.interrupts¹³) ``` ## Tests - `TestFoundBinders` (19 tests) on the fixtures `classes/found_true_binders_error`, `classes/found_field_state_error`, `ErrorStateHintPathDependent.java` and the new `ErrorOtherObjectFieldWrite.java`: - dropped: the reproducer's field row; a `true` parameter, call result and local; several fields; `port` behind an unrelated guard; `p`/`q` behind one combined guard. - kept: `x` linked through `y == x + 1`; an Expected variable in a refinement error; `ready` in the path-dependent state error; the field row holding `empty(...)`. - row order for a loop variable. - names: static `Basket.served`; `this.amount` and bare `amount` written alike; an inner-class field; `other.level` for writes and decrements on another object, and `this.level` for an own write. - parity with `main` for writes: `other.level++` and `other.level += 1` verify; `other.level--` and the write through an alias of `this` are reported. - the original the panel steps back to keeps the dropped row and the raw binder names, and the history includes a `Binder` step. - `VCBinderSimplificationTest` (13 new, including a primitive ghost argument that is kept): - linked through one or several rows; - an unlinked guard; - a split negated disjunction; - an Expected binder in refinement vs state errors; - linking through a ghost while keeping only value variables; - an object named only inside a ghost; - moving a kept binder before its first use; - the `NeededVariables` factories. - `VCSimplificationUtilsTest` (4): splitting conjuncts and negated disjunctions; transitive linking. - `VariableFormatterTest` (6 new) and `ExpressionNameResolverTest` (2 new): instance, static, nested, long-package and other-object field names; clearing the static record between runs; a field next to a local of the same name; a field inside a ghost argument. Without the fix (`main` sources, the new tests kept), `TestFoundBinders` fails 15 of 19 and `ExpressionNameResolverTest` fails 2 of 5. The unit tests on the new API do not compile on `main`. `mvn test` (verifier): **583 tests, 0 failures** (`main`: 534). **Changed existing expectations** - `VCBinderSimplificationTest.keepsTrueBinderWhenVariableIsUsedDownstream` became `removesTrueBinderUsedDownstreamWhenNothingNeedsIt`: with nothing to link to, the binder goes. That is the rule change itself. - `VCBinderSimplificationTest.skipsInapplicableTrueBinderAndSimplifiesLaterBinder` now makes `x` an Expected variable, so its binder is still the inapplicable one the test is about. The expected steps are unchanged. - `VCTestUtils.parseImplication` splits a binder at the first `.` after the `:`, so binder names can contain dots. No `// Expect:` fixture, `TestCounterexamples` or `TestErrorCategories` expectation changed. ## Not in this PR - Raw names in the Original step (above). - `random.nextInt(items.size() + extras.size())` fails on `main` with `Sort mismatch … supplied sort is …Basket`, a separate translation bug. The fixture uses two locals instead. - The client's hover (`getOriginalVariableName`) strips only `this#`, so it does not match fields, before or after this change. Stripping a leading `this.` there would be a small follow-up in vscode-liquidjava. Fixes #457 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 <[email protected]>
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
Counterexample leaves out the variable the expected refinement constrains, when Found knows nothing about it
There was an error while loading. Please reload this page.