Skip to content

Per-invariant source ranges for loop-invariant diagnostics#1361

Merged
olivier-aws merged 7 commits into
strata-org:reviewed-kbd-will-merge-to-mainfrom
kadirayk:feat/invariant-sourcerange-main2
Jun 16, 2026
Merged

Per-invariant source ranges for loop-invariant diagnostics#1361
olivier-aws merged 7 commits into
strata-org:reviewed-kbd-will-merge-to-mainfrom
kadirayk:feat/invariant-sourcerange-main2

Conversation

@kadirayk

Copy link
Copy Markdown

Summary

Loop-invariant verification diagnostics (a failing invariant(...) in a while/for loop) 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 LoopElim and were tagged with the loop-wide metadata md. The per-invariant source range was lost earlier in the pipeline: Core loop invariants are bare (label, expr) pairs and Core expressions are Unit-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 MetaData array and use it per-invariant in LoopElim. 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 metadata md, so loops not originating from Laurel (Core .st, C_Simp) are unchanged.

Testing

StrataTest/.../Fundamentals/T13_WhileLoopsError.lean adds two caret-annotated regression tests using the existing diagnostic harness (TestDiagnostics, matchesDiagnostic), which checks the exact start/end line and column of each diagnostic:

  1. badInitialInvariant — a single invariant i >= 0 that fails on entry. The caret asserts the diagnostic lands on the invariant expression, not the while loop.
  2. 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_msgs tests pass, including the new ones.
  • The fallback to the loop md keeps existing Core .st and C_Simp loop diagnostics unchanged.

kadirayk added 2 commits June 11, 2026 16:43
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.

@fabiomadge fabiomadge left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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/getRelatedFileRanges pattern, 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 processLaurelFile

Good to merge once that's in. Remaining points inline.

Comment thread Strata/Transform/LoopElim.lean Outdated
Comment thread Strata/Transform/LoopElim.lean Outdated
Comment thread Strata/Languages/Laurel/LaurelAST.lean
Comment thread Strata/DL/Imperative/MetaData.lean Outdated

@fabiomadge fabiomadge left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

(duplicate of the review above — posted twice by mistake; see the other review for the full content)

@kadirayk kadirayk changed the base branch from main2 to reviewed-kbd-will-merge-to-main June 12, 2026 16:08
@kadirayk kadirayk requested a review from fabiomadge June 12, 2026 16:09

@fabiomadge fabiomadge left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

@aqjune-aws aqjune-aws left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Great patch :)

@olivier-aws olivier-aws enabled auto-merge June 15, 2026 21:37
@olivier-aws olivier-aws added this pull request to the merge queue Jun 16, 2026
Merged via the queue into strata-org:reviewed-kbd-will-merge-to-main with commit 24b8f04 Jun 16, 2026
14 checks passed
fabiomadge pushed a commit to strata-org/jverify that referenced this pull request Jun 17, 2026
… 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>
@kadirayk kadirayk deleted the feat/invariant-sourcerange-main2 branch June 19, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants