Skip to content

Add break/continue/labeled statements/do-while support#418

Open
olivier-aws wants to merge 10 commits into
mainfrom
feat-break-continue-label
Open

Add break/continue/labeled statements/do-while support#418
olivier-aws wants to merge 10 commits into
mainfrom
feat-break-continue-label

Conversation

@olivier-aws
Copy link
Copy Markdown
Contributor

@olivier-aws olivier-aws commented May 20, 2026

Resolves #407 (break/continue/labeled/do-while portion; decreases deferred).

Design

  • Label stack for break/continue resolution
  • Only wrap loops in labelledBlock when break/continue is present (tree scan)
  • When break/continue present, desugar for-loops to while manually matching PythonToLaurel pattern
  • do-while desugared via sentinel variable
  • Labeled statements pass label to next loop via pendingLabel

Translation examples

Simple for loop (no break/continue) — uses forLoop IR node directly

for (int i = 0; i < 5; i = i + 1) {
    invariant(i >= 0 && i < 5);
    sum = sum + 1;
}

forLoop(
  var i: int32 = 0,
  i < 5,
  i = i + 1,
  [invariant(i >= 0 && i < 5)],
  { sum = sum + 1 }
)

For loop with break — desugars to while with labelled blocks

for (int i = 0; i < 10; i = i + 1) {
    invariant(i >= 0 && i <= 5);
    if (i == 5) { break; }
}

labelledBlock(loop_break_0, {
  var i: int32 = 0;
  while (i < 10)
    invariants: [i >= 0 && i <= 5]
  {
    labelledBlock(loop_continue_1, {
      if (i == 5) { exit loop_break_0; }
    });
    i = i + 1;
  }
})

For loop with continue

for (int i = 0; i < 5; i = i + 1) {
    if (i == 2) { continue; }
    x = x + i;
}

labelledBlock(loop_break_0, {
  var i: int32 = 0;
  while (i < 5)
    invariants: [...]
  {
    labelledBlock(loop_continue_1, {
      if (i == 2) { exit loop_continue_1; }
      x = x + i;
    });
    i = i + 1;
  }
})

Note: continue exits the inner labelled block (skipping the rest of the body) but the step i = i + 1 still executes because it is outside the continue-labelled block but inside the while body.

While loop with break

while (x < 10) {
    if (x == 5) { break; }
    x = x + 1;
}

labelledBlock(loop_break_0, {
  while (x < 10) {
    labelledBlock(loop_continue_1, {
      if (x == 5) { exit loop_break_0; }
      x = x + 1;
    });
  }
})

Do-while loop — desugared via sentinel

do {
    invariant(x >= 0 && x <= 5);
    x = x + 1;
} while (x < 5);

var __first_0: bool = true;
while (__first_0 || x < 5)
  invariants: [x >= 0 && x <= 5]
{
  __first_0 = false;
  x = x + 1;
}

Test plan

New VerifyBreakContinue test with 8 methods: for-break, while-break, infinite-while-break, nested-break (inner only), for-continue (i==2 and i>=2), while-continue (i==2 and i>=2). All existing tests pass.

fabiomadge and others added 10 commits May 19, 2026 22:30
Bump the Strata submodule from 8593f7cf to 53a3df53 (latest main) and
regenerate Laurel Java classes.

Schema-affecting Strata commits:
- strata-org/Strata#1076 — disallow transparent procedures (require
  `opaque` for procedures with visible bodies)
- strata-org/Strata#969 — add `opaque` keyword to Laurel grammar; ensures
  and modifies are now wrapped in an optional OpaqueSpec
- strata-org/Strata#1031 — add `modifies *` wildcard (new ModifiesWildcard
  variant; unused here)
- strata-org/Strata#1077 — generalized multi-target assign (new
  MultiAssign + AssignTarget AST nodes; unused here)
- strata-org/Strata#1140 — Provenance type / metadata migration

Hand-written changes:
- JavaToLaurelCompiler: emit OpaqueSpec for impure procedures (always)
  and for pure functions with non-empty ensures.
- LaurelDriver: pass `--solver z3` explicitly. The previous Strata
  hardcoded `solver := "z3"` for the Laurel pipeline; the bumped Strata
  removed that override and now defaults to cvc5 (which our CI doesn't
  install). Passing `--solver z3` preserves prior behavior.

All other diffs in verifier/src/main/java/org/strata/jverify/laurel/ are
generated by `make generate-laurel-ast`.

Note: pure functions that declare @ensures used to be emitted as
Transparent and are now emitted as Opaque (forced by the schema). No
existing test combines @pure with @ensures, so nothing in the suite
regresses; callers of such functions in the future will reason via the
postcondition rather than by definitional unfolding.
Add convertStatement cases for JCBreak, JCContinue, JCLabeledStatement,
JCDoWhileLoop, and JCSkip in JavaToLaurelCompiler, resolving issue #407
(excluding decreases clause which is deferred to Strata-side work).

Design:
- Label stack with (javaLabel, breakLabel, continueLabel) tuples pushed
  on each loop/labeled-statement entry, popped on exit
- Only wrap loops in labelledBlock when break/continue is actually
  present in the loop body (detected via tree scan)
- When break/continue is present, desugar for-loops to while manually
  (not using the forLoop IR node) to produce the correct structure:
  labelledBlock(breakLbl, { init; while(cond) { labelledBlock(continueLbl, body); step } })
  This matches the PythonToLaurel pattern and allows Strata to verify
  invariants correctly across continue paths.
- break/continue emit as exit(label) to the resolved target
- do-while desugared via sentinel variable
- Labeled statements pass their label to the next loop via pendingLabel

Add VerifyBreakContinue test exercising for-break, while-break,
infinite-while-break, nested-break, for-continue, and while-continue.

Fixes #407 (break/continue/labeled/do-while portion).
Extract the shared loop emission logic (labelled block wrapping,
while node creation, preamble handling) into a single emitLoop()
method used by while, for, and do-while cases.

Parameters:
- cond, invariants, loopBody: the core loop components
- step: optional (non-null for for-loops, null for while/do-while)
- preamble: statements before the while (init for for, sentinel for do-while)
- needsLabels, breakLbl, continueLbl: label wrapping control

No behavioral change.
extractLoopParts now accepts JCStatement directly instead of requiring
a JCBlock, internalizing the if-block-then-extract-else-just-convert
pattern that was duplicated in all three loop cases.
Two sequential for-loops with continue in the same method, each
generating distinct labelled block labels (loop_break_N, loop_continue_N)
to confirm no label conflicts.
Exercises both exit targets (continue skips i==3, break exits at i==7)
within a single for-loop, verifying x == 0+1+2+4+5+6 == 18.
Remove whileWithBreak, whileWithContinue, forLoopBreak, forLoopContinue,
nestedForLoopBreak, and twoForLoops from VerifyStatements. These are now
covered by VerifyBreakContinue which uses static methods that actually
exercise the Laurel compiler code paths. The removed tests were instance
methods that never went through the compiler (due to the Flags.STATIC
gate), making them effectively no-ops.

Kept in VerifyStatements:
- nestedForLoopContinue: uses labeled 'continue outerLoop' (unique)
- doWhileLoop: uses 'decreases' clause (unique)
- forLoop, nestedForLoop: test loops without break/continue
- skip, nativeAssert, P/P2/P3, underscoreVariableName,
  methodWithResult, ignoreCallResult: unrelated to break/continue
@olivier-aws olivier-aws marked this pull request as ready for review May 20, 2026 18:57
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.

Laurel compiler: break, continue, labeled statements, and decreases clauses

2 participants