Skip to content

Use found … but expected … for refinement errors, drop the general hint, declare every from - #525

Merged
CatarinaGamboa merged 2 commits into
mainfrom
fix/found-expected-all-froms
Nov 10, 2026
Merged

CatarinaGamboa merged 2 commits into
mainfrom
fix/found-expected-all-froms

Conversation

@CatarinaGamboa

@CatarinaGamboa CatarinaGamboa commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #520
Closes #521

What and why

  1. One headline frame (One headline frame: found … but expected … for refinement errors #520). Refinement errors used X is not a subtype of Y, while state errors use found X but expected Y. Refinement errors now use found %s but expected %s too (RefinementError). An empty Found keeps expected Y but could not prove it.
  2. No general UNKNOWN hint (Drop the general UNKNOWN hint and show every from in declared-here #521, part 1). "Not enough information to prove the expected refinement. Add a refinement or condition to constrain it." is gone. Both fixes it suggests are graded wrong in the study (A2_04).
  3. Every from in declared-here (Drop the general UNKNOWN hint and show every from in declared-here #521, part 2). When Expected is the disjunction of several @StateRefinement entry states, the "Refinement declared here" excerpt marks every from, and each one gets its where … means … line. Before, AuxStateHandler took a findFirst().
    • StateRefinementError now carries the list of declaration positions. LJDiagnostic.getDeclarationPositions() returns it, and getDeclarationPosition() still returns the first one, so the IDE's Locations and State Machine are unchanged.
    • Fixing this exposed a position bug: each from was found by its text in any LiquidJava annotation of the method. In A1_01, line 111's from = "compressionTypeSet(this)" was placed at line 110's to = "compressionTypeSet(this)". from/to positions now come from the annotation's own value, and the text search is only a fallback.

Soundness: no message claims anything new. The excerpt and where lines quote the spec, and the Expected they explain is already the disjunction of these same froms.

Study tasks: before (main) → after

Each task still gives exactly one error, on its reportedLine. A1_38, A1_56 and A1_09 are unchanged.

A2_10

- Refinement Error: failures.size() == size(this.failures) && failures.size() >= 0 is not a subtype of failures.size() > 0
+ Refinement Error: found failures.size() == size(this.failures) && failures.size() >= 0 but expected failures.size() > 0

practice

- Refinement Error: toPay == price - discount is not a subtype of toPay >= 0
+ Refinement Error: found toPay == price - discount but expected toPay >= 0

practice2

- Refinement Error: delay == atMinute - nowMinute is not a subtype of delay >= 0
+ Refinement Error: found delay == atMinute - nowMinute but expected delay >= 0

A2_04: the headline is unchanged (expected timeout >= 0 but could not prove it), and the hint block is gone:

- Hints:
-   - Not enough information to prove the expected refinement. Add a refinement or condition to constrain it.

A1_01: declared-here, after:

--> Refinement declared here:
109 |      */
110 |     @StateRefinement(from = "compressionExplicit(this)", to = "compressionTypeSet(this)")
    |                              ^^^^^^^^^^^^^^^^^^^^^^^^^
111 |     @StateRefinement(from = "compressionTypeSet(this)", to = "compressionTypeSet(this)")
    |                              ^^^^^^^^^^^^^^^^^^^^^^^^
112 |     void setCompressionType(String compressionType);
  where compressionExplicit(this) means compression(this) == compressionExplicit
  where compressionTypeSet(this) means compression(this) == compressionTypeSet

Before, only line 110 was marked, and only its where line was shown.

Tests

  • Updated the 6 expectations that assert is not a subtype of (TestFoundBinders, TestStaticConstantNames, RefinementErrorCounterexampleTest).
  • RefinementErrorCounterexampleTest.messageWithNothingFoundGivesNoHint: an empty Found gives no hint.
  • TestDeclarationPositions.stateErrorPointsToEveryEntryStateWithWhatItMeans with the new fixture ErrorStateRefinementEveryFrom.java, a named set with two froms where the second from repeats the first annotation's to. It checks both positions and both where lines.
  • Each new or changed test fails on main. mvn test: 731 tests, 0 failures.

Study env: re-run after release (not edited here)

  • test/verify-tasks.py → test/out/verify-new.json: the practice, practice2 and A2_10 headlines, and the A2_04 hint.
  • The recorded CLI outputs of practice and practice2, and of A2_10, A2_04 and A1_01.
  • Tour screenshots (test/e2e.mjs): test/out/B-01…B-07-tour-step*.png (the tour runs on the practice file, whose headline changes).
  • Practice hover/repair screenshots: B-02-practice2-hover.png, B-11-practice2-hover.png, B-01/B-10-practice2-repair.png, and the block-1 practice equivalents.
  • A2_10: A-run2/A-03-A2_10-hover.png, A-run2/A-04-A2_10-liquidjava-view.png (headline).
  • A2_04: A-19-A2_04-hover.png, A-20-A2_04-liquidjava-view.png (hint removed).
  • A1_01: the hover and panel are unchanged. The IDE gets only the first declaration position, so showing both froms in the panel needs an extension change (it can read getDeclarationPositions()).

🤖 Generated with Claude Code

…y entry state

Refinement errors now use the same headline frame as state errors, found X but
expected Y, instead of "X is not a subtype of Y". An empty found keeps
"expected Y but could not prove it", without the general hint to add a
refinement or condition, which pointed away from the fix.

When a call has several @StateRefinement entry states, the declared-here
excerpt marks every from that makes up expected, each with its where line.
Each from is located in its own annotation, not at the first annotation that
writes the same text.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CatarinaGamboa

Copy link
Copy Markdown
Collaborator Author

Clean-code review

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

Fixed:

  • TestDeclarationPositions — [core-dry] the new test computed the offset of a from value twice, inline. It is now one fromStart(source, from) helper. The existing position assertions are reused through assertPosition, which was split out of assertPredicatePosition.
  • AuxStateHandler.getStates — [core-dry] from and to positions share one valuePosition helper: the annotation's own value first, the text search as fallback.

Declined:

  • LJDiagnostic.getSnippet — [func-small] the marker block is now one loop deeper. It could move into its own method, but that method would take eight parameters (func-few-arguments), and the PR should stay small. The loop keeps the code as it was, just repeated for each position.
  • StateRefinementError — [solid-isp] declaration positions and notes stay as two parallel lists, not one Declaration record. A record would change more signatures (VCChecker, TypeChecker) that other open work touches. The notes are not one per position anyway: a from that writes no named-set state has no where line.
  • TypeChecker.throwStateRefinementError short overload — [core-fail-fast] it wraps its single position in List.of. Its only caller passes a type position, which is never null.

@CatarinaGamboa

Copy link
Copy Markdown
Collaborator Author

Codex review, round 1

  • [P2] Handle concatenated state annotations when locating values (AuxStateHandler.java:438-439): for a value such as from = "open(" + "this)", getAnnotationValuePosition gets the whole binary expression and strips its first and last characters as if it were one quoted literal, so the highlight covers expression syntax. Use the literal's own position only for a single string literal, and fall back otherwise.

A concatenated value falls back to the text search, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@CatarinaGamboa

Copy link
Copy Markdown
Collaborator Author

Round 1 reply: fixed. valuePosition now uses the annotation's own position only when the value is a CtLiteral. A concatenated value falls back to the text search, which is what main does (main's search never matches a concatenation either, so it ends up at the element position). No test added: concatenated state values are rare, and the fallback is the unchanged main path. Suite: 731 tests, 0 failures.

@CatarinaGamboa

Copy link
Copy Markdown
Collaborator Author

Codex review, round 2

No findings: "I found no actionable regressions in the diff. The focused declaration-position and diagnostic tests pass." The loop ends here.

@CatarinaGamboa
CatarinaGamboa merged commit 98fa854 into main Nov 10, 2026
1 check passed
CatarinaGamboa added a commit that referenced this pull request Nov 11, 2026
…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 <noreply@anthropic.com>
CatarinaGamboa added a commit to liquid-java/vscode-liquidjava that referenced this pull request Nov 11, 2026
The server sends the verifier's declaration positions and declaration notes
(the "where ... means ..." lines) of refinement and state errors, and the
Locations section shows a link to every position, with the notes under them.

Needs liquidjava-verifier 0.0.38: getDeclarationNotes() comes from
liquid-java/liquidjava#517 and getDeclarationPositions() from
liquid-java/liquidjava#525.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CatarinaGamboa added a commit to liquid-java/vscode-liquidjava that referenced this pull request Nov 11, 2026
Closes #181

## What changes
- **Server.** `RefinementErrorDTO` and `StateRefinementErrorDTO` now
send:
- `declarationPositions`, from `LJDiagnostic.getDeclarationPositions()`;
- `declarationNotes`, the `where … means …` lines, from
`getDeclarationNotes()`.

Both are read through the verifier's generic `LJDiagnostic` API. So when
the verifier adds notes or positions, they appear with no extension
change. `declarationPosition`, the first position, stays as it is for
the State Machine view.
- **Client.** The Locations section links to **every** declaration
position, then shows the notes under the links, muted and with a hanging
indent. A declaration in value form (`doorState(this) == closed`), or
one with unnamed sets, has no notes, so nothing changes for it.
- **Fallback to one position.** The verifier's default
`getDeclarationPositions()` returns the single
`getDeclarationPosition()`. Refinement errors and any other error with
one declaration therefore show one link, as before.

## Verifier version: needs 0.0.38 (not yet released)
| Part | Verifier change |
|---|---|
| `declarationNotes` (the gloss) | liquid-java/liquidjava#517 (merged in
main) |
| `declarationPositions` (every `from`, e.g. A1_01 lines 110 and 111) |
liquid-java/liquidjava#525 (**open**) |

The pom is bumped to `0.0.38`, so **CI's server build fails until 0.0.38
is on Maven Central**. Please merge this PR only after a verifier
release that has both #517 and #525. If #525 is not merged, drop the
`declarationPositions` lines from the two DTOs (one line each) and use
`declarationPosition` in `renderLocation` again. The gloss part only
needs #517.

I tested it locally against #525's head (`9d1bb1e`, which contains
main), installed as 0.0.38 in an isolated Maven repository.

