Repository navigation
Origin note: say where the wrong state of a receiver comes from - #528
CatarinaGamboa wants to merge 8 commits into
Conversation
Clean-code reviewI audited the diff with the clean-code-principles checklist. Fixed, while writing it:
Declined:
|
Codex review, round 1
Soundness prompt. It found three programs with misleading notes:
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":
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:
New test: Tests: 733 run, 0 failures. |
Codex review, round 2
Soundness prompt. It found five programs, all
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":
Checks:
Tests: 733 run, 0 failures. |
Codex review, round 3
Soundness prompt. It found three misleading notes:
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 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. 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 Checks:
Tests: 733 run, 0 failures. |
Codex review, round 4
Soundness prompt. It found three programs, all with a note where the alias rule asks for none:
Fixed (1) and all three programs in "Apply the alias checks to this and to the failing call's own arguments":
All three programs now print no note. Declined (2): it does not happen. Tests: 733 run, 0 failures. |
Codex review, round 5 (last)Soundness prompt. A bound method reference keeps the receiver: 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.
This is round 4's point again. Declined, as there: Tests: 733 run, 0 failures. This is the fifth round, the agreed limit. Each round found narrower cases, from aliases to arguments, |
"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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 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 <noreply@anthropic.com>
4757b3b to
51666ee
Compare
Origin rename (PM review), and Codex review of itWhat changed:
Codex review (one round, as asked):
Tests: 736 run, 0 failures. |
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 tomainbefore 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.Origin: line 134, new BufferedInputStream(stream) makes markState(buffered) == unmarkedOrigin: line 130, writer.getDefaultWriteParam() returns compression(wParam) == compressionNotExplicitOrigin: line 69, frame.getUndoManager() says nothing about redoState(undoManager)Origin: line L, new T(…) creates x with <state>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:
thisis 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 areturn/throw.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
fromchecks. 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 whosetonames a shown set. The note is printed only if:StateChange, so the walk crosses no join, loop head or unknown origin;tokeeps the sets it does not name, throughset(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:
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
StateChangethat holds the call result it was assigned. Instances now record aCallResultwhen a local is declared or assigned from a call to a refined method. A state-preserving call keeps it, as it keeps theStateChange. 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:to(asto = "running(_)");_, and uses no alias. This rules out_ == this-style returns;state1), which the message would otherwise never show.DebugLog check for A1_38 (asked before coding F1)
With
-d, the VC atbuffered.reset()(line 157) has a singlebufferedinstance,#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 aStateChangeon that instance. So neither loop (lines 136 and 150) creates a join forbuffered.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) == unmarkedState Refinement Error: found compression(wParam) == compressionNotExplicit but expected compression(wParam) == compressionExplicit || compression(wParam) == compressionTypeSet + Origin: line 130, writer.getDefaultWriteParam() returns compression(wParam) == compressionNotExplicitState Refinement Error: expected redoState(undoManager) == redoable but could not prove it + Origin: line 69, frame.getUndoManager() says nothing about redoState(undoManager)new JFrame(…)gives.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,
thisin 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
AuxStateHandlerdeclared-here): the onethrowStateRefinementErrorcall inchangeState, which gets amessageNotesargument. The rest is additive.StateHints): none.throwStateRefinementError, a one-line rebase.🤖 Generated with Claude Code