Skip to content

Enable calling procedures in contracts#28

Open
keyboardDrummer wants to merge 136 commits into
transparency-pass-onlyfrom
issue-924-contract-and-proof-pass
Open

Enable calling procedures in contracts#28
keyboardDrummer wants to merge 136 commits into
transparency-pass-onlyfrom
issue-924-contract-and-proof-pass

Conversation

@keyboardDrummer

@keyboardDrummer keyboardDrummer commented Apr 23, 2026

Copy link
Copy Markdown
Owner

Builds on:

Functional changes

  1. [Debugging] Improvement the printing of Laurel if-then-else expressions
  2. EliminateReturnsInExpression now runs for procedures as well
  3. Refactoring of how Laurel function bodies are handled. The body is now immediately wrapped in a return statement during parsing.
  4. Allow calling procedures from contracts - making them strictly more powerful than functions
  5. Let the transparency pass rewrite the bodies of assume statements so they don't assert anything.
  6. Improve diagnostics related to contracts, using the correct verbiage "precondition" and "postcondition" instead of "assertion"

Implementation

Add these passes:

  • [New] EliminateReturnStatements: rewrite return to exit statements, needed for the next pass.
  • [New] ContractPass: translate away pre and postconditions entirely by introducing assertion and assumptions at call sites and at procedure starts and ends
  • [Updated] Lift assertions, assumptions and procedure calls when they occur in expressions.

Follow-up work

  • Remove the now obsolete functions from Laurel
  • Create WF proofs for quantifier bodies
  • Lift assumptions in expressions to axioms.
  • In the transparency phase, if something has no asserts and only calls functions, only create a function and no procedure

@keyboardDrummer keyboardDrummer changed the title Issue 924 contract and proof pass Add contract and transparency pass Apr 23, 2026
@github-actions github-actions Bot added Git conflicts PR has merge conflicts with the base branch SMT GOTO dependencies github_actions and removed Git conflicts PR has merge conflicts with the base branch labels Apr 23, 2026
@keyboardDrummer-bot
keyboardDrummer-bot force-pushed the disallow-transparent-statement-bodies branch from 3a0f094 to df6ccdc Compare April 23, 2026 14:37
@keyboardDrummer
keyboardDrummer force-pushed the disallow-transparent-statement-bodies branch from 2e860cf to e3fe027 Compare April 29, 2026 08:44
keyboardDrummer-bot added a commit that referenced this pull request May 1, 2026
Merges PR #31 (strata-org PRs strata-org#1076 and strata-org#1077) into this branch.

