GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Join an if on the instances its condition read by CatarinaGamboa · Pull Request #408 · liquid-java/liquidjava

Conversation

@CatarinaGamboa

@CatarinaGamboa CatarinaGamboa commented Oct 20, 2026

Copy link
Copy Markdown
Collaborator

Problem

Two failures in how visitCtIf joins the branches, both from the guard it puts in the joined instances.

1. A reference comparison loses the then-branch state (missed error).

Door d = new Door();          // open
if (other == d) {
    d.close();
}
d.close();                    // passes, but d may already be closed

The guard was the condition with variable names and with their instances (other == d && other == #d_8). After the if, a variable's name stands for its latest instance, so d in the guard became the joined instance: the then-case required the joined d to be the same object as #d_8, which is open, so the closed branch was impossible and the error was missed.

2. A call result in a nested if's condition leaves scope (internal error).

Door d = new Door();
if (b) {
    if (current() == d) { ... }
}
d.close();   // Not Found Error: Variable '#fresh_11' could not be found

The fresh variable for current() lives in the outer then-block, but the inner join's guard (in d's joined instance) still uses it after that block ends.

Fix

  • The join uses a guard that names only the instances the condition read (lastExpRefs). The branch assumptions are unchanged.
  • The fresh values in that guard (and the fresh values their refinements use) are kept in the context as instance variables, so they outlive the block that created them.

With the fix, case 1 reports found open(d⁸) && (d⁸ == other ? closed(d¹³) : open(d¹³)) but expected open(d¹³).

Tests

  • CorrectNestedIfCallComparison: case 2, correct code. It failed before this change.
  • ErrorNestedIfCallComparison: case 2 with a close in the inner branch, then another close: reported.
  • ErrorIfReferenceComparison: case 1: reported. It passed before this change.
  • mvn test: 415 tests, 0 failures.

Found on a real fixed bug (a game engine's window switching display modes, which compares the full-screen window with its frame inside an outer if). We also checked 53 exercises built from real OSS bugs on main (e0a6c81) and with this change: the results are identical except for one exercise whose fix crashed with Variable 'length' could not be found and now verifies.

🤖 Generated with Claude Code

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@CatarinaGamboa @claude
Join an if on the instances its condition read
19d25af
The join's guard used the condition both with variable names and with
their instances. After the if, a name stands for its latest instance, so
`if (other == d) d.close(); d.close();` compared the joined d with the
old one, which made the closed branch impossible and the error was missed.
The guard now names only the instances the condition read.

Fresh values the condition creates (a call's result) are kept in the
context: inside another if's branch they left scope at the outer join
while the joined instances still used them ('Variable #fresh_N could not
be found').

Co-Authored-By: Claude Opus 5.5 <[email protected]>
rcosta358
rcosta358 approved these changes Oct 20, 2026
View reviewed changes
CatarinaGamboa
CatarinaGamboa commented Oct 20, 2026
View reviewed changes
Comment thread ...va-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java
Predicate branchGuard = valueIsCondition ? Predicate.createVar(pathVarName) : expRefs;
// The joined instances are read after the if, where a variable's name stands for its latest instance; the
// guard must name the instances the condition read (`other == d` would otherwise compare the joined d)
Predicate joinGuard = valueIsCondition ? branchGuard : lastExpRefs;

@CatarinaGamboa CatarinaGamboa Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This still leaves a soundness gap when a condition reads a variable that has no last instance (for example, a method parameter). substituteAllVariablesForLastInstance leaves b as a name, so a later assignment changes the meaning of the already-joined guard:

// Door() establishes open(this); close() requires open(this) and establishes closed(this).
void closeTwice(boolean b) {
    Door d = new Door();
    if (b == true) d.close();
    b = false;
    d.close(); // should be a State Refinement Error
}

I ran this on the PR head; LiquidJava says Correct! Passed Verification. The join uses the current b == true (false), discarding the path where d was closed. Could we snapshot condition operands even when they have no existing instance, and add a regression for mutation after the if? This appears to be an existing weakness that the new joinGuard does not cover.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
CatarinaGamboa
CatarinaGamboa commented Oct 20, 2026
View reviewed changes
Comment thread ...va-verifier/src/main/java/liquidjava/processor/refinement_checker/RefinementTypeChecker.java Outdated
if (!name.startsWith(freshPrefix))
continue;
RefinedVariable value = context.getVariableByName(name);
if (value != null && !context.getCtxInstanceVars().contains(value)) {

@CatarinaGamboa CatarinaGamboa Oct 20, 2026

Copy link
Copy Markdown
Collaborator Author

There was a problem hiding this comment.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The retention walk stops at a value already in ctxInstanceVars, even though that value's refinement can refer to a block-local #fresh value. This leaves the nested-if scope failure unresolved for a refined boolean call whose argument is an expression:

@Refinement("_ == 1") static int one() { return 1; }
@Refinement("_ == (n > 0)") static boolean positive(int n) { return n > 0; }
void check(boolean b) {
    Door d = new Door(); // initially open
    if (b) {
        if (positive(one() + 1)) d.close();
    }
    d.close(); // should be a State Refinement Error
}

On this head the second close() instead reports Not Found Error: Variable '#fresh_12' could not be found. Here the joined guard names the synthetic condition value, which is already an instance; the chain through the call's argument refinement to #fresh_12 is never traversed. Please test transitive dependencies of nested conditions, including values already retained as instances.

More broadly, this fix promotes block-local values into the method-wide instance list. Could we document the intended lifetime/scope invariant for values captured by a join and check that this approach fits it? Issue #406 describes the same kind of out-of-scope refinement dependency across try/finally; a general treatment may be safer than handling only names with the #fresh prefix here.

Sorry, something went wrong.

Uh oh!

There was an error while loading. Please reload this page.

All reactions
@CatarinaGamboa
Preserve mutable condition values and nested fresh dependencies
dd8ba71
@CatarinaGamboa
CatarinaGamboa merged commit 0b7343a into main Oct 24, 2026
1 check passed

Uh oh!

There was an error while loading. Please reload this page.

@CatarinaGamboa CatarinaGamboa mentioned this pull request Oct 24, 2026
Land the rest of the state hints stack (#412, #413, #414) #417
Merged
1 task done
CatarinaGamboa added a commit that referenced this pull request Oct 24, 2026
@CatarinaGamboa @claude
Land the rest of the state hints stack (#412, #413, #414) (#417)
69895fa
## Description
#412, #413 and #414 were merged into their stacked base branches instead
of `main`: once #411 was merged, their bases were never retargeted.
Their code is only on `feat/state-hints-unknown`.

This PR applies the three reviewed commits on top of `main`, unchanged:
- Explain dead ends in failing state transitions (#412)
- List the calls that reach an allowed state when the state is unknown
(#413)
- Build each class's state machine once per checker (#414)

The hint and state machine files are identical to
`feat/state-hints-unknown`. Rebase-merging keeps the three commits
separate.

## Checklist
- [x] `mvn test` passes locally on top of the current `main` (#408,
#409)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa CatarinaGamboa mentioned this pull request Nov 4, 2026
Name generated values after what they are instead of #fresh #477
Merged
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

@rcosta358 rcosta358 rcosta358 approved these changes

Uh oh!

There was an error while loading. Please reload this page.

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Uh oh!

There was an error while loading. Please reload this page.

2 participants

@CatarinaGamboa @rcosta358
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.