Skip to content

pyspec: native contract decorator recognition surface#1427

Open
julesmt wants to merge 71 commits into
mainfrom
julesmt/feature/decorator-recognition-surface
Open

pyspec: native contract decorator recognition surface#1427
julesmt wants to merge 71 commits into
mainfrom
julesmt/feature/decorator-recognition-surface

Conversation

@julesmt

@julesmt julesmt commented Jun 27, 2026

Copy link
Copy Markdown
Member

Stacked on pyspec/decorator-framework (review/merge that first).

Recognizes PySpec's native contract decorators on stub declarations and
translates their lambda bodies into the spec data model, with a full DDM
round-trip. Recognition + round-trip only — enforcement/lowering to Laurel/Core
is deferred.

Decorators: @requires, @ensures, @modifies, @snapshot, @ghost
(per-method) and @invariant (per-class). Bodies are translated to SpecExpr
via the same transExpr path used by assert, stored on new
FunctionDecl/ClassDef fields, and round-tripped through the DDM serializer.

joscoh and others added 30 commits May 12, 2026 21:33
*Issue #, if available:*

*Description of changes:* Test PR for `main2` branch


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: Josh Cohen <cohenjo@amazon.com>
*Issue #, if available:*

*Description of changes:*

- Support `decreases <int expr>` termination measures alongside
structural ADT recursion
- Int-recursive functions are pure UFs (no definitional axioms);
termination obligations assert non-negativity and strict decrease at
each call site
  - Compound measures supported (e.g., `decreases m + n`)
  - Mixed structural/int-valued mutual blocks are rejected
- New `inlineIfAllCanonical` attribute enables concrete evaluation of
int-recursive functions

Core logic in `Strata/Transform/TerminationCheck.lean` — new
`DecreasesKind` type classifies each function's measure, unified
obligation generation via callback.

Tests in `StrataTest/Languages/Core/Tests/IntRecursionTests.lean` and
`StrataTest/Languages/Core/Tests/RecursiveFunctionErrorTests.lean`.