Key conflict resolution decisions:
- Removed AstNode.md field (from PR #28) in favor of PR #31's source-only approach
- Adapted ContractPass, TransparencyPass, EliminateReturnStatements to use 2-field AstNode
- Used diagnosticFromSource/identifierToCoreMd helpers from PR #31
- Kept PR #28's contract pass pipeline (transparencyPass, orderFunctionsAndProofs)
- Kept PR #28's translateMethod for Python class methods
- Kept PR #28's wildcard modifies filtering in HeapParameterization
- Took PR #31's modifiesClausesToArgs (handles multiple wildcards correctly)
- Took PR #31's buildSpecBody (adds postcondition support)
- Took PR #31's instance procedure diagnostics
- Kept PR #28's test expectations (contract pass changes error messages)
@github-actions github-actions Bot added GOTO and removed Java Git conflicts PR has merge conflicts with the base branch labels May 1, 2026
@keyboardDrummer
keyboardDrummer force-pushed the disallow-transparent-statement-bodies branch from fd2988e to 5fb8577 Compare May 1, 2026 12:22
@keyboardDrummer
keyboardDrummer changed the base branch from disallow-transparent-statement-bodies to merge-1076-1077 May 1, 2026 12:23
@github-actions github-actions Bot added Git conflicts PR has merge conflicts with the base branch and removed github_actions labels May 1, 2026
@keyboardDrummer
keyboardDrummer changed the base branch from merge-1076-1077 to issue-21-assign-variable-type May 4, 2026 13:11
keyboardDrummer and others added 30 commits June 18, 2026 13:51
…rdDrummer/Strata into issue-924-contract-and-proof-pass
…strata-org#1381)

EliminateValueInReturns only folded over program.staticProcedures, so a
value-returning instance method with a body (using `return expr`) was
never rewritten into `outParam := expr; return`. Updated the order of
passes to run the `LiftInstanceProcedure` pass before
`EliminateValueInReturns`.

Add T9_ValueReturningInstanceMethods.lean: 9 positive cases (field/
computed/parameterized returns, conditional/early returns, modifies +
return, bool return, shared method names, chained receiver, local var)
and 2 negative cases pinning the no-output and multiple-output valued
return diagnostics, proving the pass now reaches instance procedures.

**Warning:** This repository will shortly undergo a split into several
separate repositories. If you're creating a PR that crosses the
boundaries between these repositories, you may want to hold off until
the split is complete or be prepared to rework your PR into multiple PRs
once the split is complete.

The code that will be moved includes:
- Strata/DDM/*
- Strata/Languages/Boole/*
- Strata/Languages/Python/* along with Tools/Python/*
- Tools/BoogieToStrata
The last testing framework update regressed the error reporting to be
tested-snippet-relative, but that is inconvenient. So whenever we flag
an error now, it is relative to the entire file (easier to jump to)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
### Changes
- Report resolution errors that occur after transformation passes during
the compilation of Laurel to Core. This change triggers errors in
existing tests that lead to the remainder of the changes in this PR.
- Remove `.THeap` and `.TTypedField`. The usages have been replaced by
`.UserDefinedType "Heap"` and `.UserDefinedType "Field"`, so that all
references to the heap type use that form, preventing type equality
errors.
- Only re-resolve after HeapParam, TypeInference and ModifiesClauses
passes all complete, since they are part of the same logical pass. In
the future, we should refactor so they're actually one pass, but still
three components, since TypeHierarchy and ModifiesClauses are features
that built on top of Composite types.
- Replace all reference types with `Composite` during the TypeInference
pass. This also enables a slight simplification of
LaurelToCoreTranslator.
- Correct a source location usage in the lifting pass. Without this
change, the reported location for an error would change, and this is
detected during re-resolution.
- In `Resolution.lean`, synth instead of void-check non-last block
elements. This is required because some passes create non-void non-last
block elements. In particular the `EliminateIncrDecr` pass creates them.
This change also means that `Resolution.lean` no longer needs to
special-case checking `| .StaticCall .. | .InstanceCall .. | .IncrDecr
`.
- Because of the previous change, `Resolution.lean` needs to support
synthing for any StmtExpr, which it now does.
- In the resolver, add special casing for calls to select/update/const.
These are polymorphic procedures which we can't type check without
special casing yet, since we don't support polymorphism yet.
- Remove expecting the body of a function to correspond to its result
type. This check failed for more complicated functions from
`T3_ControlFlow` after they were lowered by some passes. Note that
function will be removed entirely. In the test related to this removed
check, I have changed the function into a procedure and the same
diagnostic is reported then, although through a different mechanism.
- Refactor TypeAliasElim so it uses a generic traversal to update types.
This generic traversal was added a part of changes to TypeHierarchy

### Testing

Added one test, but mostly this PR relies on the existing tests.

---------

Co-authored-by: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
laurel: wire old() to Core two-state semantics

Modifies-clause frame conditions now flow through Core's `old`-prefixed
identifiers via Core's native two-state semantics, replacing the
previous
synthetic `$heap_in` parameter.

1. HeapParameterization: heap-writing procedures take `$heap` as a true
   inout parameter (same name in inputs and outputs) rather than a
   `$heap_in` / `$heap` pair with a synthesized assignment prelude.

2. ModifiesClauses: frame condition references `old($heap)` via
   `StmtExpr.Old` instead of a separate `$heap_in` variable.

3. New PushOldInward Laurel-to-Laurel pass: distributes `StmtExpr.Old`
through its sub-expressions until each `Old` immediately wraps a Local
   Var. Warns if `old(...)` does not mention any inout parameter.

4. LaurelToCoreTranslator: `Old (Var (Local n))` translates directly to
`fvar (mkOld n)`. Inout parameters at call sites are detected and emit
   `.inoutArg` rather than paired `.inArg` + `.outArg`. Call-arg
   construction is shared between the two StaticCall sites via a new
   buildCallArgs helper.

Tests: T9_OldHeapTwoState drives the feature end-to-end — `old` of heap
field reads, arithmetic/unary/comparison sub-expressions,
`old(old(...))`,
`old` inside quantifiers and if-then-else, multiple modifies/ensures,
modifies-wildcard, wrong-body negative cases, the no-inout warning, and
a
caller asserting the post-state. PushOldInward's normalization shape
(every
`Old` wraps an inout Local Var) is enforced at translate time in
LaurelToCoreTranslator: the `.Old` arm emits a `StrataBug` diagnostic if
the
invariant is ever violated.

---------

Co-authored-by: Jules <julesmt@amazon.com>
Co-authored-by: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
Co-authored-by: Shilpi Goel <shigoel@gmail.com>
Co-authored-by: Aaron Tomb <aarotomb@amazon.com>
Co-authored-by: Michael Tautschnig <mt@debian.org>
Co-authored-by: Kiro <kiro-agent@users.noreply.github.com>
Co-authored-by: Juneyoung Lee <136006969+aqjune-aws@users.noreply.github.com>
Co-authored-by: Mikaël Mayer <MikaelMayer@users.noreply.github.com>
Co-authored-by: thanhnguyen-aws <ntson@amazon.com>
Co-authored-by: Fabio Madge <fmadge@amazon.com>
Co-authored-by: Joe Hendrix <joehx@amazon.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: June Lee <lebjuney@amazon.com>
Co-authored-by: David Deng <daviddenghaotian@gmail.com>
Co-authored-by: David Deng <htd@amazon.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Mikael Mayer <mimayere@amazon.com>
Co-authored-by: Remy Willems <rwillems@amazon.com>
Co-authored-by: keyboardDrummer-bot <keyboarddrummer.bot@gmail.com>
Co-authored-by: Sagar Joshi <72283186+sagjoshi@users.noreply.github.com>
…trata-org#1401)

With the new testing framework, we were correctly locating file-wide any
uncaught diagnostic, but if an annotation was not matched (when we were
over-expecting errors, or the error has changed), the annotation was
located snippet-relative instead of file-relative.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…org#1391)

Fixes strata-org#1390.

**Problem:** `Program.eval` threads one `Env` through all procedures. A
structured `exit` out of a labeled block doesn't pop its path-condition
frames (`.block` exit pops `exprEnv.state`, not `pathConditions`; the
exiting path bypasses `Env.merge`), so a procedure's
preconditions/assumptions leak into later procedures. A contradictory
leaked set then makes the next procedure prove false obligations
vacuously — silent unsound pass. Laurel lowers non-final `return` to
`exit "$body"`, so ordinary early-return code hits it.

**Fix:** reset `pathConditions` to the pre-procedure state in the
`.proc` fold (deferred obligations and fresh names carry forward). A
`.block`-level reset was rejected — breaks fall-through.

**Test:** `ProcedurePathConditionIsolation` — unsatisfiable-precondition
`first` + structured `exit`, `second`'s `assert false` must fail. Red
without the fix, green with it; full `StrataTest` green (539). Control
matrix + scope in strata-org#1390.
Laurel had only a pre-test `while`. This adds a `doWhile` grammar op and
a body-tested `do … while` loop. Rather than a separate AST node, a
do-while is represented as a post-test `While` — the existing `While`
constructor gains a `postTest : Bool := false` field — and is lowered in
a new `EliminateDoWhile` Laurel-to-Laurel pass into the existing
pre-test loop:

```
  do S while(G) invariant I
```
becomes
```
  { while(true) invariant I { S; if (!G) { exit L } } } L
```

`L` is a fresh, collision-free label (`$dowhile_exit_{n}`, `$`-prefixed
so it can't collide with user identifiers), so nested do-whiles and user
`break`/`continue` don't capture each other's exits. The body runs once
per iteration with the guard re-checked after it, and the real guard
reaches post-loop code via the structured `exit` — so the encoding is
sound, complete, and linear (single body in the IR, no
peeling/duplication).

Invariant placement is head-tested (checked before each body), matching
`while`. **Gotcha (documented in T23):** because the guard is re-tested
only after the body, the invariant must hold of the *pre-body* state, so
`do { x:=x+1 } while(x<3)` needs the bound `x <= 2`, not `x <= 3`.

**Why a flag + a pass:** `postTest` captures the while/do-while
distinction at the type level (per review feedback) rather than
duplicating the loop as a second constructor — so most passes match
`While` once and carry the flag through, with no `.DoWhile` arms.
`postTest := false` (pre-test) is the default, so every existing
`.While` construction is unchanged. The desugaring lives in
`EliminateDoWhile` rather than in `ConcreteToAbstractTreeTranslator`, so
it runs for every program reaching the pipeline — including one supplied
directly as abstract AST, bypassing the concrete translator. The parser
builds a post-test `While`; the serializer emits `while`/`doWhile` by
the flag; the pass (modeled on `EliminateIncrDecr`, with its own
fresh-label counter) does the desugar and runs first, so no later pass
observes `postTest = true`. No new Core construct or `LoopElim` change —
the desugar reuses the existing verified pre-test loop machinery.
Resolution handles `postTest` in `Check.while` directly (it runs before
the passes, the same arrangement as `IncrDecr`).

The per-program traversal over static and composite-instance procedures,
which both `EliminateDoWhile` and `EliminateIncrDecr` had hand-rolled,
is now `mapProgramProceduresM` in `MapStmtExpr` (per review feedback) —
so neither pass refers to unrelated features like instance procedures.

Tests in `T23_DoWhile` (end-to-end): basic, runs-at-least-once (guard
false at entry), nested, break-out via labelled block, no-invariant
(zero `invariant` clauses — asserts the negated guard, the only fact
provable without an invariant), and `falsePostRejected` (a false
postcondition is rejected, confirming the `while(true)` desugar isn't
vacuous). Plus `EliminateDoWhileTest` (pass-output shape, incl. distinct
fresh labels for nested loops) and a round-trip case in
`AbstractToConcreteTreeTranslatorTest` (serialization arg order).

**Earlier label-clash warning, now resolved.** Multi-procedure `T23`
used to surface a latent label clash (`⚠️ [addPathCondition] Label clash
detected for assume_invariant_0_0, …`) — `do-while` was just the first
construct to trip it, because path-condition labels weren't reset
between procedures. This branch has merged the base fix strata-org#1391
(path-condition isolation at the procedure boundary), so the warning is
gone (verified: zero clashes on the T23 build). Not caused by this PR.

**Strata half only.** To use do-while end-to-end, the jverify front-end
must emit the new op (regenerate the `laurel/` bindings and add a
`JCDoWhileLoop` case to `JavaToLaurelCompiler`, which currently rejects
do-while) — a separate follow-up. Implements the front-end desugar from
strata-org#1350.
…overage (strata-org#1382)

Two field-access improvements on the single `fieldAccess` grammar op,
plus test coverage that closes out strata-org#1371.

Note: the chained-field-access *capability* (`a#b#c` parsing,
resolution, and heap elimination) already landed via strata-org#1328. This PR adds
the one missing ergonomic piece, hardens the chained-access machinery
with the tests it was missing, and a small refactor.

**Grammar — paren-free field `++`/`--`.** `fieldAccess` precedence is
raised 90 → 95. At 90 it tied with the postfix `++`/`--` ops (also 90),
forcing `(c#n)++`; at 95 it binds tighter, so `c#n++` parses paren-free
as `(c#n)++`. (`leftassoc`, already present from strata-org#1328, is unchanged.)
Note the new precedence is shared with `call` (also 95, via its
`callee:89`) — the two must stay equal or `a#b(x)` parsing shifts;
documented at the op.

**Resolution — refactor only.** The duplicated type-scope field lookup
in `targetTypeName` and `incrDecrTargetType` is factored into a shared
`fieldTypeInScope` helper. No behavior change. (This helper is also a
dependency of the stacked compound-assignment PR.)

**Tests — fill the chained-access coverage gap.** strata-org#1328 shipped chained
access with only a read-side smoke test (no assertions). This adds:
- `T9_ChainedFieldAccess`: chained **write** (`o#inner#count := …`),
read-after-inner-assign, must-alias, depth-3 (`a#mid#inner#count`),
chained reads on both sides of `==`, and paren-free chained
`o#inner#count++` (the one test that exercises this PR's grammar
change), plus three **negative** tests (unconstrained read, two-object
may-alias, write isolation) that pin the encoding as non-vacuous and
frame-sound.
- `T23b_IncrDecrField`: paren-free single-level field `++`/`--`.

Full `lake build` and `lake test` pass.
## Summary

Adds support for **constrained types as composite fields** in Laurel —
e.g. a composite with a field of a refinement type:

```
constrained nat = x: int where x >= 0 witness 0
composite Counter { var count: nat }
```

Field writes now check the constraint (`c#count := -1` fails), and the
field is boxed as its base type on the heap.

## ConstrainedTypeElim changes made along the way

Supporting this cleanly required running `ConstrainedTypeElim`
**earlier** in the Laurel pipeline (right after `typeAliasElim`, before
`HeapParameterization`) so that constrained field types are lowered to
their base types before the heap-boxing pass needs them. Moving the pass
forward exposed a few places where it had implicitly relied on running
last, which this PR fixes:

1. **Composite field types** — `ConstrainedTypeElim` removed the
constrained type definitions but left `UserDefined "nat"` references
inside composite fields, which then failed to resolve in Core
translation. `elimCompositeType` now resolves constrained field types to
their base types (and runs elimination on instance procedures) before
the definitions are removed. The field-write constraint check
(previously enabled by `HeapParameterization` via a `Declare` temporary)
is now emitted directly from the `.Field` assignment target.

2. **Constrained-typed assignments in expression position** — `elimStmt`
only checked assignments that appear as statements, so `y := (x := -1) +
1` with `x : nat` was unchecked. `wrapExprAssigns` now traverses
expression positions and wraps such assignments as `{ x := v; assert
T$constraint(x); x }`, asserting on a read-back so the RHS is evaluated
exactly once (semantics-preserving).

3. **`LiftExpressionAssignments` assert ordering** — for the read-back
form to work, the assert must stay *after* the assignment through
lowering. `transformExpr` was prepending `.Assert`/`.Assume` in
expression-position blocks only after the assignment had already been
prepended, moving them ahead of it (so they checked the stale value).
`transformExpr` now lifts `.Assert`/`.Assume` during its traversal so
they keep their position relative to assignments lifted from the same
block. This is a general correctness fix for the lifting pass.

4. **Cleanup** — once `ConstrainedTypeElim` runs first,
`HeapParameterization` no longer needs its own constrained-type
resolution (the field types reaching it are already base types), so the
dead `resolveConstrainedType` helper and the shared
`resolveConstrainedTypeWith` helper in `LaurelAST` were removed. Also
included the offending type name in the `LaurelToCoreTranslator` "could
not be resolved" diagnostic.

## Pipeline placement

`constrainedTypeElimPass` is moved to the second position (after
`typeAliasElimPass`). It has no `comesBefore` ordering constraints and
nothing depends on it running late, so the load-time
`comesBeforeRespected` check still holds.

## Notes

- Bitvector-as-composite-field changes that previously sat on this
branch have been removed; they live in a separate branch/PR. This PR is
scoped to constrained types.
- Rebased onto the latest `main2` (which includes the strata-org#1222
pipeline-framework refactor).

## Testing
- Full `lake build` passes (495 jobs, including all `#guard_msgs`
elaboration-time tests and the load-time pipeline-ordering check).
- `T11_ConstrainedField` covers constrained composite fields
(valid/invalid writes + a documented read-side completeness gap).
- `T10_ConstrainedTypes` gains a `sideEffect` case exercising an
assignment-in-expression to a constrained local.

---------

Co-authored-by: Remy Willems <rwillems@amazon.com>
Co-authored-by: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
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.