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 — Origin note: say where the wrong state of a receiver comes from by CatarinaGamboa · Pull Request #528 · liquid-java/liquidjava

Conversation

@CatarinaGamboa

@CatarinaGamboa CatarinaGamboa commented Nov 10, 2026 •
edited
Loading

Uh oh!

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

Copy link
Copy Markdown
Collaborator

Closes #522, part 2: origin notes F1 and F2, where the wrong state comes from. Design: #522 (comment)

Base: main. #526 is merged. #530 is stacked on this PR, so retarget it to main before merging this one with --delete-branch.

The Origin note

The PM's review renamed the note from "From:" to "Origin:", because "From" read like @StateRefinement(from = …) in declared-here. Every origin note now has one shape, Origin: <where>, <call or variable> <verb> <state or refinement>, and only the verb changes. The first commit renames the note #526 merged (FromNotes → OriginNotes, TestFromNotes → TestOriginNotes). The soundness conditions are unchanged.

Case Text
F1, the call that last set the sets Origin: line 134, new BufferedInputStream(stream) makes markState(buffered) == unmarked
F2, a returned value Origin: line 130, writer.getDefaultWriteParam() returns compression(wParam) == compressionNotExplicit
F2, nothing known Origin: line 69, frame.getUndoManager() says nothing about redoState(undoManager)
F1 fallback, creation Origin: line L, new T(…) creates x with <state>
F3 (#526) Origin: line 94, timeout is a parameter of checkCimReachable(String, int) with no refinement

"Makes" is used only for the call the walk proves last set the shown sets. The fallback says "creates … with", because a later call may have set the state again. That is a fact about the constructor, not about the current state.

When there is no note at all

No origin note is given when the receiver may change state out of the verifier's sight before the call. That covers three cases:

  • Another name may hold it. A local, a parameter or this is used, before the call, other than as the target of a call or field access or in ==/!=. That means copied, passed as an argument (including to the failing call itself), stored in an array, returned, bound in a method reference (x::start), or used in a lambda or a local class. "Before" means earlier in the source, in the failing call's arguments, or in a loop around the call, unless the use is inside a return/throw.
  • A method of the analyzed code with no state refinement is called on it before. The verifier assumes such a call keeps the state.
  • It is a field.

Declared state contracts are trusted, as the verifier trusts them. A body that breaks its own declared contract is a verifier issue, outside the notes (see the round 3 reply).

Which sets

The note covers the sets that the failing call's from checks. When the state machine models the call, it narrows them to the sets where some state the receiver may be in is the state of no allowed state. This only selects what is shown (S1).

F1, a state set by a call

Walk back from the receiver through StateChange/previous() to the first change whose to names a shown set. The note is printed only if:

  • every instance on the way has a StateChange, so the walk crosses no join, loop head or unknown origin;
  • every change crossed names none of the shown sets. Each to keeps the sets it does not name, through set(this) == set(old(this));
  • StateSolver.implies(<instance after that change>, <Found's facts on the shown sets>) holds.

Text: Origin: line L, <call> makes <Found on the sets that change names>. With two wrong sets, a change to one is not given the other.

Fallback, when the walk breaks. The note gives the creation fact only if all hold:

  • the receiver is a local whose only write is its initializer, a constructor call with a single state refinement;
  • no other call on it is to a method that changes state, and no loop may repeat the failing call;
  • Found's facts on the shown sets imply what that constructor gives.

Text: Origin: line L, new T(…) creates x with <state>. It is a fact about the constructor. The last condition stops it from speaking when the state may have changed since (A1_09).

F2, a state from a returned value

Printed only if the walk reaches, possibly after crossing changes to other sets, an instance with no StateChange that holds the call result it was assigned. Instances now record a CallResult when a local is declared or assigned from a call to a refined method. A state-preserving call keeps it, as it keeps the StateChange. Joins, loop heads and transitions make new instances without one.

  • Origin: line L, <call> returns <facts>: the conjuncts of the instantiated return refinement on the shown sets. Printed only if they imply Found's facts on those sets.
  • Origin: line L, <call> says nothing about <set>(x): printed only if all hold:
    • Found has no fact on the sets;
    • the method has no state refinement with a to (as to = "running(_)");
    • the return refinement names no state of them, mentions no variable but _, and uses no alias. This rules out _ == this-style returns;
    • no shown set has a generated name (state1), which the message would otherwise never show.

DebugLog check for A1_38 (asked before coding F1)

With -d, the VC at buffered.reset() (line 157) has a single buffered instance, #buffered_259 : (stream == open) ∧ (markState == unmarked). That is the constructor's state, with no ITE and no loop-head instance. The note also took the F1 path, which needs a StateChange on that instance. So neither loop (lines 136 and 150) creates a join for buffered.

A broken walk now logs [WARN] Origin note: no known call gives #x_n its state, e.g. a join of branches. A1_09 logs it for #host_95.

Study tasks, before (main) and after

 State Refinement Error: found stream(buffered) == open && markState(buffered) == unmarked but expected markState(buffered) == marked && stream(buffered) == open
+  Origin: line 134, new BufferedInputStream(stream) makes markState(buffered) == unmarked
 State Refinement Error: found compression(wParam) == compressionNotExplicit but expected compression(wParam) == compressionExplicit || compression(wParam) == compressionTypeSet
+  Origin: line 130, writer.getDefaultWriteParam() returns compression(wParam) == compressionNotExplicit
 State Refinement Error: expected redoState(undoManager) == redoable but could not prove it
+  Origin: line 69, frame.getUndoManager() says nothing about redoState(undoManager)
 Refinement Error: expected timeout >= 0 but could not prove it
-  From: timeout is a parameter of checkCimReachable(String, int) (line 94) with no refinement
+  Origin: line 94, timeout is a parameter of checkCimReachable(String, int) with no refinement
  • A1_09: no origin note. Its receiver is a join, and Found does not say the state new JFrame(…) gives.
  • A2_10, practice and practice2: unchanged.

Each task still gives exactly one error, on its reportedLine. All three notes are the review's mock-ups, word for word.

I checked every state-error fixture of the suite. Each note names the creating or the last call. After the Codex rounds, the fallback fires in none, and fixtures where the receiver is captured, a field, this in a loop, or given an unspecified call get no note.

Tests

TestOriginNotes, three tests that fail without the change:

  • ErrorStateHintMultipleSets (existing): Origin: line 27, d.lock() makes lockState(d) == locked. Only the wrong set is named, and the walk stops at the change to it;

  • ErrorStateFromReturnedValue (new, small; no existing fixture assigns a state from a return refinement): Origin: line 27, FromReturnedTask.create() returns phase(task) == idle.

  • ErrorDeferredCallbackState (existing): the receiver captured by a lambda gets no note.

mvn test: 736 run, 0 failures.

Conflicts

  • With One headline frame: found … but expected … for refinement errors #520/Drop the general UNKNOWN hint and show every from in declared-here #521 (another agent: AuxStateHandler declared-here): the one throwStateRefinementError call in changeState, which gets a messageNotes argument. The rest is additive.
  • With Drop the guard hint when a call-sequence hint exists, and reword it #518/State hints: name only the state the call leads to #519 (StateHints): none.
  • With Show a counterexample for state errors, restricted to the shown Found #527 (state counterexample, mine): both add a parameter to throwStateRefinementError, a one-line rebase.

🤖 Generated with Claude Code

Sorry, something went wrong.

Uh oh!

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

All reactions
@CatarinaGamboa

CatarinaGamboa commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Clean-code review

I audited the diff with the clean-code-principles checklist.

Fixed, while writing it:

  • [solid-srp] The walk and the three note kinds are in their own class, StateOrigin. AuxStateHandler only calls it, best effort, as it calls the hint and category helpers.
  • [core-fail-fast] The call's code is read from its source position, and it is left out when the position or source is missing. Spoon's getOriginalSourceFragment threw on A1_09's applyDisplayMode call and aborted verification. I caught that on the study tasks before pushing.
  • [core-dry] One factsOn helper projects every predicate (Found, a to, a return refinement) on the shown sets. One implies helper wraps StateSolver.
  • [name-meaningful] CallResult / getReturnedBy instead of reusing StateChange for returned values. It also keeps DecidingArgument's walk unchanged.

Declined:

  • [func-few-arguments] StateOrigin.note(...) and AuxStateHandler.fromNote(...) take 10 and 9 arguments. They mirror the neighbouring best-effort helpers (checkedElsewhere, stateHints), which pass the same pieces. A parameter object for one call site is a refactor of changeState, outside this PR.
  • [core-dry] StateOrigin.setsOf repeats the three-line "sets a predicate names" stream in DecidingArgument.find. Sharing it would edit DecidingArgument, which Drop the guard hint when a call-sequence hint exists, and reword it #518/State hints: name only the state the call leads to #519 work near. It is left for a follow-up.
All reactions

Sorry, something went wrong.

Uh oh!

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

@CatarinaGamboa

CatarinaGamboa commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Codex review, round 1

codex exec review:

[P2] Check the returned call after crossing unrelated state changes. After task = create() and a lock-only transition, a later phase error loses its note: the walk reaches the original instance but calls creationNote instead of checking returnedBy.

Soundness prompt. It found three programs with misleading notes:

Program Printed Why it is wrong
x.start(); LastSetTask y = x; y.finish(); x.needsIdle(); From: phase(x) == running, last set at line 14: x.start() y.finish() changed the same object later
x = new BranchTask(); if (flag) { x.start(); x.reset(); } x.await(); From: x is created at line 13 by new BranchTask(), which makes it phase(x) == idle on the flag path, reset() gave the state
x = create(); y = x; y.start(); x.await(); From: x = ReturnedAliasTask.create(), line 14, which returns phase(x) == idle y.start() changed it

The verifier itself does not follow state changes through aliases (#393 is open), so Found is already wrong in cases 1 and 3. A From note must not present that as a cause.

Fixed, all four, in "Leave out the From note for an aliased receiver, and follow a returned value across other sets":

  1. No note when another name may hold the receiver before the call. That covers a local or parameter copied (T y = x, y = x, ?:), returned, or used in a lambda or a local or anonymous class, where that use comes earlier or sits in a loop around the call. A field receiver also gets no note; this keeps it.
    • Uses after the call do not count: A1_01 returns wParam after line 136 and keeps its note.
    • Passing the receiver as an argument does not count either. The verifier does not model a callee changing an argument's state, so the note would still agree with Found. I'll raise this with the PM.
  2. The creation fallback also needs no other call on the local to a method that changes state, and no failing call that a loop may repeat.
  3. The walk now checks returnedBy wherever it stops, also after crossing changes to other sets. It reads Found's facts about the instance it reached when Found has none about the receiver itself. The P2 program now gives From: task = TwoSetTask.create(), line 16, which returns phase(task) == idle.

All four programs now print no note, or a sound one. The study tasks are unchanged: A1_38, A1_01 and A1_56 keep their mock-up notes, and A1_09 has none.

In the suite's fixtures:

  • ErrorDeferredCallbackState loses its 4 notes (the receiver is captured, or a field);
  • the ErrorLoopStateNone fallback is gone (the failing call has a to and a loop repeats it);
  • ErrorArgumentPicksTheStateEarlier gains From: tilingOff(param), last set at line 25: param.setTilingMode(1), across compress(). That is correct.

New test: stateOfAReceiverAnotherNameMayHoldIsNotTraced, on the lambda case of the existing ErrorDeferredCallbackState. It fails without the fix.

Tests: 733 run, 0 failures.

All reactions

Sorry, something went wrong.

Uh oh!

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

@CatarinaGamboa CatarinaGamboa mentioned this pull request Nov 10, 2026
Fold a state error's Found under the constants its path conditions state #529
Closed
@CatarinaGamboa

CatarinaGamboa commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Codex review, round 2

codex exec review:

  • [P2] Treat passing the receiver to another method as possible aliasing. A callee may keep a reference and change the state.
  • [P2] Limit the last-set note to the states that call changed. With two wrong sets, the note attributed both to a call that changed one.

Soundness prompt. It found five programs, all found phase(task) == idle but expected phase(task) == running, where task escapes before the call:

  • mutate(task) calls start() (also on one branch);
  • ArrayTask[] all = {task}; all[0].start();
  • all.add(task); all.get(0).start();
  • task = create(); mutate(task).

In each one, the note named the constructor or the factory as the origin.

Fixed, all, in "Count a receiver passed to a method as aliased, and say only what a change set":

  1. Any use of the receiver local or parameter that may run before the call counts as a possible alias, other than as a call's receiver or in ==/!=. That covers arguments, array initializers, copies, returns, conditionals, and lambda or local-class uses.
    • A use inside a return or throw cannot run again in a loop around the call. This keeps A1_38's note: its createTaskIterator(buffered, …) is in a return inside the loop around reset().
  2. The last-set note shows only the facts on the shown sets the change names. d.lockIt(); d.walkThrough() (needs opened && unlocked) now gives From: lock(d) == locked, last set at line 15: d.lockIt(), not the door state too.

Checks:

  • All eight programs from rounds 1 and 2 now print no note, except the round 1 P2 case, which is sound (From: task = TwoSetTask.create(), line 16, which returns phase(task) == idle).
  • The study tasks are unchanged: A1_38, A1_01 and A1_56 keep their notes; A1_09 has none.
  • The fixture notes are unchanged from round 1.

Tests: 733 run, 0 failures.

All reactions

Sorry, something went wrong.

Uh oh!

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

@CatarinaGamboa CatarinaGamboa mentioned this pull request Nov 10, 2026
Origin note: say what a call's return refinement guarantees about an argument it gives #530
Merged
@CatarinaGamboa

CatarinaGamboa commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Codex review, round 3

codex exec review: "No actionable regressions were identified."

Soundness prompt. It found three misleading notes:

Program Printed Why
@StateRefinement(to = "running(_)") static T makeRunning(), then x = makeRunning(); x.needIdle() From: x = makeRunning(), line 15, which says nothing about phase(x) the method's state refinement does give the value a state
x.start(); x.resetInternally(); x.needIdle(), where resetInternally is @StateRefinement(from = "running(this)") but its body calls reset() From: phase(x) == running, last set at line 13: x.start() the body changes the state, against its contract
a constructor @StateRefinement(to = "idle(this)") whose body calls start() From: phase(x) == idle, last set at line 10: new ConstructorCallsMethod() the body breaks the constructor's contract

Fixed (1) in "Leave out the From note after an unspecified method body on the receiver". "Says nothing" also needs the called method to have no state refinement with a to.

Fixed, a related gap, while looking at (2). No note when a method of the analyzed code with no state refinement is called on the receiver before the failing call. The verifier assumes such a call keeps the state, but that is an assumption, not a contract. ErrorUnknownMethodInComparisonState (where count() has a body) loses its note as a result, which is conservative.

Declined (2) and (3). In both, a method or constructor body breaks its declared state contract, and the verifier does not check bodies against state contracts. The error trusts the contract (its Found is running / idle), and the note quotes the same contract. Dropping notes whenever a body might break its own spec would drop every note on source classes. I'll flag the unchecked bodies to the PM as a verifier issue, separate from the messages.

Checks:

  • The study tasks are unchanged: A1_38, A1_01 and A1_56 keep their notes; A1_09 has none.
  • The earlier Codex programs are still silent or sound.

Tests: 733 run, 0 failures.

All reactions

Sorry, something went wrong.

Uh oh!

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

@CatarinaGamboa

CatarinaGamboa commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Codex review, round 4

codex exec review:

  1. [P2] Check earlier helper calls before attributing this state. The alias and unspecified-body checks returned early for a this receiver.
  2. [P2] Keep note-generation errors from replacing the state error. An LJError from the note's implies query is not caught.

Soundness prompt. It found three programs, all with a note where the alias rule asks for none:

  • hold(this); this.needsIdle(): hold stores this in a static field;
  • x.needsIdle(hold(x)): the argument runs before the call but comes later in the source, so the "before the call" test missed it;
  • the same with a factory-assigned x (F2).

Fixed (1) and all three programs in "Apply the alias checks to this and to the failing call's own arguments":

  • For a this receiver, the same checks now run over every this access of the method in the same class, explicit or implicit: any escape, and any call to an unspecified source method. A field access or a call on this is not an escape.
  • A use inside the failing call's arguments counts as running before the call.

All three programs now print no note. ErrorLoopPostState (implicit this) loses its note, which is conservative. The study tasks are unchanged: A1_38, A1_01 and A1_56 keep their notes; A1_09 has none.

Declined (2): it does not happen. LJError extends RuntimeException, and AuxStateHandler.fromNote catches RuntimeException and returns no note. My first version had catch (LJError | RuntimeException e), and javac rejected it: "Alternative LJError is a subclass of alternative RuntimeException". So a failing implies query never replaces the state error.

Tests: 733 run, 0 failures.

All reactions

Sorry, something went wrong.

Uh oh!

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

@CatarinaGamboa

CatarinaGamboa commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Codex review, round 5 (last)

Soundness prompt. A bound method reference keeps the receiver: Runnable later = task::start; …; later.run(); before the failing call. Since task was the target of the method reference, it counted as a safe use, and notes named the constructor, task.start() or the factory as the origin.

Fixed in "Count a method reference to the receiver as aliasing it". Only the target of a call or a field access is a safe use, and a method reference counts as an escape. The two programs (three errors) now print no note. The study tasks and fixture notes are unchanged.

codex exec review:

[P2] Preserve the state error when origin lookup fails.

This is round 4's point again. Declined, as there: LJError extends RuntimeException, and javac rejects catch (LJError | RuntimeException e) for that reason, so fromNote's catch (RuntimeException e) already turns a failed or unknown solver query into "no note", and the error is still reported. I added a one-line comment at the catch so the next reader doesn't have to check.

Tests: 733 run, 0 failures.

This is the fifth round, the agreed limit. Each round found narrower cases, from aliases to arguments, this, argument order and now method references. They all share one root cause: the verifier does not follow state changes through other names (#393 is open). The note now leaves itself out whenever the receiver may escape before the call. The remaining risk is the same as the verifier's own Found: a declared contract whose body breaks it.

All reactions

Sorry, something went wrong.

Uh oh!

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

This was referenced Nov 10, 2026
Say where the wrong state or value comes from (From note), and counterexamples for state errors #522
Closed
Mark code in every message with backticks: blue in the CLI, code font in the IDE #532
Closed
CatarinaGamboa and others added 8 commits November 10, 2026 19:58
@CatarinaGamboa @claude
Call the note Origin and lead with where the value comes from
2623683
"From" read like the from of @StateRefinement, which declared-here
shows. The note is now "Origin: line L, <p> is a parameter of <m> with
no refinement", the shape every origin note takes.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa @claude
Say where the wrong state of a receiver comes from
548a570
A state refinement error now has a From message note when what the
verifier knows proves it:
- the call that last set the wrong state set, found by walking the
  receiver's StateChange chain through changes to other sets only, and
  printed only if that change's instance proves the Found on the set;
- else, for a receiver assigned the value of a call and not changed
  since, what the call's return refinement says about the set, or that
  it says nothing about it when Found says nothing either;
- else, when the walk breaks (a join, a loop head) and the receiver is a
  local only ever written by its constructor initializer, what the
  constructor makes it, if Found still says that state.

The note names only the sets where no possible state is allowed, when
the state machine tells. Instances record the call result they were
assigned, and a state-preserving call keeps it, as it keeps the
StateChange.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa @claude
Leave out the From note for an aliased receiver, and follow a returne…
8f82b34
…d value across other sets

- No note when the receiver may be held by another name before the
  call: a local or parameter copied, returned or captured by a lambda or
  local class earlier or in a loop around the call, or a field.
- The creation note also needs no other call on the local to a method
  that changes state, nor a failing call a loop repeats.
- A walk that crosses changes to other sets and reaches the call result
  the receiver was assigned gives the returned-value note, reading what
  Found says about that instance.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa @claude
Count a receiver passed to a method as aliased, and say only what a c…
496759c
…hange set

- A local or parameter used before the call other than as a call's
  receiver or in a comparison (passed to a method, stored in an array,
  copied) may be held by another name, so it gets no From note. A use in
  a return or a throw cannot run again in a loop around the call.
- The last-set note shows only the facts on the shown sets the change
  names, so a change to one of two wrong sets is not given the other.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa @claude
Leave out the From note after an unspecified method body on the receiver
160ad72
- No note when a method of the analyzed code with no state refinement is
  called on the receiver before the failing call: the verifier takes it
  to keep the state, which its body may still change.
- "says nothing" also needs the called method to give no state through a
  state refinement, as to = "running(_)" does.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa @claude
Apply the alias checks to this and to the failing call's own arguments
5bb7647
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa @claude
Count a method reference to the receiver as aliasing it
bf683fa
Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa @claude
Write the state origin notes as Origin: line L, <call> <verb> <state>
51666ee
- the call that last set the sets "makes" their state;
- a call the receiver was assigned "returns" it, or "says nothing
  about" the sets;
- the creation fact says the constructor "creates" the receiver with
  its state, not that it makes the current one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@CatarinaGamboa
CatarinaGamboa force-pushed the from-note-state branch from 4757b3b to 51666ee Compare November 10, 2026 20:06
@CatarinaGamboa
CatarinaGamboa changed the base branch from from-note-f3 to main November 10, 2026 20:06
@CatarinaGamboa

CatarinaGamboa commented Nov 10, 2026

Copy link
Copy Markdown
Collaborator Author

Origin rename (PM review), and Codex review of it

What changed:

  • "From:" is now "Origin:", and every origin note has one shape, Origin: <where>, <call or variable> <verb> <state or refinement>:
    • makes for the call that last set the state sets;
    • returns or says nothing about for a returned value;
    • creates x with for the constructor fallback, so it doesn't claim the current state;
    • is a parameter of … with no refinement for the parameter note.
  • From note: name an unrefined parameter as the unknown value's origin #526 was merged with the old text. So this PR's first commit renames it: FromNotes → OriginNotes, the test file too, and the text becomes Origin: line 94, timeout is a parameter of checkCimReachable(String, int) with no refinement.
  • The PR is rebased onto main (which now has State hints: name only the state the call leads to, and no guard when a call sequence works #524, Use found … but expected … for refinement errors, drop the general hint, declare every from #525 and From note: name an unrefined parameter as the unknown value's origin #526) and retargeted to main. The only overlap was Use found … but expected … for refinement errors, drop the general hint, declare every from #525's declarationPositions list in throwStateRefinementError, kept with messageNotes beside it.
  • The soundness conditions are unchanged. The PR body has the new before/after.

Codex review (one round, as asked):

  • codex exec review: "I found no actionable regressions in the diff. The targeted origin-note tests pass."
  • Soundness prompt: "I found no Java program where the new origin text is false or misleading. The rename preserves the note conditions… the constructor fallback uses 'creates … with,' not 'makes.'"

Tests: 736 run, 0 failures.

All reactions

Sorry, something went wrong.

Uh oh!

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

@CatarinaGamboa CatarinaGamboa mentioned this pull request Nov 10, 2026
Mark code in every message with backticks, and name the lines they refer to #533
Merged
@CatarinaGamboa CatarinaGamboa changed the title From note: say where the wrong state of a receiver comes from Origin note: say where the wrong state of a receiver comes from Nov 10, 2026
@CatarinaGamboa CatarinaGamboa mentioned this pull request Nov 10, 2026
Show the code of messages in the editor font and link the lines they name liquid-java/vscode-liquidjava#193
Merged
@CatarinaGamboa
CatarinaGamboa merged commit 070fb75 into main Nov 11, 2026
1 check passed

Uh oh!

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

CatarinaGamboa added a commit that referenced this pull request Nov 11, 2026
@CatarinaGamboa @claude
Origin note: say what a call's return refinement guarantees about an …
590268e
…argument it gives (#530)

Closes #522, part 3: **origin note F4**, a value returned by a call.
Design:
#522 (comment)

**Stacked on #528** (`CallResult` and the Origin rename). Retarget this
PR to `main` before #528 is merged with `--delete-branch`, or GitHub
closes it.

A refinement error whose Expected is about a **call argument that is
itself a call to a refined method** now says what that method's return
refinement guarantees. This is the UX sign-off's amendment 1 for A2_10:
instantiated as the error writes it, with the ghost-defining conjunct
dropped.

## When it is printed
For each variable of Expected that is an argument instance with no
variable of its own (shown as the call), holding the `CallResult` of a
refined method whose refinement has a source position. Locals assigned
from a call are left to #528's state notes. A refinement-error form for
them (`Origin: line 44, read() guarantees only n >= -1`) is a question
for the PM.

It is also left out when the refinement still names an argument the
verifier calls after the callee's parameter (`twice(0)` for `_ == b *
2`), which would read as that parameter.

**Text:** `Origin: <File>:<line>, <call> guarantees only <R'>`, or
`Origin: <File>:<line>, <call> guarantees nothing about <call>` when
nothing is left. This is the shape #528 gives every origin note.

**Why "only" is sound.** `R'` is the instantiated return refinement
minus each conjunct `_ == g(…)` where all hold:
- `g` is a declared ghost (`@Ghost`), and never a built-in name like
`length`, even when some class declares a ghost of that name;
- `g`, on any arguments, appears in no other conjunct and not in
Expected. Any arguments, because an alias of the receiver may constrain
the ghost under another name;
- every fact of the error's Found that applies `g` is the argument's own
equality with it;
- no alias application in the refinement, Expected or Found may hide a
use of `g`.

Then nothing constrains `g(…)` but that equality, and ∃g. (_ == g ∧
Q(_)) ≡ Q(_), since `g := _` is a witness. So `R'` is exactly what the
refinement says about the value. Nothing else is dropped. Otherwise the
equality is kept, e.g. `Origin: …, count(xs) guarantees only count(xs)
== length(xs)`.

## A2_10, before and after
```diff
 Refinement Error: found failures.size() == size(this.failures) && failures.size() >= 0 but expected failures.size() > 0
+  Origin: ArrayListRefinements.java:162, failures.size() guarantees only failures.size() >= 0
 75 |     public Failure get() {
```
This is the amended mock-up, in the Origin shape. The other notes are
unchanged from #528, and every task still gives one error on its
`reportedLine`.

Across all refinement-error fixtures, the only new notes are the two
call arguments in `classes/found_binders_error`: `items.size()` and
`count()`.

## Test

`TestOriginNotes.callArgumentIsTracedToWhatItsReturnRefinementGuaranteesAboutIt`,
on the existing `classes/found_binders_error` (the A2_10 reproducer,
line 25): `Origin: ArrayListRefinements.java:13, items.size() guarantees
only items.size() >= 0`. It fails without the change.

`mvn test`: 737 run, 0 failures.

## Conflicts
- With #520/#521 (another agent, `RefinementError` headline): none in
the files changed here.
`MethodsFunctionsChecker.createVariableRepresentingArgument` gets one
line, and `Utils` gets `isBuiltin(name)` over its existing
`DEFAULT_NAMES`.

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
CatarinaGamboa added a commit that referenced this pull request Nov 11, 2026
@CatarinaGamboa @claude
Mark code in every message with backticks, and name the lines they re…
cfe9405
…fer to (#533)

Closes #532. **Stacked on #530** (→ #528 → `main`). Retarget it to
`main` before #530 is merged with `--delete-branch`.

Every message that shows code now marks it with backticks, inside the
message string:
- headlines (`found … but expected …`, `expected … but could not prove
it`);
- hints;
- origin notes;
- the declared-here gloss (`where … means …`);
- alias `where` lines;
- counterexamples.

## How each client sees it
- **Plain text** (`getMessage()`, `getMessageNotes()`, `Hint.message()`,
`getCounterexampleStr()`, `getDeclarationNotes()`, and so the Problems
tab and the hover): the string with backticks, as rustc writes its
messages.
- **CLI:** each span in blue (the existing `Colors.BLUE`), without
backticks. The backticks stay when colour is off: `NO_COLOR` set
(https://no-color.org) or no console (output piped or redirected). The
colour switch applies to code spans only; the CLI's other colours are
unchanged.
- **IDE:** the same backticked strings, plus
`LJDiagnostic.getReferences()`, a `List<SourceReference>` with one entry
per line reference. See below.

**Escaping:** `Code.of` fences code that holds backticks with a longer
run, and pads it with one space on each side when it holds a backtick or
starts or ends with a space: ``` `` c == '`' `` ```. `Code.render`
strips exactly that padding, so it shows any non-empty code exactly as
marked.

## Source references (for vscode-liquidjava#192)
`record SourceReference(String message, String text, int start, String
file, int line, int column)`:
- `message`: the whole message line it belongs to, exactly as
`getMessageNotes()` or `Hint.message()` gives it;
- `text`: the words of `message` that name the place, e.g. `line 134` or
`ArrayListRefinements.java:162`;
- `start`: where `text` starts in `message`, as a UTF-16 index (a
Java/JS string index), so `message.substring(start, start +
text.length())` is `text`. Use it instead of searching, because the code
of a call can hold the same words (`d.lock("line 18")` on line 18);
- `file`: the absolute path; `line`, `column`: 1-based, from Spoon.

Who carries one:
- every origin note: the call, parameter, constructor or
return-refinement declaration it names;
- the deciding-argument hint (`… on line 20 decides …`), through the new
`Hint.reference()`, which is null for other hints.

`getReferences()` returns the notes' references first, then the hints'.

## Before → after (CLI piped, so backticks shown)
```text
A1_38  State Refinement Error: found `stream(buffered) == open && markState(buffered) == unmarked` but expected `markState(buffered) == marked && stream(buffered) == open`
         Origin: line 134, `new BufferedInputStream(stream)` makes `markState(buffered) == unmarked`
         - Call `buffered.mark(int)` before this call; afterwards `markState(buffered) == marked`
         where `marked(this)` means `markState(this) == marked`, `open(this)` means `stream(this) == open`
A1_01    Origin: line 130, `writer.getDefaultWriteParam()` returns `compression(wParam) == compressionNotExplicit`
         - Call `wParam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT)` before this call; afterwards `compression(wParam) == compressionExplicit`
A2_04  Refinement Error: expected `timeout >= 0` but could not prove it
         Origin: line 94, `timeout` is a parameter of `checkCimReachable(String, int)` with no refinement
          --> Counterexample: `timeout == -1`
A1_56    Origin: line 69, `frame.getUndoManager()` says nothing about `redoState(undoManager)`
         - Guard this call with `if (undoManager.canRedo())`; inside it, `redoState(undoManager) == redoable`
A2_10  Refinement Error: found `failures.size() == size(this.failures) && failures.size() >= 0` but expected `failures.size() > 0`
         Origin: ArrayListRefinements.java:162, `failures.size()` guarantees only `failures.size() >= 0`
```
For all eight tasks (six study tasks and both practice tasks), the
output with backticks removed is byte-identical to #530's. Each task
still gives exactly one error, on its `reportedLine`. On a terminal the
spans are blue and the backticks are gone. With `NO_COLOR=1` the
backticks stay.

## Changes
- `Code` (`of`, `render`), `SourceReference`, and `Colors.enabled()`.
- `LJDiagnostic`:
  - `addMessageNote(SourceReference)` and `getReferences()`;
- `toString` renders the message, notes, counterexample, hints and
declaration notes.
- Marked at the leaves where messages are built:
  - `ErrorMessage` (both headline frames);
  - `RefinementError` (counterexample, alias lines);
  - `AuxStateHandler.addMeanings` (gloss);
- `StateHints`: each state name, call, receiver and constructor, with
the prose words `or`, `and`, `then`, `with` left unmarked;
  - `OriginNotes`/`StateOrigin`, whose notes are now `SourceReference`s.
- `NotFoundError` marks the missing name and the suggestion (``Alias
`Positiv` could not be found``, ``Did you mean `Positive`?``).
- Not marked: the other error kinds (state conflicts, syntax errors) and
warnings, which the issue does not list. They are a follow-up if wanted.

## Tests
- 59 existing expectations gained backticks. Each change was made by a
script that accepts a new string only if removing its backticks gives
exactly the old one.
- `TestStateHints.assertHints` compares kind and message.
- New tests, each failing without the change:
  - `CodeTest` (fence, colour on and off);
  - `TestOriginNotes.noteNamesTheLineOfTheCallForClientsToLinkTo`;
  - a reference check in the deciding-argument hint test.

`mvn test`: 740 run, 0 failures.

## Conflicts
- **#524 and #525 are merged**, and this branch sits on top of them (via
#528's rebase), so it does not conflict with them. The lines of their
code it changes are:
- `StateHints.java`, on main: the hint templates at 116–119, 131–132,
182–184, 234, 239–240, 249, 252, 258, 260, 269, 295–297, 301, 303–305,
349, and `stateName` at 387–389;
- `RefinementError.java`, on main: 71 (counterexample) and 123 (alias
`where` lines);
  - `ErrorMessage.java` 39–41 (headline);
  - `AuxStateHandler.java` 865–866 (gloss).
- With the #529 agent's work: both may touch
`VCChecker.throwStateRefinementError`. This PR changes only the notes'
parameter type there.

🤖 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 11, 2026
Do not link Found facts through an enum constant they only compare with #534
Merged
CatarinaGamboa added a commit to liquid-java/vscode-liquidjava that referenced this pull request Nov 11, 2026
@CatarinaGamboa @claude
Show the code of messages in the editor font and link the lines they …
8c1a96f
…name

The server sends each diagnostic's message notes and the source references
of its notes and hints. The panel shows the notes under Found, and renders
every message line (headline, notes, hints, declared-here gloss) with the
code the verifier marks in backticks in the editor font and each reference,
such as `line 134`, as a link that opens that line.

Needs a liquidjava-verifier release with liquid-java/liquidjava#528, #530
and #533.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
CatarinaGamboa added a commit to liquid-java/vscode-liquidjava that referenced this pull request Nov 11, 2026
@CatarinaGamboa @claude
Show the code of messages in the editor font and link the lines they …
9aa2590
…name

The server sends each diagnostic's message notes and the source references
of its notes and hints. The panel shows the notes under Found, and renders
every message line (headline, notes, hints, declared-here gloss) with the
code the verifier marks in backticks in the editor font and each reference,
such as `line 134`, as a link that opens that line.

Needs a liquidjava-verifier release with liquid-java/liquidjava#528, #530
and #533.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
CatarinaGamboa added a commit to liquid-java/vscode-liquidjava that referenced this pull request Nov 11, 2026
@CatarinaGamboa @claude
Show the code of messages in the editor font and link the lines they …
addd135
…name

The server sends each diagnostic's message notes and the source references
of its notes and hints. The panel shows the notes under Found, and renders
every message line (headline, notes, hints, declared-here gloss) with the
code the verifier marks in backticks in the editor font and each reference,
such as `line 134`, as a link that opens that line.

Needs a liquidjava-verifier release with liquid-java/liquidjava#528, #530
and #533.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
CatarinaGamboa added a commit to liquid-java/vscode-liquidjava that referenced this pull request Nov 11, 2026
@CatarinaGamboa @claude
Show the code of messages in the editor font and link the lines they …
d73c500
…name (#193)

Closes #192

**Stacked on #191** (base `feat/181-declaration-notes`): it renders
#191's declared-here gloss, and it needs the same 0.0.38 bump. Retarget
this PR to `main` before #191 is merged with `--delete-branch`.

## What changes
**Server**
- `LJDiagnosticDTO` now sends two more fields for every diagnostic:
  - `messageNotes`, from `getMessageNotes()`;
- `references`, from `getReferences()`, as `SourceReferenceDTO(message,
text, start, position)`. The position counts lines and columns from 0,
like the other DTO positions.
- Both come from the verifier's generic `LJDiagnostic` API. New notes
and references therefore show up with no extension change.

**Panel (Verification tab)**
- The **message notes**, e.g. `Origin: line 134, …`, are shown under
Found and the Counterexample. Before this PR the panel did not show
notes at all.
- Every message line goes through one renderer,
`webview/message-text.ts`: the header message, notes, hints and the
declared-here gloss.
- **Code** the verifier marks with backticks is shown in the editor
font, without the backticks, with VS Code's inline-code colours
(`textPreformat`).
- Spans are parsed as the verifier's `Code.render` parses them: the same
run-of-backticks pattern, and one space of padding stripped.
- **Each reference** (`line 134`, `ArrayListRefinements.java:162`) is a
link that opens that file at that line. It is placed at the reference's
`start`; the text is not searched for, as the verifier asks. It reuses
the panel's existing `location-link` handler.
- **Hints** are now in the UI font, so their code stands out. Before,
the whole hint was monospace.
- **Header message:** it is now escaped. Before, it was put into the
HTML unescaped.

**Hover and Problems:** unchanged by this PR. See the two options below.

## Verifier version
Needs a release (0.0.38 or later) containing
**liquid-java/liquidjava#528, #530 and #533**, on top of #517 and #525
(both on main).
- `getReferences()`, `SourceReference` and the backticks come from #533.
- The Origin notes come from #528/#530.

I tested against #533's head (`1e94930`), installed as 0.0.38 into an
isolated Maven repository. Like #191, CI's server build fails until
0.0.38 is on Maven Central.

## Panel at the study width: before (#191) → after (this PR)
Both use verifier #533, with Hints open. The notes are new, the code is
set apart, and `line 134` is a link.

| A1_38 | A2_10 |
|---|---|
| <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_38.png"
width="420"> | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A2_10.png"
width="420"> |

| A2_04 | A1_56 |
|---|---|
| <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A2_04.png"
width="420"> | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_56.png"
width="420"> |

| A1_01 | A1_09 (hints) |
|---|---|
| <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_01.png"
width="420"> | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_09.png"
width="420"> |

**Clicking `line 134`** in A1_38 opens line 134, `new
BufferedInputStream(stream)`:

<img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_38-link.png"
width="860">

Panel text of the notes (verifier #533):
```
A1_38  Origin: line 134, new BufferedInputStream(stream) makes markState(buffered) == unmarked
A1_01  Origin: line 130, writer.getDefaultWriteParam() returns compression(wParam) == compressionNotExplicit
A2_04  Origin: line 94, timeout is a parameter of checkCimReachable(String, int) with no refinement
A2_10  Origin: ArrayListRefinements.java:162, failures.size() guarantees only failures.size() >= 0
A1_56  Origin: line 69, frame.getUndoManager() says nothing about redoState(undoManager)
```
A1_09 has no Origin note.

## Hover: the two options
VS Code shows a diagnostic's message as plain text. So the diagnostic
hover and the Problems view keep the backticks.

- **A, plain text (#190):** the diagnostic message is the headline, then
the notes, then the counterexample, all plain with backticks, as rustc
writes them. One text, the same as the CLI with colour off, in the hover
and in Problems.
- **B, markdown provider (prototype on branch
`proto/192-markdown-hover`, not in this PR):**
  - The diagnostic message stays the headline only, as on main.
- The extension's `HoverProvider` adds a markdown block with the notes
and the counterexample: code spans, and `line 134` as a link.
- VS Code draws it under the problem part, so nothing is shown twice.
That holds only as long as #190's notes are not also in the diagnostic
message.

| | A: plain (#190 + #533) | B: markdown provider |
|---|---|---|
| A1_38 | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_38-hover-a.png"
width="430"> | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_38-hover-b.png"
width="430"> |
| A2_04 | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A2_04-hover-a.png"
width="430"> | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A2_04-hover-b.png"
width="430"> |
| A2_10 | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A2_10-hover-a.png"
width="430"> | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A2_10-hover-b.png"
width="430"> |
| A1_01 | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_01-hover-a.png"
width="430"> | <img
src="https://raw.githubusercontent.com/liquid-java/vscode-liquidjava/pr-screenshots/193/A1_01-hover-b.png"
width="430"> |

**Recommendation: A for the study.**
- **Same text everywhere.** The hover is the one surface the baseline
shares, and the joint review's decision 5 says compared elements must be
in the hover/CLI text. A gives one string: hover = Problems = CLI with
colour off.
- **B splits the message.** It shows the message in two hover parts with
two styles: the headline plain with backticks, the notes styled. The
Problems view also loses the notes.
- **B reads better on its own:** the code is set apart, and the cause is
one click away. It is a good candidate after the study. Moving to it
means taking the notes back out of #190's diagnostic message, so the two
must not both be merged.

## Overlap with open PRs
- **#191:** the base of this PR.
- **#186** may conflict in `sections.ts`: both change the imports at the
top.
- **#189** may conflict in `errors.ts` (imports, and next to the
Expected lines) and in `styles.ts` (separate rules, so unlikely).
- **#185:** probably no textual conflict. It edits `LJErrorDTO` and the
error types in `types/diagnostics.ts`; this PR edits `LJDiagnosticDTO`
and `BaseDiagnostic`.
- **#190:** no conflict in files. It is option A of the hover (above).

## Checks
`npm run test:unit` (126 passed; the new panel test fails on the base),
`npm run test:typecheck`, `npm run lint`, server `mvn -B test` (40
passed, against #533 as 0.0.38; the new converter test fails on the
base).

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

---------

Co-authored-by: Claude Opus 5.5 <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

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.

Say where the wrong state or value comes from (From note), and counterexamples for state errors

Uh oh!

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

1 participant

@CatarinaGamboa
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.