## Before → after (Locations section, at the study width)
Before is main, with its server built on verifier main. After is this PR
on #525's head. Headless VS Code, 1600×900, Dark Modern, default side
bar.

**A1_01**: both `from`s, each with its gloss
```diff
  E_OSS_A1_01_SlideRecoder.java:136
  ImageWriteParamRefinements.java:110
+ ImageWriteParamRefinements.java:111
+ where compressionExplicit(this) means compression(this) == compressionExplicit
+ where compressionTypeSet(this) means compression(this) == compressionTypeSet
```
**A1_38**
```diff
  E_OSS_A1_38_FormatReaderManager.java:157
  BufferedInputStreamRefinements.java:81
+ where marked(this) means markState(this) == marked, open(this) means stream(this) == open
```
**A1_56**: `+ where redoable(this) means redoState(this) == redoable`
**A1_09**: `+ where notDisplayable(this) means display(this) ==
notDisplayable`
**A2_04**: unchanged. `@Refinement("_ >= 0")` has no gloss. The missing
Hint in the after shot comes from #525, not from this PR.

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

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

<details><summary>A2_04</summary>

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

The A1_01 and A1_09 shots are scrolled to Locations. The yellow outline
is the focus left on Hints by the screenshot script.

## Overlap with open PRs
- **#186 (Context tab)** may conflict in
`client/src/webview/views/sections.ts`. It changes
`renderVariableHighlightButton` and the imports, right after
`renderLocation`, which this PR edits.
- **#185 (Found as facts)** may conflict in
`client/src/types/diagnostics.ts`. Both PRs add fields to
`RefinementError` and `StateRefinementError`, in nearby hunks. No
conflict in logic.
- **#189 (panel wrapping)**: the notes are in the Locations `<pre>`,
which keeps `break-word`, so a long `compressionExplicit(this)` can
still wrap inside the name at 240 px (see A1_01). They do not share
files apart from `styles.ts`, in separate rules.

## Checks
`npm run test:unit` (125 passed; the new test fails on main and also
checks the copied text), `npm run test:typecheck`, `npm run lint`,
server `mvn -B test` (39 passed, against #525 as 0.0.38; the converter
test now also checks the notes and the positions).

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

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Drop the general UNKNOWN hint and show every from in declared-here One headline frame: found … but expected … for refinement errors

1 participant