CC @kondylidou 


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: Josh Cohen <cohenjo@amazon.com>
Keeps `main2` in sync with `main`

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: 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: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
Co-authored-by: Mikaël Mayer <MikaelMayer@users.noreply.github.com>
Co-authored-by: Josh Cohen <cohenjo@amazon.com>
…plentations (#1160)

Add a one-time pad example using three different methods for the array
data structure: a map of ints, a linked list, and the new sequence type.
The map version is verified with `gen_smt_vcs` and the two others with
`#eval verify`.

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Josh Cohen <36058610+joscoh@users.noreply.github.com>
## Problem

When a Boole program had 4+ datatype declarations and the **first**
datatype's
testers or selectors (e.g. `color..iscolor_Red`, `color..color_Red_0`)
were
referenced in a later function, the verifier reported them as free
variables
during type-checking. Moving the referenced datatype to any position
other than
first worked around the bug.

Closes #1142

## Root cause

`initFVarIsOp` built a symbol-class table by scanning program commands —
one
entry per command (one `false` per `datatype` command). But the
`GlobalContext`
registers many entries per datatype: the type itself plus every
constructor,
tester, and selector generated by templates. This made `fvarIsOp` too
short and
misaligned with the actual `fvar` indices.

When the referenced datatype was declared first, its testers/selectors
landed at
low `fvar` indices that fell inside the misaligned table. Those indices
returned
`false` (treat as type symbol), so the testers were emitted as `.fvar`
(variable
references) instead of `.op` (function applications). The type-checker
then
rejected them as undeclared free variables.

When the datatype was not first, the tester indices fell past the end of
the
table and hit the fallback path in `getFVarIsOp`, which correctly reads
`GlobalContext.vars[i]` — the same source of truth the DDM elaborator
uses for
all symbols.

## Fix

Removed `fvarIsOp`, `initFVarIsOp`, and `registerCommandSymbols`
entirely.
`getFVarIsOp` now reads directly from `GlobalContext.vars[i]`:

- `GlobalKind.type` → emit as `.fvar` (type symbol, not a callable)
- `GlobalKind.expr` → emit as `.op`, **except** for `command_var` global
variables, which become procedure parameters in Core and must remain
`.fvar`

The `command_var` carve-out uses the existing `globalVarTypes` map
(already
collected in the pre-pass), so no new state is needed.

## Test

Added `StrataTest/Languages/Boole/datatype_tester_freevar.lean` — a
direct
regression for the issue's failing case: `color` declared first among
four
datatypes, with `get_val` referencing `color..iscolor_Red` and
`color..color_Red_0`.

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: 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: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
Co-authored-by: Mikaël Mayer <MikaelMayer@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
## Summary

Extends the Boole language pipeline with new language features, curated
benchmark
targets anchored to dalek-lite (Curve25519/Ed25519), and a testing
infrastructure
upgrade across all Boole seeds.

## Language features

**`Sequence T` type and slicing ops**
- `toCoreMonoType` handles `.Sequence _ elem → .tcons "Sequence" [elem]`
- All 8 Core inherited ops wired up in `Verify.lean`
- Three Boole-specific wrappers: `Sequence.skip`, `Sequence.dropFirst`,
  `Sequence.subrange`
- Typed empty-sequence constants:
`Sequence.empty_bv8/bv16/bv32/bv64/int` — each
needs a distinct token since 0-ary polymorphic `Sequence.empty` has no
arguments
  to infer the type from

**Bitvector loop variables** (`for i : bvN := init to limit`)
- `for_to_by` and `for_downto_by` dispatch guard/step/increment to
  `Bv{N}.ULe/Add/Sub` when the loop variable is a bitvector type

**`decreases` annotations**
- `for v := init to/downto limit` accepts an optional `decreases e`
clause;
forwarded to the Core while-loop measure field and actively verified by
cvc5
- Functions and procedures accept an optional `decreases e` clause using
Core's
  existing `Measure` category — no new grammar category introduced
- All three forms reuse Core's single `measure_mk` op; no duplicate
constructs
- Function termination is verified by #1092; procedure-level `decreases`
is
silently dropped with a `dbg_trace` warning pending int-based
termination support

**Lambda abstraction and application**
- `fun x : T => body` lowers to nested Core `.abs` nodes
- `(f)(x)` lowers to `.app () f x`

**Inline `let`-block postconditions**
- `let v := e in body` in spec/ensures positions lowers via
`withBVarExprs`

**`choose` assignment**
- `w := choose z : T :: pred(z)` lowers to `havoc w; assume pred[z/w]`

**Bitvector comparisons**
- Unsigned (`<`, `<=`, `>`, `>=`) default to `Bv{N}.ULt/ULe/UGt/UGe` via
  `toBvCmpOp`
- Signed (`<s`, `<=s`, `>s`, `>=s`) lower to `Bv{N}.SLt/SLe/SGt/SGe`

## New seeds

- `embedded_postcondition.lean` — inline let-block in `ensures`
- `montgomery_loop_invariant.lean` — relational while-loop invariant;
linear
  arithmetic case verifies via cvc5 and `smtVCsCorrect`
- `scalar_reduce.lean` — B2 `reduce()` axiom with abstract types
- `sha256_compact_indexed.lean` — SHA-256 compact port (indexed
`Sequence`
encoding); all 19 VCs pass; loop counters use `int` (faithful to Rust's
`usize`
semantics, avoids uninterpreted cast to `Sequence` index); remaining
gaps:
iterator protocol (#27), fixed-size array syntax (#25), slice types
(#26)

Fully implemented seeds graduated from `FeatureRequests/` to the main
Boole test
folder: `early_return.lean`, `choose_operator.lean`,
`bitvector_ops.lean`,
`embedded_postcondition.lean`.

## Seed test infrastructure

Replaced `#guard_msgs (drop info) in` with explicit `/-- info: ... -/` +
`#guard_msgs in` across all Boole seeds. Added `(options := .quiet)`
uniformly.

## Documentation

- New `docs/BooleBenchmarks.md`: five real-world benchmark targets from
dalek-lite
- Updated `docs/BooleFeatureRequests.md`: all new seeds in inventory
table,
  implemented features extended

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: Michael Tautschnig <mt@debian.org>
Co-authored-by: Aaron Tomb <aarotomb@amazon.com>
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: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
Co-authored-by: Mikaël Mayer <MikaelMayer@users.noreply.github.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Instead of running all the commands to build the verso docs, which
quickly become repetitive on quick iterations of documentation building,
I propose to chain the commands :
1. build the docs
2. serve via a python server (localhost)
3. open automatically
Regular merge of `main` into `main2`.

---------

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: keyboardDrummer-bot <keyboardDrummer-bot@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: Josh Cohen <36058610+joscoh@users.noreply.github.com>
Co-authored-by: Josh Cohen <cohenjo@amazon.com>
…#1220)

Previously, the RHS of an `init` statement could involve uninitialized
variables, which were implicitly nondetermistic and which were used to
implement default expressions for `init`. Since
#432 added an explicit
`.nondet` initialization method, this workaround is no longer needed,
and this PR removes that capability. The Boole -> Core translation used
a similar method; this PR also changes it to use `.nondet`.

Co-authored-by: Josh Cohen <cohenjo@amazon.com>
Fixes #1236

`translateFromDDMTermToUntyped` was missing a case for `sc_decimal_neg`,
causing negative real-valued counter-examples (e.g. `-2.0`) to fall
through to the catch-all error and be silently dropped from the parsed
model.

Added the missing case that negates the mantissa, mirroring the existing
`sc_numeral_neg` pattern. Since all `SpecConstant` constructors are now
exhaustively matched, the catch-all was removed and a comment documents
the deliberate exhaustiveness so future reviewers don't re-add a
defensive catch-all.

Tested: all library and test modules compile successfully.
…1271)

Fixes #1231

`translateFromTerm` threw on `Term.none` and `Term.some`, which caused
`termToSMTString` to produce an error (or previously, via `panic!`, an
empty body that solvers reject).

This PR adds proper SMT-LIB encoding:
- `Term.none ty` → `(as none (Option T))`
- `Term.some inner` → `(some <inner>)`

Tested: existing tests pass, new `#guard_msgs` tests added for both
cases at the `termToString` and `termToSMTString` levels.
…#1217)

Adds three cross-sort conversion operators to Core, as proposed in
#1191.
Split from the original PR per reviewer request — Core layer only.
Boole surface syntax follows in a separate PR.

## Operators

| Core name | SMT-LIB 2.7 | Lean | Direction |

|-----------------|-------------------|-------------------|-----------------|
| `Bv{n}.ToUInt` | `ubv_to_int` | `BitVec.toNat` | bv → int (unsigned) |
| `Bv{n}.ToInt` | `sbv_to_int` | `BitVec.toInt` | bv → int (signed,
two's complement) |
| `Int.ToBv{n}` | `(_ int_to_bv n)` | `BitVec.ofInt n` | int → bv (mod
2^n) |

Supported widths: 1, 8, 16, 32, 64, 128. All three are total — no
preconditions, no Safe variants, no axioms.

## Changes

- **CoreOp**: `BvOpKind.ToUInt`, `BvOpKind.ToInt` (unary, cross-sort);
  `CoreOp.intToBv n`
- **Factory**: `bvToUIntFunc`, `bvToIntFunc`, `intToBvFunc` + per-width
  instances for all 6 widths; registered in `WFFactory`
  (factoryOps: 286 → 304)
- **SMTEncoder**: maps `.bv ⟨_, .ToUInt⟩` → `ubv_to_int`,
  `.bv ⟨_, .ToInt⟩` → `sbv_to_int`, `.intToBv n` → `(_ int_to_bv n)`
- **DL/SMT/Op**: `Op.BV.ubv_to_int`, `Op.BV.sbv_to_int`,
`Op.BV.int_to_bv n`
  \+ `mkName` entries
- **DL/SMT/Denote + Translate**: handle `ubv_to_int` / `sbv_to_int` /
`int_to_bv`
- **Core DDMTransform/Grammar + Translate**: `bv128` type + literal;
  `bv128 → .bitvec 128`
- **Boole/Verify** (minimal): `bv128` cases in `typeRange`,
  `toCoreMonoType`, `bvWidth` — required because `bv128` enters
  `BooleDDM.BooleType` via the grammar change above
- **Tests**: `ProgramEvalTests` (18 new func entries for all 3 ops × 6
  widths), `StatisticsTest` (count bump)

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Shilpi Goel <shigoel@gmail.com>
…ns (indices discarded) (#1270)

Fixes #1232

`resolveQI` was discarding the indices from `iden_indexed` identifiers,
so solver-returned bitvec literals like `(_ bv5 32)` were decoded as UF
applications (`mkUFApp "bv5" []`) instead of proper bitvec values.

This extends `resolveQI` to preserve indices, then detects the `bv{N}`
pattern with a numeral width index in `translateFromDDMTermToUntyped` to
reconstruct the correct `.prim (.bitvec (BitVec.ofNat width value))`.

Tested: existing tests pass, new round-trip decode tests added for
bitvec literals.
Lower a `Sequence.of_<ty>[v0, ..., vn]` literal to a left-fold of
`seq_build` over a typed `seq_empty`. The element type is required on
the seed so that empty literals retain their type and the
bounds-precondition pass does not emit polymorphic obligations.

---------

Co-authored-by: David Deng <htd@amazon.com>
…mts` (#1265)

Remove the dead `var_map.find?` lookup in `outputSetStmts`. The lookup
always returned `.none` because `out_vars` already contains renamed
identifiers (from `renameAllLocalNames`), while `var_map`'s keys are the
original pre-rename identifiers. Also simplifies `renameAllLocalNames`
to return `CoreTransformM Procedure` instead of a tuple, since `var_map`
has no remaining consumers.

No behaviour change. Existing tests pass.

Fixes #1234
…e callers to `Maps.remove` (#1261)

Fixes #1240

## Summary

`Maps.remove` was functionally identical to `Maps.erase` (both erase
from every scope) despite its docstring claiming first-occurrence
semantics. This PR deletes the duplicate and keeps `Maps.remove` as the
canonical name, migrating all callers accordingly.

## Changes

- Deleted the old `Maps.remove` (which had incorrect docstring) and its
four supporting theorems
- Renamed `Maps.erase` → `Maps.remove` (keeping the correct
implementation)
- Renamed all related theorems to use `remove` instead of `erase` (e.g.
`Maps.keys_erase_subset` → `Maps.keys_remove_subset`)
- Migrated all call sites

## Testing

Full build passes, all existing tests pass.
… `findUnique` registry (#1250)

Fixes #1230

## Problem

The SMT encoder emitted identifiers from multiple sites without a
unified disambiguation registry. A user-defined UF named `f.N` could
collide with the Nth `encodeFunction` output, and
sort/datatype/constructor names flowed directly to the solver without
being tracked, creating potential duplicate-declaration errors.

## Solution

Every SMT-LIB identifier emitted by the encoder now routes through a
unified `usedNames` registry in `EncoderState`:

- **`Encoder.uniquify` helper** (batch path): Encapsulates the
find-unique-and-register pattern (checks `usedNames` + SMT reserved
keywords, disambiguates via `@N` suffix, registers the result).
- **`AbstractEncoder.uniquify` helper** (incremental path): Same pattern
lifted into the `AbstractEncoderState` monad, used by both `encodeUF`
and `encodeFunction`.
- **`smtReservedKeywordsSet`**: Pre-computed `HashSet` lifted to
top-level, avoiding recomputation on every `uniquify` call.
- **Sort/datatype/constructor/selector names**: Pre-populated into
`EncoderState` via `SMT.Context.preDeclaredNames` (shared helper used by
both `encodeCore` and `encodeDeclarationsAbstract`). Includes
constructor names (`c.name.name`) and selector names (`d.name ++ ".." ++
fieldName.name`) for all seen datatypes, plus built-in Option names
(`none`, `some`, `val`).
- **Constructor names in `declareType`**: `declareType` uniquifies
constructor names in addition to the type name.
- **Quantifier-bound names**: `toSMTTerm` now includes sort and datatype
names in its disambiguation set.
- **`encode` standalone function**: Pre-populated with the `Option`
datatype names.
- **No `$__` prefix**: Generated base names are now plain `f.0`, `f.1`,
... for readability; uniqueness is enforced by the registry, not by
naming conventions.

## Testing

- All existing tests pass
- New `#guard_msgs` unit tests verify collision avoidance for:
  - UF-vs-function name collisions
  - UF-vs-sort/datatype name collisions
  - Constructor disambiguation in `declareType`
  - Built-in `Option`/`none`/`some`/`val` collision
  - AbstractEncoder paths (`encodeUF`/`encodeFunction`) via mock solver
…1260)

Fixes #1242

Fix typo in `SourceRange.fromIon`: the `asSexp` call used `"Source
rang"` instead of `"Source range"`, producing a truncated error message
on malformed input.

The repeated `"Source range"` prefix is now hoisted into a local `let
tag` binding to prevent the same class of typo from recurring.

Tests added:
- Error-path test asserting the error message starts with `"Source
range"`
- Success-path round-trip tests for both the null case and a valid sexp
with start/stop values
…ter and produce duplicate labels (#1267)

Fixes #1235

Unlabeled `cover` statements were reading the `assert_def` counter
instead of `cover_def` for their default label generation. This caused
consecutive unlabeled `cover` statements to produce duplicate `cover_0`
labels when no asserts intervened.

The fix uses the correct counter for each statement kind. The duplicated
assert/cover translation pattern is extracted into a
`translateLabeledCheck` helper.

Tested: existing tests pass, new regression tests verify distinct labels
for consecutive covers and independent counters across assert/cover
combinations.
…ization (#1286)

Fixes #1228

When deserializing Ion binary structs, NOP pads (type code 0 with length
!= 15) between fields caused subsequent field-key symbol indices to
shift, because the field-key symbol ID was unconditionally pushed to the
symbol stack even for NOP pads.

The fix introduces a `TypeDesc.isNopPad` helper and uses it to skip
pushing the field-key symbol for NOP pads, so subsequent fields retain
their correct symbol indices.

Tested with `#guard` tests covering NOP pad between fields and NOP pad
at start of struct. Existing tests pass.
…ed input (#1263)

Fixes #1238

Adds a `checkArgMin` bounds check before accessing `args[1]` in the
`"indent"` case of `SyntaxDefAtom.fromIon`. Previously, a malformed sexp
like `(indent)` with no second argument would crash with a panic. Now it
returns a graceful error message.

Also cleans up the neighbouring `"ident"` case by dropping panicking `!`
indexing (the proof from `checkArgCount` already provides bounds),
renames unused proof variables to `_`, and renames a shadowed `have p`
to `have _h` for clarity.

Tested: existing tests pass, `#guard_msgs` regression tests added
covering the malformed indent path, successful empty tail, successful
with inner atoms, and wrong-type-at-args[1] error path.
Fixes #1243

`elabDialectImportCommand` was pushing the import name into
`dialect.imports` before attempting to load the dialect. When the load
failed, the early `return` left the name in imports, causing downstream
`addDialect!` panics with "Unknown dialect".

The fix extracts a `getOrLoadDialect` helper that encapsulates the
lookup-or-load logic (including error reporting and `missingImport`
flag). `elabDialectImportCommand` is now a linear sequence: resolve the
dialect, push to imports, open it — making the invariant that
`dialect.imports` only contains successfully resolved dialects locally
evident.

Tested: existing tests pass, new test added that elaborates a dialect
with a non-existent import, asserts the failed name is absent from
`dialect.imports`, and exercises `openLoadedDialect!` on the result to
confirm the previously-panicking path succeeds.

---------

Co-authored-by: Aaron Tomb <aarotomb@amazon.com>
…t" (#1296)

Fixes #1295

Replaces the "path unreachable" outcome label with "unreachable in this
context" in all user-facing verification output (outcome labels,
diagnostic messages, and SARIF messages). The SARIF message no longer
includes "path condition is contradictory" since unreachability can stem
from reasons beyond the path condition. This is a wording-only change
with no impact on proof obligations or SMT encoding.

Tested: all existing tests pass with updated expected output.
…ctor name (#1264)

Fixes #1237

The `checkArgCount` call in the `"decimal"` case of `ArgF.fromIon` was
copy-pasted from the `"num"` case and still passed `"num"` as the
constructor name. A malformed `decimal` sexp would produce an error
saying "num expects 3 arguments" instead of "decimal expects 3
arguments".

Fixed by passing `"decimal"` to `checkArgCount`. Added a test in
`StrataTest/DDM/Ion.lean` that constructs a 4-element `decimal` sexp and
verifies the error message starts with `"decimal"`.

Filed #1294 to track a broader refactoring that would eliminate this
class of copy-paste bug.
## Summary

Extracts the formatting and debugging improvements from #34 into a
standalone PR.

### Formatting improvements
- **Block formatting**: Changes block output from single-line `{ stmt1;
stmt2 }` to vertical layout with `indent(2)`:
  ```
  {
    stmt1;
    stmt2
  }
  ```
- **Semicolon separator**: Uses newlines instead of spaces between
semicolon-separated items in the formatter.

### Debugging improvements
- **Better diagnostic reporting**: Replaces the boolean
`coreProgramHasSuperfluousErrors` with a `coreDiagnostics : List
DiagnosticModel` that records *why* the Core program was suppressed.
When no other diagnostics explain the suppression, these are surfaced to
the user.
- **Informative `invalidCoreType`**: Adds `source` and `reason`
parameters so each suppression site provides context about what went
wrong.
- **Intermediate file output**: Adds
`processLaurelFileKeepIntermediates` test helper that writes pipeline
intermediate files to `Build/` for debugging.
- **`.gitignore`**: Adds `Build/` directory.

### Test updates
- All expected outputs updated to match the new block formatting.
- Minor `#guard_msgs` whitespace fixes.
- Some test inputs updated to use `opaque` keyword where needed for the
new formatting to apply correctly.

---------

Co-authored-by: Remy Willems <rwillems@amazon.com>
Co-authored-by: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
…amFromDialect (#1285)

Fixes #1244

`parseStrataProgramFromDialect` in `Strata/DDM/Elab.lean` threw
`IO.userError "Internal {dialect} missing from loaded dialects."` as a
plain string literal, missing the `s!` prefix. The user saw the literal
text `{dialect}` in the error message instead of the actual dialect
name.

This PR:
- Adds the missing `s!` prefix to the original error message
- Fixes 5 additional instances of the same bug across the codebase (in
`panic!`, `throw`, and `dbg_trace` calls)
- Adds a CI lint check (`.github/scripts/checkMissingInterpolation.sh`)
that detects this class of bug going forward

Tested with a new `#guard_msgs` test that triggers the error and asserts
the interpolated value appears. Existing tests pass. The new CI lint
check passes on the fixed codebase.
… loadDialectRec) (#1253)

Fixes #1227

`loadDialectRec` accepted a `stk : Array DialectName` parameter for
cycle detection but never checked it. Two dialect files with mutual
imports (A imports B, B imports A) caused unbounded recursion / stack
overflow.

Now checks whether the dialect name is already on the stack before
attempting to load, returning a clear "Circular import detected" error
that renders the full cycle path (e.g. `A -> B -> C -> A`), making the
offending edge obvious.

Tested with integration tests covering:
- 2-cycle (A ↔ B)
- Self-import (A imports A) — caught by the existing `openDialectSet`
check
- 3-cycle (X → Y → Z → X)

All existing tests pass.
…Map` iteration order (#1269)

Fixes #1233

`DeclState.ofDialects` folds over `LoadedDialects.dialects.toList` to
open all dialects. Since `DialectMap` wraps a `HashMap`, iteration order
is non-deterministic. If a child dialect is visited before its parent,
`addDialect!` opens the parent transitively, and then when the parent is
visited directly, `openLoadedDialect!` panics because it's already open.

The fix replaces `openLoadedDialect!` with `ensureLoaded!` in the fold.
`ensureLoaded!` is idempotent — it returns immediately if the dialect is
already open.

Tested: existing tests pass, new regression test added that directly
exercises `DeclState.ofDialects` and deterministically tests the
idempotency of `ensureLoaded!` by simulating the problematic
child-before-parent iteration order.
…pe for ≥3 distinct-type args (#1280)

Fix local function declaration building wrong arrow type for ≥3
distinct-type args.

The `funcDecl_statement` case in the Core translator used `mkArrow` with
reversed inputs, producing incorrect type ordering (e.g. `int → real →
bool → int` instead of `int → bool → real → int`) for local functions
with 3+ parameters of distinct types. Replaced with `mkArrow'` which
takes the input list in order, conforming to the Denote-layer pattern
used in Assumptions.lean, CallOfLFuncDenote.lean, and Semantics.lean.

Tested with a regression test that declares a local function `f(x:int,
b:bool, r:real):int` and calls it, plus a `#guard` that directly
verifies the `mkArrow'`/`destructArrow` roundtrip invariant. Existing
tests pass.

Fixes #1226
…peArgs (#1284)

Fixes #1229

## Problem

`TEnv.addTypeAlias` had two defects:
1. **No chain resolution** — the RHS was stored without de-aliasing, so
chained aliases (e.g., `BarAlias a := FooAlias a` where `FooAlias` is
itself an alias) were never fully resolved.
2. **Internal type argument names** — stored `typeArgs` used
`lhs.freeVars` (which produces internal `$__ty0`, `$__ty1` names)
instead of the user-supplied names.

## Fix

Replace the `instantiateEnv` + pattern-match logic with a direct call to
`LMonoTy.resolveAliases` on the RHS before storing, and use
`alias.typeArgs` directly. This maintains the invariant that stored
alias types are never themselves aliases, and preserves user-supplied
type argument names.

## Invariant theorems

Two theorems in `LExprTypeSpec.lean` lock the invariants this PR
establishes:

- `TEnv.addTypeAlias_preserves_typeArgs` — fully proved; the stored
alias retains the user-supplied `typeArgs` and `name`.
- `TEnv.addTypeAlias_stored_dealiased` — states that the stored type is
a fixpoint of `resolveAliases`. Proof reduced to idempotence of
`resolveAliases` (remaining `sorry` to be completed in a follow-up).

A helper `LMonoTy.resolveAliases_env` / `LMonoTys.resolveAliases_env`
proves that `resolveAliases` preserves the full `TEnv` (stronger than
context-only preservation).

## Testing

- New `#guard_msgs` tests verify chained alias resolution and preserved
`typeArgs`.
- All existing tests pass.

## Related issues (separate PRs)

- #1239 (reopened): arity mismatch error reporting
- #1297 (new): typed `seqEmptyOp` in Boole sequence literals
leo-leesco and others added 20 commits June 11, 2026 22:42
Previously, we were using raw strings and manually routed them to be
translated to `Strata.Program`
Now, we use the DDM mechanism that creates a `Strata.Program` out of the
box. This comes with several advantages :
- we don't have to calculate character offsets manually anymore (for
error reporting)
- we get to leverage Lean's LSP integration for DSLs

For formatting and other purposes, you can use `Strata.SourcedProgram`
which is essentially `Strata.Program` with added metadata (line/col
number, filename…). An automatic coercion is in place to strip metadata
back into a `Strata.Program`.

---------

Co-authored-by: Michael Tautschnig <mt@debian.org>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Juneyoung Lee <136006969+aqjune-aws@users.noreply.github.com>
## Summary
Split out of #1356, this PR contains **only the bitvector-related
changes**. The constrained-types work from #1356 is being handled
separately.

Adds support for bitvector-typed fields in composite types, plus
bitvector literal syntax in the Laurel grammar.

### Changes
- **HeapParameterization**: add `.TBv n` cases to `boxConstructorName`,
`boxDestructorName`, and `boxConstructorDef` so composite types can have
bv-typed fields (boxed as `BoxBv{n}`, generated per width).
- **Grammar**: add `bvLiteral(value, width)` operation with syntax
`<value> bv <width>` (e.g. `100 bv 16`).
- **AST**: add `LiteralBv (value width : Nat)` variant to `StmtExpr`.
- **Parsing/formatting/translation/typing**: handle bv literals in
`ConcreteToAbstractTreeTranslator`, `AbstractToConcreteTreeTranslator`,
`LaurelToCoreTranslator` (-> Lambda `bitvecConst`), and type inference
(`LiteralBv` has type `TBv width`).

### Tests
- `T10_CompositeBvField`: covers writing/reading bv fields, bv literals,
and an error case (postcondition violation when a bv field is not
written / wrong literal value).
- Verified with `lake build` of the affected Laurel modules and the T10
test (`#guard_msgs` passes).
Adds prefix and postfix increment/decrement operators to the Laurel
dialect, usable both as statements (`x++;`) and inside expressions (`int
y := (x++) + (x++);`).

Surface tokens `++` and `--` are reserved for the new operators by
renaming Laurel's string-concatenation operator from `++` to `^`
(OCaml-style). Existing tests using the old token are migrated.

Implementation:
* New `.IncrDecr` constructor on `StmtExpr` carrying mode (Pre/Post), op
(Incr/Decr) and an lvalue target (Local or Field).
* New `EliminateIncrDecr` pass, run first in the Laurel pipeline, lowers
IncrDecr to existing constructs:
    Pre  -> (target := target ± 1)            (yields new value)
    Post -> ((target := target ± 1) ∓ 1)      (yields old value)
  Statement-position IncrDecr lowers to a clean assignment without a
  dead snapshot.
* Lift fix: `transformStmt` now lifts side effects out of expression-
  statements (only when they contain an assignment / imperative call),
  so postfix-as-statement reaches Core correctly.
* All exhaustive StmtExpr matches updated: MapStmtExpr, computeExprType,
  Resolution.resolveStmtExpr / collectStmtExpr, FilterPrelude, lift
  helpers, and both grammar tree translators.

Tests:
* StrataTest/Languages/Laurel/IncrDecrLiftTest.lean — exact lowering of
stmt form, prefix, and postfix.
* StrataTest/Languages/Laurel/Examples/Fundamentals/T23_IncrDecr.lean —
10 verification procedures covering Java semantics: prefix yields new,
postfix yields old, (x++)+(x++), (++x)+(++x), mixed pre/post, pre/post
decrement, postfix in for-loop step, intentional failing assert.

A design plan accompanying this change is at
docs/LaurelIncrDecrPlan.md.

**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
## 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](strata-org/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.
… lowering to Core (#1328)

## Summary

This PR addresses the problem that Laurel composite types could
*declare* instance procedures inside `composite { ... }` blocks, but
they couldn't be compiled or called. The Laurel→Core translator
unconditionally rejected every instance procedure with a
`NotYetImplemented` diagnostic, and even without that block they would
have been silently dropped: the SCC ordering in
[`CoreGroupingAndOrdering.lean`](Strata/Languages/Laurel/CoreGroupingAndOrdering.lean)
only enumerates `program.staticProcedures`, so anything on
`CompositeType.instanceProcedures` never reached Strata Core.

### 1. Surface syntax of instance procedure calls: `obj#method(args)`

- **Parser**
([`ConcreteToAbstractTreeTranslator.lean`](Strata/Languages/Laurel/Grammar/ConcreteToAbstractTreeTranslator.lean)):
when the callee of `call(...)` is a `fieldAccess` node, emit
`InstanceCall target method args` instead of dropping the receiver into
an empty-string static call.
- **Grammar**
([`LaurelGrammar.st`](Strata/Languages/Laurel/Grammar/LaurelGrammar.st)):
adjust `call`'s precedence so `c#m(args)` parses as `call(fieldAccess(c,
m), args)`.
- **Resolution**
([`Resolution.lean`](Strata/Languages/Laurel/Resolution.lean)):
pre-register instance procedures in the global scope under their lifted
key (`<CompositeName>$<methodName>`). Two composites can now share a
method name without colliding (Note: this change seems to resolve #1321
but the the issue of the Resolver still exists - variable scope should
be carefully refactored for the resolution pass). `InstanceCall`
resolution looks up the receiver's composite type, builds the lifted
key, and stamps the resolved `uniqueId` on the original callee
identifier.

### 2. Laurel-to-Laurel pass: `LiftInstanceProcedures`

A new pass under
[`Strata/Languages/Laurel/LiftInstanceProcedures.lean`](Strata/Languages/Laurel/LiftInstanceProcedures.lean),
wired into
[`LaurelCompilationPipeline.lean`](Strata/Languages/Laurel/LaurelCompilationPipeline.lean)
with `needsResolves := true` between `EliminateValueReturns` and
`HeapParameterization`. The pass:

- Walks every `.Composite ct` in `program.types` and clones each `proc ∈
ct.instanceProcedures` into a fresh top-level static procedure named
`<CompositeName>$<methodName>`. Body, parameters, contracts, and
`invokeOn` are copied verbatim.
- Walks the entire program and rewrites every `InstanceCall` whose
callee so it points at the lifted name (with the receiver prepended as
the first argument to match the lifted procedure's `self :
<CompositeName>` parameter).
- Clears `ct.instanceProcedures := []` on every composite and appends
the lifted procedures to `program.staticProcedures`.

Downstream simplifications:
[`HeapParameterization.lean`](Strata/Languages/Laurel/HeapParameterization.lean)
drops its secondary traversal of `instanceProcedures` (now always
empty), and the `NotYetImplemented` block in
[`LaurelToCoreTranslator.lean`](Strata/Languages/Laurel/LaurelToCoreTranslator.lean)
is replaced by a defensive `StrataBug` assertion that fires only on a
pass-ordering regression (all instance procedures should already be
lifted and rewritten at this point).

---------

Co-authored-by: olivier-aws <obouisso@amazon.com>
…n heap-writing procedures (#1349)

## Summary

`HeapParameterization` rewrites `==`/`!=` on heap references into a
`Composite..ref!` reference comparison, gated on the operand type being
`.UserDefined _`. That pattern matches **both** composites (heap
references, where `ref!` is correct) **and** datatypes (values, where
`ref!` is wrong — it unifies a datatype value against `Composite`, which
is an `int` synonym).

## Symptom

The bug only surfaces inside a procedure that **writes the heap**,
because only then does the heap-rewriting pass descend into the body and
reach the equality arm. A datatype comparison sitting next to any heap
write (e.g. a `new C` allocation) fails Core type checking with:

```
Impossible to unify (arrow Composite int) with (arrow <Datatype> ...)
```

This is a latent, general correctness bug — it affects **any** datatype
`==`/`!=` in a heap-writing procedure, including a plain `datatype Pair
{ MkPair(a: int, b: int) }`. It is independent of any particular field
type.

## Fix

Guard the `ref!` rewrite on `!isDatatype` (using the existing
`isDatatype` helper) in both the `.Eq` and `.Neq` arms, so datatype
equality falls through to structural comparison. Composite
reference-equality semantics are unchanged.

## Tests

`StrataTest/Languages/Laurel/DatatypeEqHeapProcTest.lean` covers both
`==` and `!=` on a datatype inside a heap-writing procedure. Verified
both arms **fail Core type checking without the guard** and **verify
cleanly with it** (non-vacuous regression guard). Full `Strata` +
`StrataTest` build passes (554 jobs), no other regressions.

## Notes

Found while investigating `Array<T>` in datatype constructor arguments
(the Seq/Array PR #1073): allocating an `Array<T>` forces `writesHeap`,
which made this pre-existing bug reachable. This PR fixes the root cause
independently; the array-facing follow-up (lifting the validator gate,
flipping that test to positive) is left to #1073.

Co-authored-by: Siva Somayyajula <somayyas@amazon.com>
## Summary

Adds type checking to Laurel's `Resolution.lean` as requested in #1120.

## Changes

- **`resolveStmtExpr` now returns `ResolveM (StmtExprMd × HighTypeMd)`**
— both the resolved expression and its synthesized type.

- **Type checks added:**
  - Boolean conditions in `if`/`while`/`assert`/`assume` must be `TBool`
- Arithmetic/comparison operands must be numeric (`TInt`, `TReal`,
`TFloat64`)
- Logical operands (`And`, `Or`, `Not`, `Implies`, etc.) must be `TBool`
  - Static call argument types must match parameter types
- Instance call argument types must match parameter types (skipping
`self`)
  - Assignment value type must match target type (single-target only)
- Functional procedure body type must match declared output type
(transparent bodies only)

- **Diagnostics, not hard failures** — type mismatches are reported via
`ResolveState.errors` and compilation continues.

- **Cascading error prevention:**
  - `Unknown` types are compatible with everything
- `UserDefined` types skip strict assignability checks
(subtype/inheritance relationships are not tracked during resolution)
- `TVoid` types skip assignment/output checks (statements like
`return`/`while` don't produce values in the expression sense)
- `MultiValuedExpr` types skip assignability checks (arity mismatch
already reported separately)
- Kind-mismatched type references (e.g., using a variable name as a
type) produce `Unknown` to avoid cascading

- **`computeExprType` in `LaurelTypes.lean` is unchanged** — it
continues to work alongside the new type checking.

- **Callers updated** to use the returned type from `resolveStmtExpr`
(e.g., `resolveBody`, `resolveProcedure`, `resolveInstanceProcedure`,
`resolveConstant`, `resolveTypeDefinition`).

## Testing

All existing tests pass (`lake build StrataTest` — 592 jobs successful).

Closes #1120"

---------

Co-authored-by: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
Co-authored-by: Léo LEESCO <leo.leesco@gmail.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Léo Leesco <109468520+leo-leesco@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: 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: Sagar Joshi <72283186+sagjoshi@users.noreply.github.com>
…#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>
…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>
Fixes #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 #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 #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
#1350.
…overage (#1382)

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

Note: the chained-field-access *capability* (`a#b#c` parsing,
resolution, and heap elimination) already landed via #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 #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.** #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 #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>
## Functional changes
1. [Debugging] Improve the printing of Laurel if-then-else expressions
1. `EliminateReturnsInExpression` now runs for procedures as well, which
enables more types of transparent bodies for procedures. To make it work
for both functions and procedures, it was also necessary for the body of
functions to be immediately wrapped in a return statement during
parsing.
1. Allow calling procedures from contracts. Combined with the previous
change this makes procedures strictly more powerful than functions
1. Let the transparency pass rewrite the bodies of assume statements so
they don't assert anything.
1. Improve diagnostics related to contracts, using the correct verbiage
"precondition" and "postcondition" instead of "assertion"
1. Generalized the `LaurelPass` concept so it works for all
transformation between Laurel source and Core, not just the
Laurel->Laurel transformation. This helps make the documentation more
complete.

### Why let the transparency pass rewrite the bodies of assume
statements so they don't assert anything?
After the contract pass, a call will look like `assert <preconditions>;
call(..); assume <postconditions>`, where the body of the callee looks
like `assume <preconditions>; <body>; assert <postconditions>`. If we
now do either concrete execution, or we do inlining, then any assertions
that occur inside the pre or postconditions will be asserted twice,
because they occur once in an assert and once in an assume. By ignoring
the assertions inside the assume, we prevent the duplication.

Whether you also want this behavior for assumptions that were created by
users is something I'm not sure about. However, if we want we can let
those behave differently. Right now I think we don't have enough data to
decide what we want for user created assumptions, and they are AFAIK not
yet used, so I think it's OK to change their behavior.

## 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. Note: the changes in this pass could have been
extracted to a different PR to reduce the scope of this one, but I think
that keeping them in this PR is most efficient from a developer time
perspective.

## 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

---------

Co-authored-by: keyboardDrummer-bot <keyboardDrummer-bot@users.noreply.github.com>
Co-authored-by: Fabio Madge <fabio@madge.me>
Under `--use-array-theory`, a `modifies` clause naming only individual
object
references is now encoded as a single **quantifier-free heap equation**
instead of
a `∀`-quantified frame, so the solver discharges it by array rewriting
instead of
instantiating a quantifier per object/field.

∀ obj, fld. obj ≠ c ⇒ readField(old,obj,fld) == readField(new,obj,fld)
-- before
data(new) == store(data(old), c, select(data(new), c)) -- now

The `∀` frame is unchanged for set-valued/empty `modifies` and when
array theory is
off, so default behavior is untouched.

---------

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: keyboardDrummer-bot <keyboardDrummer-bot@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>
Co-authored-by: Jules <julesmt@amazon.com>
Extract PySpecMClass into Specs/Diagnostics.lean to break the import
cycle, then add Specs/Decorators.lean: a reusable decorator-recognition
framework (DecoratorForm/ofExpr? normalizing the 4 surface shapes,
lambda/kwarg helpers, and the decline-is-noop DecoratorScheme). Pure
library gearing with no pipeline wiring yet; consumed later by the
recognition layer.
Stacked on the decorator-framework branch. Recognizes PySpec's native,
unqualified contract decorators on stub declarations, translates their lambda
bodies into the spec data model, and round-trips them through DDM. Recognition
+ round-trip only; enforcement/lowering to Laurel/Core is deferred.

Decorators: @requires, @ensures, @Modifies, @snapshot, @ghost (per-method) and
@invariant (per-class). Bodies are translated to SpecExpr via the same transExpr
path used by `assert`, stored on new FunctionDecl/ClassDef fields, and
round-tripped via the DDM serializer.

- Specs/Native.lean: DecoratorScheme-based recognizers (method + class) built on
  the framework; RawGhost/RawSnapshot bundles; uniform specError diagnostics
- Specs/Decls.lean: Snapshot/Ghost structs; new FunctionDecl/ClassDef fields;
  SpecExpr.containsPlaceholder
- Specs/DDM.lean: snapshot/ghost/modifies/invariant categories + toDDM/fromDDM
- Specs.lean: transExpr .Attribute case scoped via allowFieldAccess (on for the
  not-yet-lowered targets, off for @requires/@ensures and asserts); contract-body
  translation helpers (runNoWarn + a deep containsPlaceholder drop);
  class-invariant translation
- Specs/Decorators.lean: extends the framework with functionParamNames,
  exprKeyword?, hasKeyword, and an extra-positional-arg warning in expectLambda?

Tests (StrataPythonTestExtra): content assertions over the recognized SpecExprs,
negative/error tests, a DDM serde round-trip, and field-access boundary tests
across 18 native_cases fixtures.

Verified: lake build StrataPython (228 jobs), StrataPythonTestExtra (245),
StrataPythonTest (239); all with zero warnings.
@julesmt
julesmt requested a review from a team June 27, 2026 03:45
@github-actions github-actions Bot added Waiting-For-Review Git conflicts Laurel Core GOTO dependencies Pull requests that update a dependency file github_actions Pull requests that update GitHub Actions code SMT labels Jun 27, 2026
@julesmt julesmt changed the title Julesmt/feature/decorator recognition surface pyspec: native contract decorator recognition surface Jun 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Core dependencies Pull requests that update a dependency file Git conflicts github_actions Pull requests that update GitHub Actions code GOTO Laurel SMT Waiting-For-Review

Projects

None yet

Development

Successfully merging this pull request may close these issues.