Skip to content

Add division-by-zero verification for Laurel via Core safe operators#510

Merged
keyboardDrummer merged 12 commits intomainfrom
tautschnig/division-by-zero-checks
Mar 4, 2026
Merged

Add division-by-zero verification for Laurel via Core safe operators#510
keyboardDrummer merged 12 commits intomainfrom
tautschnig/division-by-zero-checks

Conversation

@tautschnig
Copy link
Contributor

@tautschnig tautschnig commented Mar 3, 2026

Laurel division and modulo operations (/, %, /t, %t) now fail verification when the divisor may be zero.

This is achieved by translating Laurel's division/modulo to Core's safe operator variants (Int.SafeDiv, Int.SafeMod,
Int.SafeDivT, Int.SafeModT), which carry a built-in y ≠ 0 precondition. The existing PrecondElim transform then
automatically generates the necessary verification conditions at each use site — no Laurel-level instrumentation is
required.

Description of changes:

New Core operators — Int.SafeDivT and Int.SafeModT added to the Lambda IntBoolFactory and Core Factory, mirroring the
existing Int.SafeDiv/Int.SafeMod. SMT encoding and AST-to-CST printing updated accordingly.

Laurel-to-Core translator — Div/Mod/DivT/ModT now emit the safe operator variants instead of the unchecked ones.

End-to-end tests — New DivisionByZeroCheckTest.lean verifies:

  • Safe division with a known non-zero divisor passes ✅
  • Division by an unconstrained parameter fails ❌
  • A pure function with an explicit requires y != 0 precondition passes, but callers that don't satisfy it fail ❌

Co-authored-by: Kiro kiro-agent@users.noreply.github.com

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@tautschnig tautschnig requested a review from a team March 3, 2026 21:08
Copilot AI review requested due to automatic review settings March 3, 2026 21:08
@tautschnig tautschnig requested a review from fabiomadge as a code owner March 3, 2026 21:08
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a Laurel-to-Laurel transformation pass intended to prevent division-by-zero by inserting assert divisor != 0 before division/modulo operations, with a new CLI command to run the pass + verification and a new test suite for the transformation.

Changes:

  • Introduce insertDivisionByZeroChecks Laurel transformation to add non-zero divisor assertions.
  • Add unit + end-to-end tests for the transformation and verification behavior.
  • Add laurelDivCheck CLI command and register it under the “Laurel” command group.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
Strata/Languages/Laurel/DivisionByZeroCheck.lean Implements the div-by-zero assertion insertion pass.
StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean Adds unit tests for AST output and an e2e verification test for unsafe division.
StrataMain.lean Adds and wires a laurelDivCheck command to run the pass and verification.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

tautschnig and others added 4 commits March 3, 2026 21:36
Implement a transformation pass that inserts 'assert divisor != 0'
before every division and modulo operation (Div, Mod, DivT, ModT)
in Laurel programs.

New files:
- Strata/Languages/Laurel/DivisionByZeroCheck.lean: the transformation
- StrataTest/Languages/Laurel/DivisionByZeroCheckTest.lean: tests including
  unit tests verifying the transformed AST structure, and end-to-end tests
  that verify safe division passes and unsafe division is flagged

Modified files:
- StrataMain.lean: add 'laurelDivCheck' command to parse a Laurel file,
  apply the division-by-zero check transformation, and run verification

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
- Remove unused emptyMd and bare definitions (review comment 1)
- In collectDivChecks, only collect checks from the condition of
  IfThenElse and While, not from their branches/body. Branch checks
  are already inserted inside each branch by insertDivChecksStmt,
  avoiding false positives on non-taken branches (review comment 2)
- Add unit test for division inside an if-then-else branch to verify
  the check is inserted inside the branch, not hoisted out (comment 5)

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Instead of hoisting division-by-zero assertions out of quantifiers
(which would reference bound variables out of scope), rewrite:
- forall(x) => body  →  forall(x) => (divisor != 0) ==> body
- exists(x) => body  →  exists(x) => (divisor != 0) && body

Add insertDivChecksExpr for expression-level rewriting and refactor
collectDivChecks to use a shared collectDivConditions helper.
Add unit test for forall with division (review comment 3).

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
For isFunctional procedures, inserting assert statements would break
purity and prevent translation to Core functions. Instead, collect all
division conditions from the body and add them as 'requires' preconditions.
Callers of the function must then prove the divisor is non-zero.

