Per-invariant source ranges for loop-invariant diagnostics#1361
Conversation
Loop-invariant verification diagnostics previously pointed at the whole loop because LoopElim tagged synthesized invariant obligations with the loop-wide metadata, and Core loop invariants (bare (label, expr) pairs over Unit-annotated expressions) carried no per-invariant source range. Thread each invariant's source range through the loop's MetaData array and use it per-invariant in LoopElim, falling back to the loop metadata when absent so non-Laurel loops (Core .st, C_Simp) are unchanged. - MetaData: add invariantProvenanceLabel, pushInvariantProvenance, getInvariantProvenances. - LaurelAST: add fileRangeToProvenance. - LaurelToCoreTranslator: append each invariant's provenance to the loop metadata in invariant order during .While lowering. - LoopElim: add invProvs/invMd selector; entry/maintain invariant asserts and matching assumes use the per-invariant metadata. - Add T13_WhileLoopsError regression test pinning invariant diagnostics to the specific failing invariant's source range.
There was a problem hiding this comment.
LGTM, non-blocking comments (leaving as a comment, not a formal approval). Clean, well-scoped fix for the LoopElim whole-loop-range issue (jverify#437). Built and verified: build green, loop suite green (T13/T17/Core Loops), VCs unchanged — each invariant's expr, label, and property-type are untouched; only the per-invariant MetaData differs, and metadata drives diagnostics, not the VC.
Design is the right tradeoff. The Core loop IR stores invariants as (String × Expr) with no per-invariant metadata slot, and Core exprs are Unit-annotated. Threading provenances through the loop md is the correct localized choice (4 files, no IR/proof/backend churn):
- Metadata on the invariant tuple is cleaner but changes
Stmt.loop+ its induction principle → ripples into semantics proofs and the CBMC backend. Too invasive. - Metadata on Core exprs needs a slot the
Unit-annotated type lacks — the "front-end-only fix doesn't help" point, confirmed. - It mirrors the existing
relatedFileRange/getRelatedFileRangespattern, so it's house idiom.
Main ask: add a for-loop test. For-loops work (verified: a failing 2nd for-loop invariant lands on the invariant expr, since for desugars to the same .While), but only while-loops are tested. Verified drop-in for T13_WhileLoopsError.lean:
def forSecondInvFailsProgram := r"
procedure forSecondInvFails()
opaque
{
var j: int := -1;
for(var i: int := 0; i < 10; i := i + 1)
invariant i >= 0
invariant j >= 0
// ^^^^^^ error: assertion does not hold
{
j := j + 1
}
};
"
#guard_msgs (drop info, error) in
#eval testInputWithOffset "ForSecondInvFails" forSecondInvFailsProgram 60 processLaurelFileGood to merge once that's in. Remaining points inline.
fabiomadge
left a comment
There was a problem hiding this comment.
Thanks for the follow-ups — all four points from my earlier comments are addressed: the .loc gating (so a sourceless invariant falls back to the loop range instead of losing it), the fileRangeToCoreMd/fileRangeToProvenance de-dup, the documented invProvs[i] contract with graceful fallback, and the MetaData.getInvariantProvenances naming. The added for-loop test is a nice extra. Built T13_WhileLoopsError locally and the per-invariant caret annotations all match. LGTM.
24b8f04
… do/while bad invariant tests (#453) ### What was changed? Bumped the Strata submodule from `e115b8ec` to `24b8f04e` (nightly, "Per-invariant source ranges for loop-invariant diagnostics", [#1361](strata-org/Strata#1361)) and brought JVerify in line with the updated Laurel dialect. - **Strata submodule**: updated pointer to `24b8f04e`. - **`VerifyDoWhile` test**: updated the expected diagnostic for `doWhileBadInitialInvariant` to the new, tighter per-invariant range (points at the invariant condition rather than the whole do-while statement); added a negative regression case `whileBadInvariant` (invariant holds on entry but is not preserved by the loop body) with a matching diagnostic marker and doc comment; updated the `@JVerifyTest` counts to enforce the new expectations (`methodsInvalid = 3, errorCount = 3`). This is not a user-visible behavior change for JVerify itself; it tracks the upstream Strata improvement to invariant diagnostic source ranges. ### How has this been tested? - `./gradlew build` passes (compile + full test suite) - `./gradlew :verifier:test --tests "*VerifyDoWhile*" --rerun-tasks` passes, exercising the updated and newly added invariant diagnostic cases. Fixes #437 <small>By submitting this pull request, I confirm that my contribution is made under the terms of the [Apache-2.0](https://github.com/strata-org/jverify?tab=Apache-2.0-1-ov-file#readme).</small>
Summary
Loop-invariant verification diagnostics (a failing
invariant(...)in awhile/forloop) previously pointed at the whole loop instead of the specific invariant that failed. This change threads each invariant's source range through the loop's metadata so loop elimination can attribute each invariant's verification condition to that invariant's own source location.This support is required for JVerify#437
Problem
In Strata, loop-invariant proof obligations are synthesized by
LoopElimand were tagged with the loop-wide metadatamd. The per-invariant source range was lost earlier in the pipeline: Core loop invariants are bare(label, expr)pairs and Core expressions areUnit-annotated, so they carry no source range.A front-end-only change does not help: the diagnostic location is driven by the synthesized assert's metadata, not by anything attached to the invariant expression. The fix therefore has to live in Strata.
Solution
Thread each invariant's source range through the loop's existing
MetaDataarray and use it per-invariant inLoopElim. When a per-invariant provenance is present, each invariant's generated assert/assume is attributed to that invariant's own source location; otherwise we fall back to the loop metadatamd, so loops not originating from Laurel (Core.st, C_Simp) are unchanged.Testing
StrataTest/.../Fundamentals/T13_WhileLoopsError.leanadds two caret-annotated regression tests using the existing diagnostic harness (TestDiagnostics,matchesDiagnostic), which checks the exact start/end line and column of each diagnostic:badInitialInvariant— a singleinvariant i >= 0that fails on entry. The caret asserts the diagnostic lands on the invariant expression, not thewhileloop.secondInvariantFails— two invariants where the first holds on entry but the second (invariant j >= 0) does not. The caret asserts the diagnostic points specifically at the failing second invariant. This is the case that distinguishes per-invariant attribution from loop-wide attribution: before the fix the range would resolve to the loop, so the carets would not match.Verification:
lake build Strata— compiles, no proof breakage.lake build StrataTest— all#guard_msgstests pass, including the new ones.mdkeeps existing Core.stand C_Simp loop diagnostics unchanged.