Repository navigation
Keep the values expected depends on in the counterexample - #460
Conversation
|
[Codex review] The fallback still produces a counterexample that points at the wrong value when Found already states the value that violates Expected. Reproducer: |
|
[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 |
|
[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 |
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 <noreply@anthropic.com>
… 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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
712759f to
b9d7e9b
Compare
…#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 <noreply@anthropic.com>
Problem
When Found says nothing about the variable that Expected constrains, the counterexample leaves that variable out. It shows values for unrelated path conditions instead:
The value that breaks the call (
timeout == -1) is in the SMT model, but it gets filtered out. In the OpenNMS study exercise the panel shows#fresh³¹⁶ == false && port³¹³ == 0, which points the reader at the port guard.Cause
RefinementError.filterCounterexamplekept only assignments to the binders of the simplified Found. That rule is wrong in three ways:trueloses its binder during simplification, so its value goes too (timeout, and alsosize(bag)for ghosts).l == ((as const (Array Int Int)) 4)).Fix
The counterexample exists to show values that make Expected false under Found.
What is kept
size(bag),length(a)), named as the SMT model names them.!(a || b)gives!aand!b), and double negations are dropped. So one combined guard!(port < 0 || … || timeout > 60000)links onlytimeout > 60000totimeout, not every variable in the guard. A fact that shares no term with Expected only constrains values that cannot affect it, so it is left out.x == vfact is compared structurally. Enum constants match the model's value (Format.JPGin Found,JPGin the model), and so do ghost applications with several arguments.int value = 0; if (port > 0) take(value);against_ > 1shows no counterexample rather thanport == 1. This is the same rule that already gives literal arguments no counterexample (ErrorLiteralZero). A path condition is shown only when it is linked to Expected, e.g.return port + 1;underport < 0shows#ret == 0 && port == -1.TranslatorToZ3now skips array-sorted model values, as it already skipped uninterpreted sorts. Their ghosts (length(a)) remain.Order, most relevant first:
#freshnames last.Display.
VariableFormatter.formatformats ghost application arguments (size(#s_3)becomessize(s³)). It uses a scanner that only splits top-level commas, sof(g(#a_1, #b_2), #c_3)becomesf(g(a¹, b²), c³). The CLI uses this, and so does the VS Code server'sCounterexampleDTO. The fresh-name prefix now lives inFormats.FRESH_PREFIX, shared withVCBinderSimplification.Behaviour changes in existing outputs
Sweep of main against this PR over 149
Error*testSuite entries, 25ErrorCounterexample*fixtures and 97 study exercises (257 entries, main at19991d36): 70 outputs change.#ret) now first and#freshlast:ErrorAfterIf,ErrorAssignmentBeforeReturn,ErrorBitwiseUnknownOperand,ErrorBoolean,ErrorCharLiteralComparisons,ErrorDeferredCallbackState,ErrorDependentUpperBound,ErrorEmptyLoopBody,ErrorFieldOfOtherObject,ErrorIdentity,ErrorIntegerDivision,ErrorOperatorAssignments,ErrorRecursion,ErrorRecursiveDecrement,ErrorRecursiveSiblingParameter,ErrorUnknownMethodInOperationE_A2_03,E_A2_07,E_A2_08,E_A2_10,E_OSS_A2_05x == 1 && #ret² == 0becomes#ret² == 0 && x == 1. The 7 affectedTestCounterexamplesassertions are updated.#freshnow goes last:ErrorBitwiseUnknownOperand,ErrorFieldOfOtherObject,ErrorNullLiterals,ErrorUnknownMethodInOperationandErrorCounterexampleCallArgument. The first version of this PR body did not say so.ErrorUnconstrainedRefinement,ErrorUnknownArgument,ErrorUnresolvedOperationValue,ErrorEnumConstantInCondition,E_OSS_A1_05,E_OSS_A2_04ErrorLoopCondition,ErrorInstanceof,ErrorUnknownMethodInComparison,E_A2_02,E_OSS_A2_01, plus counterexamples made only of an opaque#freshvalue that Expected does not depend on:ErrorNullLiterals(two of its errors; the others only reorder),ErrorUnresolvedBinaryOperator,ErrorUnresolvedFieldDeclaringType,ErrorUnresolvedUnaryOperatorErrorEnumFunctionRefinement,ErrorEnumNegation,ErrorLocalShadowsConstant, and the first error ofErrorEnumConstantInConditionlength(…), or dropped when the length is known:ErrorSearchIntArray,ErrorImplementationSearchValueIntArray,ErrorSearchValueIntArray,ErrorStringArrayLength,ErrorLenZeroIntArrayErrorEnumNull, which is a known limitation (see below)Relation to #299 and #459
With witness: -1 >= 0 ✗(checked earlier with a trial merge). It conflicts textually with this PR:RefinementError, both add imports, and Show counterexample values in the final expected refinement #299 insertssubstituteWitnessright above the comment this PR rewrites. Keep both sides.TestCounterexamples, Show counterexample values in the final expected refinement #299 extendsdependentUpperBound…, which this PR renames and reorders. Keep Show counterexample values in the final expected refinement #299's extra assertions on the renamed test.VariableFormatter,VariableFormatterTestandVCBinderSimplification. The application formatting is one early branch plus private methods at the end of the class, with no new imports. ItsTestFoundBindershas 3 assertions that pin the old order (a == 0 && this.amount == 0,count == 0 && Basket.served == -1,y == 0 && x == -1). Whichever PR lands second flips those three to Expected-first; nothing else changes (checked with a trial merge).Tests
25 testSuite fixtures (
ErrorCounterexample*.java, each with// Expect: Refinement Error) plus matchingTestCounterexamplescases that assert the exact assignments:UnconstrainedArgument(issue reproducer)port == 0timeout == -1CombinedGuard(if (port<0 || … || window<1 || timeout>60000) return;)port == 0 && retries == 0 && backoff == 0 && window == 1timeout == -1MixedGuard(port<0 || (retries>3 && backoff>10) || timeout>60000)port == 0 && retries == 0timeout == -1GuardRelatesArguments(retries > timeout + 5)port == 0 && retries == 0timeout == -1 && retries == 0NothingKnown(Found istrue)millis == -1GuardedArgumentport == 0timeout == -1AliasedArgumentconfigured, port, timeout¹¹timeout¹¹ == -1 && configured == -1FieldArgumentthis#…timeout, port, timeout⁶timeout⁶ == -1 && this#…timeout == -1CallArgument(connect(read(key) - 1))#fresh⁶, port, timeout⁷timeout⁷ == -1 && #fresh⁶ == 0BranchArgument(nested if/else)secure == false && #fresh⁰ == false && port == 1025timeout == -1SeveralArgumentsport == 0 && lo == 1hi == 0 && lo == 1KnownArgumentport == 0capacity == 0LinkedPathCondition(connect(port - 1))port, retries, timeout¹⁰timeout¹⁰ == -1 && port == 0ReturnPathconfigured, port, #ret⁵#ret⁵ == -1 && configured == -1ReturnConstant(return 0underport < 0)port == -1#ret == 0ReturnFromPath(return port + 1underport < 0)port == -1 && #ret² == 0#ret² == 0 && port == -1KnownGuardedArgument(int value = 0; if (port > 0) take(value);)port == 1KnownTwiceGuardedArgument(same, under two guards)port == 1 && retries == 0KnownAliasedArgument(int base = 0; int value = base;)port == 1RefinementAliasport == 0timeout == -1GhostArgumentport == 0size(bag) == 0KnownGhost(Found statessize(s⁹) == 1)port == 0KnownEnum(format == Format.JPG)format == JPGstays hiddenArrayArgument(get(a, 3))port == 0length(a) == 0ArrayIndex(int[] a = new int[4]; get(a, k))port == 0 && a⁵ == ((as const (Array Int Int)) 4)k == -115 unit tests in
RefinementErrorCounterexampleTest. Each builds aRefinementErrorfrom a VC chain and a hand-written model. They cover:VariableFormatterTestalso covers nested applications.The review fixtures fail on the commit before them:
CombinedGuard,MixedGuard,GuardRelatesArguments,KnownEnum,ArrayArgumentandArrayIndexon491d297e;KnownGuardedArgument,KnownTwiceGuardedArgumentandKnownAliasedArgumenton9e037b6b, which showedport == 1. So do their unit tests. Four tests guard behaviour that should not change and pass on both:ReturnFromPath,keepsAPathConditionTheExpectedValueIsComputedFrom,hasNoCounterexampleWhenFoundStatesEveryValueandkeepsGhostValuesWithSeveralArgumentsFoundSaysNothingAbout.mvn test(verifier): 599 tests, 0 failures, 0 errors (534 on main).CLI before / after
Issue reproducer:
OpenNMS study exercise (
E_OSS_A2_04_VmwareCimProbe):Combined guard:
Array index:
Ghost state:
Known enum value (
ErrorEnumFunctionRefinement):Left out, and why
ErrorEnumNull(Color c = null) now showsc³ == Blue. Found istruebecause the verifier does not modelnullfor enums; the assignment is lost before the error is built.Blueis a real counterexample to what Found states, but not the value the program has. Hiding values whenever Found istruewould also hidemillis == -1in the issue's own case. The fix belongs in hownullenum assignments are translated, which is a separate issue.@Refinement("open(f)")):RefinementErrorreceives Expected before states are expanded, while the model names the state ghoststate1(#f_6). Here Found already states that ghost (closed(f⁶)isstate1 == 0), so nothing new would be shown even after expansion, andstate1(f⁶) == 0would be harder to read than the state name. With nothing new to show, there is no counterexample (main showed the unrelatedport == 0). Showing values in state names needs the state mapping in the error, which this PR does not add.value == 0would only restate Found, and it is the ruleErrorLiteralZeroandErrorStaticFinalConstantalready pin. The cost isten(port)withreturn 0;underport < 0(ReturnConstant): main showedport == -1, which says which return failed. The VC cannot tell that case apart fromtake(value)underport > 0, whereport == 1misleads. 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 tox == v. Hiding that value would hide the breaking value itself (t == -3againstt >= 0).static classmakes Z3 fail withSort 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