Also rewrite quantifier bodies inside functional procedure bodies using
insertDivChecksExpr.

Add unit test verifying the precondition is generated, and an e2e test
verifying that calling a pure division function with an unconstrained
argument is flagged as an error (review comment 4).

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@tautschnig tautschnig force-pushed the tautschnig/division-by-zero-checks branch from 9d5e5d2 to e87560c Compare March 3, 2026 21:37
@joscoh
Copy link
Contributor

joscoh commented Mar 3, 2026

I haven't looked in detail yet, but is there a reason this does not just translate to the Core safe division/mod operators which automatically do this check?

Remove the Laurel-to-Laurel DivisionByZeroCheck transformation and instead
translate Laurel's division/modulo operations (Div, Mod, DivT, ModT) to
Core's safe operators (Int.SafeDiv, Int.SafeMod, Int.SafeDivT, Int.SafeModT).

These safe operators have built-in preconditions (divisor ≠ 0) that the
PrecondElim transform automatically converts into verification conditions,
eliminating the need for any Laurel-level instrumentation.

Changes:
- Add Int.SafeDivT and Int.SafeModT operators (with preconditions) to the
  Lambda IntBoolFactory and Core Factory, mirroring the existing SafeDiv/SafeMod
- Update SMT encoder and ASTtoCST to handle the new operators
- Change LaurelToCoreTranslator to emit safe operators for all division/modulo
- Remove DivisionByZeroCheck.lean and the laurelDivCheck CLI command
- Adapt DivisionByZeroCheckTest to use the standard Laurel verification pipeline
- Update expected test outputs for factory function listings

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@tautschnig tautschnig changed the title Add Laurel-to-Laurel division-by-zero check transformation Add division-by-zero verification for Laurel via Core safe operators Mar 4, 2026
@tautschnig tautschnig requested a review from Copilot March 4, 2026 10:29
@tautschnig
Copy link
Contributor Author

I haven't looked in detail yet, but is there a reason this does not just translate to the Core safe division/mod operators which automatically do this check?

Thank you, I simply wasn't aware of their existence! Implementation completely reworked in the latest commit, pull-request description updated to reflect what actually happens.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 9 out of 9 changed files in this pull request and generated 2 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

tautschnig and others added 3 commits March 4, 2026 10:44
Add safedivt_expr and safemodt_expr grammar nodes (printing as /t and %t)
so that Int.DivT/Int.SafeDivT and Int.ModT/Int.SafeModT round-trip correctly
through the Core DDM concrete syntax, preserving the truncating-vs-euclidean
distinction.

- Grammar.lean: add safedivt_expr (/t) and safemodt_expr (%t)
- ASTtoCST.lean: map DivT/SafeDivT and ModT/SafeModT to the new nodes
- Translate.lean: add CST→AST mappings and pattern match entries

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
The Laurel pipeline now emits Int.SafeDiv for floor division, which
produces an additional passing verification condition (init_calls_Int.SafeDiv_0)
in the test_arithmetic expected output.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
@keyboardDrummer
Copy link
Contributor

keyboardDrummer commented Mar 4, 2026

Some thoughts on how this connects to source languages.

In Java, dividing an integer by zero throws an ArithmeticException, so there are valid Java programs that divide integers by zero. However, I think this just means we need to wrap Laurel division when encoding Java division in Laurel. It still seems good if Laurel integer division does not allow dividing by zero.

Example (uses some made-up-syntax for exception handling):

// Laurel
function JavaDivision(top: int, bottom: int): int
  throws ArithmeticException on bottom == 0 
{
  if (bottom == 0)
    throw new ArithmeticException();
  else 
    top / bottom
}

keyboardDrummer
keyboardDrummer previously approved these changes Mar 4, 2026
Copy link
Contributor

@keyboardDrummer keyboardDrummer left a comment

Choose a reason for hiding this comment

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

Sweet! Thanks

@tautschnig
Copy link
Contributor Author

In Java, dividing an integer by zero throws an ArithmeticException, so there are valid Java programs that divide integers by zero. However, I think this just means we need to wrap Laurel division when encoding Java division in Laurel.

Yes, I think it would just mean the preconditions of SafeDiv are guaranteed to be upheld.

BTW: the tooling that compiles Java to Laurel may have to enable the passes that create those ArithmeticException runtime checks: there's a variant of the above where a valid Java program even catches that exception and does something non-trivial in that case.

joscoh
joscoh previously approved these changes Mar 4, 2026
tautschnig and others added 2 commits March 4, 2026 18:17
…ator

Attach the Python statement's source range metadata to LocalVariable
nodes and their initializer expressions in the AnnAssign handler.
This ensures that PrecondElim-generated assertions (e.g., division-by-zero
checks from SafeDiv) carry source location info through the pipeline.

Previously, LocalVariable nodes were created with mkStmtExprMd (empty
metadata), so the downstream LiftImperativeExpressions pass—which uses
the initializer expression's metadata for the output LocalVariable—would
produce nodes with no file range. The verifier output then lacked
line/column info for these assertions.

Addresses review comment by andrewmwells-amazon on PR #510.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
…translator

Extend the metadata propagation from the previous commit to cover all
statement types in translateStmt: Assign, self.field assignments,
IfThenElse, While, Return, Assert wrapper blocks, Expr statements,
Try blocks, and For loop blocks.

This ensures that any future diagnostics generated from these statement
types will carry the Python source location through the pipeline.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Previously Int.DivT and Int.SafeDivT both mapped to safedivt_expr in
the AST-to-CST printer, losing the safe/unsafe distinction on round-trip.
Same for Int.ModT vs Int.SafeModT.

Add divt_expr (prints as 'divt') and modt_expr (prints as 'modt') CST
nodes, mirroring the existing div_expr/safediv_expr pattern. Update the
CST-to-AST translator and isArithOp match accordingly.

Addresses review comment by joehendrix on PR #510.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
joehendrix
joehendrix previously approved these changes Mar 4, 2026
The previous commit used 'divt' and 'modt' as infix keywords, which
conflicts with user-defined identifiers in Boogie programs (e.g., the
DivMod.bpl test defines functions named divt and modt). Switch to
function-call syntax Int.DivT(a, b) and Int.ModT(a, b) instead, which
avoids introducing new keywords into the grammar.

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Copy link
Contributor

@joehendrix joehendrix left a comment

Choose a reason for hiding this comment

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

Reapproved after reviewing the latest change — function-call syntax for unchecked divt/modt CST nodes looks good.

@keyboardDrummer keyboardDrummer added this pull request to the merge queue Mar 4, 2026
Merged via the queue into main with commit 3a4a6c7 Mar 4, 2026
15 checks passed
@keyboardDrummer keyboardDrummer deleted the tautschnig/division-by-zero-checks branch March 4, 2026 21:13
kondylidou pushed a commit to abdoo8080/Strata that referenced this pull request Mar 5, 2026
…trata-org#510)

Laurel division and modulo operations (/, %, /t, %t) now fail
verification when the divisor may be zero.

This is achieved by translating Laurel's division/modulo to Core's safe
operator variants (Int.SafeDiv, Int.SafeMod,
Int.SafeDivT, Int.SafeModT), which carry a built-in y ≠ 0 precondition.
The existing PrecondElim transform then
automatically generates the necessary verification conditions at each
use site — no Laurel-level instrumentation is
required.

*Description of changes:*

New Core operators — Int.SafeDivT and Int.SafeModT added to the Lambda
IntBoolFactory and Core Factory, mirroring the
existing Int.SafeDiv/Int.SafeMod. SMT encoding and AST-to-CST printing
updated accordingly.

Laurel-to-Core translator — Div/Mod/DivT/ModT now emit the safe operator
variants instead of the unchecked ones.

End-to-end tests — New DivisionByZeroCheckTest.lean verifies:
- Safe division with a known non-zero divisor passes ✅
- Division by an unconstrained parameter fails ❌
- A pure function with an explicit requires y != 0 precondition passes,
but callers that don't satisfy it fail ❌

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>

By submitting this pull request, I confirm that you can use, modify,
copy, and redistribute this contribution, under the terms of your
choice.

---------

Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
tautschnig added a commit that referenced this pull request Mar 5, 2026
PR #510 added safe truncating division/modulo operators with
division-by-zero preconditions. Map them to the same GOTO operators
as their non-safe counterparts (Int.DivT/Int.ModT), since the
precondition check is handled separately by the Laurel-to-Core
translation.
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.

6 participants