feat(laurel): Seq<T> and Array<T> with bounds-checked subscript and diagnostics#1073
feat(laurel): Seq<T> and Array<T> with bounds-checked subscript and diagnostics#1073fabiomadge wants to merge 10 commits into
Seq<T> and Array<T> with bounds-checked subscript and diagnostics#1073Conversation
589d615 to
4f5dd33
Compare
6424973 to
1f4b0ce
Compare
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖 Well-structured PR. The separation between ValidateSubscriptUsage (pure diagnostics) and SubscriptElim (rewriting) is clean, and the centralized SeqOp.* / arrayCompositeName constants prevent name drift between passes. The highTypeTag approach for per-element-type Box constructors is a good solution to the deduplication collision. The bounds-precondition infrastructure (mkSeqBoundsPrecond + classifyPrecondition) integrates naturally with the existing PrecondElim pattern.
A few items below.
- Document `_model` parameter: accepted to satisfy LaurelPass.run's signature but unused because the caller's model predates $Array injection; SubscriptElim rebuilds via resolve. - Process `program.constants` in addition to types and staticProcedures so subscript access in constant initializers is eliminated too. Latent today (no grammar path produces it), defensive. - Rename `SeqOp.dataField` -> `arrayDataField" (moved out of `SeqOp.*` namespace) since it is a field name on the synthetic composite, not a Sequence.* operation. Addresses MikaelMayer's three inline comments on PR #1073.
1f4b0ce to
37649a2
Compare
MikaelMayer
left a comment
There was a problem hiding this comment.
🔍 LGTM — all comments addressed.
MikaelMayer
left a comment
There was a problem hiding this comment.
🔍 LGTM — all comments addressed. subscriptElim now maps elimExpr model over program.constants initializers, closing the gap I flagged. The subsequent refactoring and documentation commits look clean.
MikaelMayer
left a comment
There was a problem hiding this comment.
🔍 LGTM — all comments addressed. The new structural-recursion commit cleanly resolves the partial TODO by adding sizeOf lemmas and inlining the mutual helpers. Previous feedback (constants handling, _model docstring, arrayDataField separation) remains intact.
MikaelMayer
left a comment
There was a problem hiding this comment.
🔍 LGTM — all previous comments remain addressed. The new commit makes containsTArray, stmtExprUsesTArray, and validateHighType structurally recursive with proper termination_by/decreasing_by blocks, consistent with the prior commit's approach for the SubscriptElim walkers. The HighType.sizeOf_* lemmas in Laurel.lean follow the established pattern.
MikaelMayer
left a comment
There was a problem hiding this comment.
🔍 LGTM — all comments addressed. The new commit is a minor cleanup (anonymous have bindings in decreasing_by). All three original inline comments (constants handling, _model docstring, arrayDataField separation) remain properly addressed.
- Document `_model` parameter: accepted to satisfy LaurelPass.run's signature but unused because the caller's model predates $Array injection; SubscriptElim rebuilds via resolve. - Process `program.constants` in addition to types and staticProcedures so subscript access in constant initializers is eliminated too. Latent today (no grammar path produces it), defensive. - Rename `SeqOp.dataField` -> `arrayDataField" (moved out of `SeqOp.*` namespace) since it is a field name on the synthetic composite, not a Sequence.* operation. Addresses MikaelMayer's three inline comments on PR #1073.
17c6e19 to
470d9ec
Compare
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 LGTM — all comments addressed. The new commits add good deduplication (mkArrayUpdateStmt/mkArrayInitSplit helpers), exhaustive pattern matching in containsTArray/validateHighType, arity diagnostics, datatype-Array rejection, and comprehensive tests including the constants-walk coverage I originally flagged.
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 LGTM — all comments addressed. The new elimStmtAt refactor cleanly centralises the four duplicated statement-position dispatch arms into a single mutual helper with proper lex termination. Original inline comments (constants handling, _model docstring, arrayDataField separation) remain intact.
- Document `_model` parameter: accepted to satisfy LaurelPass.run's signature but unused because the caller's model predates $Array injection; SubscriptElim rebuilds via resolve. - Process `program.constants` in addition to types and staticProcedures so subscript access in constant initializers is eliminated too. Latent today (no grammar path produces it), defensive. - Rename `SeqOp.dataField` -> `arrayDataField" (moved out of `SeqOp.*` namespace) since it is a field name on the synthetic composite, not a Sequence.* operation. Addresses MikaelMayer's three inline comments on PR #1073.
4d31231 to
f4e0dde
Compare
MikaelMayer
left a comment
There was a problem hiding this comment.
🤖🔍 All previous comments appear addressed — reviewer sign-off still needed.
3d8b7df to
65681d8
Compare
…iagnostics Adds Seq<T> and Array<T> types with subscript read/write, a SubscriptElim lowering pass, and a ValidateSubscriptUsage diagnostics pass. Rebased onto reviewed-kbd-will-merge-to-main, which had advanced past the branch point with the bidirectional type checker (#1121), Java-style ++/-- (#1320), instance-procedure lowering (#1328/#1381), old()/heap rework (#1203/#1349), and diagnostics plumbing. Squashed into one commit. The feature was originally written against the pre-#1121 single-pass resolver, so bringing it up to date was more than a textual merge: - Resolution: the old single-pass `resolveStmtExpr` no longer exists. Subscript typing is re-expressed in the Synth/Check architecture as `Synth.subscript` (a value: read synthesizes the element type, functional update synthesizes the sequence type) and `Check.subscriptWrite` (a statement: TVoid), each with the lexicographic `(exprMd, n)` termination metric. Both check the index against `TInt` and the written/update value against the collection's element type (e.g. `a[0] := true` on an `Array<int>` is rejected), and reject a concrete non-collection target (`5[0]` -> "expected a sequence or array") while letting gradual `Unknown`/unresolved targets flow, mirroring `Fresh`/`ReferenceEquals`. The Seq-vs-Array mutability rules and bounds checks stay in ValidateSubscriptUsage. - Synth.staticCall: extended the existing polymorphic-primitive escape hatch (select/update/const) to the Sequence.*/Array.* ops, which carry placeholder `int` signatures the strict checker would otherwise reject. Result types are inferred structurally (e.g. `Sequence.build(s, v) : Seq<typeof v>`, so `[1, 2, 3] : Seq<int>`; `Sequence.empty() : Unknown`). - LaurelAST: `Array<T>` is a surface type whose lowered forms differ — `$Array` after SubscriptElim, `Composite` after TypeHierarchy — while its declaration type is never rewritten. Added a narrow carve-out so the surface and lowered forms interoperate: `isConsistent` treats `Array<T>` as consistent with `$Array`/`Composite` (covers the `$obj != a` modifies-clause guard), and `isArrayInitCompatible` admits the element-checked `Seq<U> -> Array<T>` initialization (`var a: Array<int> := [1, 2, 3]`). - LaurelCompilationPipeline: ported the inline SubscriptElim pass to the named-`def` LaurelPass architecture (`subscriptElimPass`); added `validateSubscriptUsage` after the initial `resolve` (it imports Resolution, so it can't fold into `resolve` like the diamond validator). When a program already failed subscript validation, a post-pass re-resolution failure is expected fallout, not a compiler bug, so the StrataBug guard drops it instead of mislabeling it. - Exhaustiveness: the new Subscript/SubscriptWrite constructors and base's IncrDecr both needed arms in every wildcard-free StmtExpr walker (`StmtExpr.constrName`, `mapStmtExprPrePostM`, `SubscriptElim.elimExpr`, `ValidateSubscriptUsage.validateStmtExpr`). Dropped the stale `.THeap`/ `.TTypedField` arms the new HighType no longer has. - Docs: added a Subscript section and Seq/Array typing rules to LaurelDoc.
65681d8 to
c37b371
Compare
| | .Local _ => false) | ||
| || stmtExprUsesTArray v | ||
| | .PureFieldUpdate t _ v => stmtExprUsesTArray t || stmtExprUsesTArray v | ||
| | .StaticCall _ args => args.attach.any (fun ⟨x, _⟩ => stmtExprUsesTArray x) |
There was a problem hiding this comment.
I think passes shouldn't refer to AST node types that don't relate to them. Instead, they use generic traversal code like what's in MapStmtExpr.lean
There was a problem hiding this comment.
Partly done.
Validator — done. The ~35-arm validateStmtExpr walker is gone, replaced by two generic collects over MapStmtExpr (mapProgramHighTypesM for types, mapStmtExpr for exprs); only Subscript/SubscriptWrite/StaticCall are named, so a new unrelated constructor needs no change.
elimExpr — proposing as a follow-up, because it's not a mechanical port:
- It expands one statement into two (
var a: Array<T> := …→ alloc +$datastore), spliced atBlock/If/While. The 1→1 combinators can't express that; the newmapStmtExprFlattenM(1→N) does solve this splice part. - But
mapStmtExprFlattenMis bottom-up, andelimExpr'sSubscriptarm must readcomputeExprTypeon the original target — a bottom-up port reads the already-rewritten child (aSequence.selecttypedint) and diverges for nested array-of-array access (latent today only becauseArray<T>isint-only). So the port needs a top-downprehook with manual child recursion, not a plain bottom-up map — tractable, but not a drop-in.
Happy to file elimExpr as a follow-up issue with these points.
… into feat/laurel-seq-array # Conflicts: # Strata/Languages/Laurel/Grammar/LaurelGrammar.lean # Strata/Languages/Laurel/LaurelCompilationPipeline.lean
Move validateSubscriptUsage (and its helpers) from the standalone ValidateSubscriptUsage.lean into Resolution.lean, and call it from `resolve` alongside validateDiamondFieldAccesses. This makes every non-verification diagnostic come from resolution rather than a separate pipeline pass, per reviewer feedback. The previously-cited circular-import blocker did not hold: the validator references no Resolution-only symbol, so no import inversion was needed. The pipeline's `programIsKnownInvalid` flag (which disarms the post-pass StrataBug guard) is now derived from all of resolve's errors rather than only the subscript subset. This is a deliberate widening: a program with any resolution error is already invalid, so downstream re-resolution failures on it are expected fallout, not compiler bugs.
…traversals Replace the bespoke ~30-arm validateStmtExpr walker (which enumerated every StmtExpr and HighType constructor, with its own termination proof) with two generic collects: validateHighType driven by mapProgramHighTypesM over every type annotation, and a small validateSubscriptNode (naming only Subscript / SubscriptWrite / StaticCall) driven by mapStmtExprM over every expression node. Per reviewer feedback: the pass no longer refers to AST node constructors unrelated to it, and a new unrelated StmtExpr/HighType constructor needs no change here. Only the program-level expr-bearing slots (procedures, constant initializers, constrained-type constraint/witness) are still named explicitly, since there is no whole-program StmtExpr collect. Behavior-preserving: the diagnostic set is unchanged (full Seq/Array test suite passes).
… into feat/laurel-seq-array # Conflicts: # Strata/Languages/Laurel/Grammar/LaurelGrammar.lean # Strata/Languages/Laurel/MapStmtExpr.lean
Not seriously yet — and there's a real design space rather than one shape, so I'd like your read on which you mean:
Two constraints regardless: (1) Since composite-over- |
keyboardDrummer
left a comment
There was a problem hiding this comment.
Is the addition of StrataTest/DDM/Integration/Java/testdata/ion-java-1.11.11.jar and the other one intentional? What about lsp.json ?
| -- and diamond-access validation). Its diagnostics are collected into | ||
| -- `allDiags` above; the flag additionally disarms the StrataBug guard below | ||
| -- (see there). | ||
| let programIsKnownInvalid := !result.errors.isEmpty |
There was a problem hiding this comment.
Is this still useful? I would think the filter in let newErrors := result.errors.filter fun e => !resolutionErrors.contains e already achieves a similar thing. Does it not?
There was a problem hiding this comment.
Not redundant. The filter drops post-pass errors identical to existing ones; this flag handles new but expected errors, when a pass desugars already-invalid input into something that re-resolves differently. Tested both removals: deleting it → T25 breaks (Array<bool> desugars to a new error the filter lets through, mislabeled StrataBug); early-return on initial resolve error → T9 breaks (skips LiftInstanceProcedures). Its job — run the full pipeline on invalid input without relabeling the fallout as a bug — isn't covered by either.
There was a problem hiding this comment.
when a pass desugars already-invalid input into something that re-resolves differently.
How come the error changes? Isn't that a bug?
There was a problem hiding this comment.
Not a bug — expected when a pass desugars already-invalid input. Array<bool> is caught at the initial resolve; the passes still run, and SubscriptElim desugars the rejected program into a different re-resolve error (expected 'Seq<int>', got 'Seq<bool>'). The flag fires only when resolution already errored, labeling that as expected fallout rather than StrataBug.
We can't early-return on the resolve error, because the passes still need to run — e.g. LiftInstanceProcedures moves composite methods into staticProcedures; Core ignores anything left on the composite, so skipping it silently drops methods from verification (the "not lifted" check guards that). So the flag is what keeps invalid input from mislabeling the fallout.
| -- type errors) if injected unconditionally. Gating on `usesTArray` keeps | ||
| -- array-free programs byte-identical to their feature-absent output. | ||
| let program := | ||
| if usesTArray program then |
There was a problem hiding this comment.
I would prefer a separate files for supporting Arrays and Sequences, since sequences don't depend on arrays and both are not insignificant features.
I think I'd be OK for now if Arrays don't support the dedicates subscript syntax, but rather require calls to static procedures. I know that will make the tests a little ugly but it means that Arrays can be defined fully as a library.
There was a problem hiding this comment.
On separate files: I prototyped two ways to split it, both passing the suite, both leaving the Seq module with no Array code references:
- (a) hook-seam — one
elimExprstays inSubscriptElim; array logic moves to a newArrayElim.lean, called as hooks. One pass, one resolve. - (b) two passes —
ArrayElimbecomes a separate pass running beforeSubscriptElim. Fully independent, but the two traversals are ~95% duplicated and array programs pay an extra resolve.
I lean (a). Which would you prefer? Happy to split it into its own PR.
There was a problem hiding this comment.
With (b), if we don't support the subscript syntax for Array, then we wouldn't need the extra pass right?
There was a problem hiding this comment.
Right in spirit — without a[i] sugar, arrays stop touching the shared .Subscript nodes, so there's no overlap to separate (they'd still need $Array injection + Array.length lowering, independently). I'd keep the syntax for now though. Of the two shapes I prototyped I lean (a) hook-seam — same separation, no duplicated traversal or extra resolve. Do it as a follow-up PR?
… into HEAD # Conflicts: # Strata/Languages/Laurel/Grammar/LaurelGrammar.lean # Strata/Languages/Laurel/LaurelCompilationPipeline.lean # Strata/Languages/Laurel/MapStmtExpr.lean
0a2ac85 to
97c7482
Compare
…ring Merging the base brought in #1352, whose ContractPass desugars every procedure contract into helper functions and strips the procedure's preconditions. Core's `mkContractWFProc` checks a postcondition's partial-op well-formedness assuming the procedure's preconditions (and earlier postconditions) in declaration order; with the preconditions stripped, that context is gone, so a postcondition that is well-formed only under those conditions becomes unprovable. This broke the Seq/Array contract tests (T24 `contractSeq`, T25 `setFirst`), where `Sequence.select` / `a[0]` carry bounds preconditions. Fix (no Core change): - The `$postN` postcondition helper carries the procedure's preconditions plus the earlier postconditions as *free* preconditions of the helper. Core's function-WF generation assumes a function's preconditions in order before checking the WF of its body, so a partial op in the postcondition is checked with that context in scope. Carrying them on the helper (rather than guarding the body as `assumed ==> condition`) means a call site that applies the helper learns the postcondition directly; `free` keeps them assumption-only, so they are never re-asserted there. - ContractPass keeps a procedure's non-procedure-calling preconditions as *free* instead of stripping them. The opaque path retains the postcondition natively on the procedure (it is the impl-satisfaction check, run by Core's `ProcedureEval`/`mkContractWFProc` over `spec.postconditions` — distinct from the `$post` helper, which only handles call sites). Without the preconditions in scope, that native postcondition's partial-op WF is unprovable. Call-site checking is unchanged (the `$pre` helpers still assert them). Procedure-calling preconditions are dropped from the spec (illegal as a contract expression) and enforced only via their `$pre` helper. - TransparencyPass strips preconditions from the `$asFunction` twin: now that ContractPass retains them on the procedure, the twin would otherwise inherit them and fire a duplicate WF obligation at every application. - SubscriptElim: adapt `subscriptElimPass` to the renamed `LoweringPass` and 3-argument `run` signature introduced by the base. Verified: full StrataTest suite (566 jobs) green; adversarial soundness probes pass (false pre/postconditions rejected, call-site precondition violations caught), differential-identical to base. See #1418.
`b0292cfa9` (a base-branch merge) pulled in three files that belong to
neither this feature nor `main`:
- `.kiro/settings/lsp.json` — local editor settings
- `StrataTest/DDM/Integration/Java/testdata/ion-java-1.11.{9,11}.jar` —
stray ~1.6 MB test jars at a path no code references (the ion-java
fixture used by tests lives at `StrataTestExtra/Languages/Java/testdata/`,
see `TestGen.lean` / `README.md`).
Flagged in review by keyboardDrummer.
97c7482 to
6895977
Compare
Per review (#1073): move the `$Array` backing composite out of a hand-built Lean AST value into a Laurel `#strata` source block in a new `SubscriptElimConstants.lean`, mirroring `HeapParameterizationConstants`. The declaration now reads as ordinary Laurel (`composite $Array { var $data: Seq<int> }`). An `initialize` check enforces, at module load, that the parsed composite stays in sync with the `arrayCompositeName` / `arrayDataField` constants the pass uses to read/write the field (name, single field, mutability) — using the same load-time-check idiom as `LaurelCompilationPipeline`'s ordering invariant, since `#guard` is unavailable in `module` files. Behavior is unchanged: the injected composite is identical to the previous hand-built value (full StrataTest suite green).
| -- sites); for transparent procs we strip them. Procedure-calling | ||
| -- preconditions are always dropped (illegal as a contract expression; | ||
| -- handled solely by their `$pre` helper). | ||
| preconditions := |
There was a problem hiding this comment.
okay, I get this now. I think this will be made obsolete by https://code.amazon.com/reviews/CR-285805099/revisions/1#/details, and that will probably land in GF before this lands there, so I suggest defer looking at this until then.
There was a problem hiding this comment.
Agreed it's interim — read CR-285805099, it supersedes this cleanly (clears the opaque ensures, asserts in-body via $post with input snapshotting for old), so it falls away once that lands. Leaving as-is to keep the Seq/Array tests green. That's on GF and this is the mirror, so they reconcile on sync — I'd still like your approval here so the branch isn't blocked on a stale review.
Adds
Seq<T>(immutable value sequences) andArray<T>(mutable heap-backed arrays) to Laurel: type-aware desugaring to Core's polymorphicSequence, bounds-checked subscript, type-checked index/element operands, validator diagnostics for common misuses, and aSequence.fromArraysnapshot operation.Targets
reviewed-kbd-will-merge-to-main. Rebased onto that branch after it advanced past the original branch point — most notably the bidirectional type checker (#1121), Java-style++/--(#1320), instance-procedure lowering (#1328/#1381), and theHeapParameterizationdatatype-==fix (#1349). The earlier draft caveat about carrying a standalone copy of #1349 is obsolete: #1349 is already in the base, so the duplicate has collapsed.Scope
Seq<T>(immutable values):Seq<T>types,[a, b, c]literals,s[i]read,s[i := v]functional update. NineSequence.*primitives (empty,build,select,update,length,append,contains,take,drop), declared in the Laurel prelude and backed by Core's polymorphicSequence.Array<T>(mutable, heap-backed;T = intonly for now):Array<T>types,a[i]read,a[i] := vdestructive write,Array.length(a),Sequence.fromArray(a)snapshot. Modeled by a synthetic$Arraycomposite with a$data: Seq<int>field, injected only into programs that actually useArray<T>. Participates inmodifiesclauses.Type checking
Because the feature was originally written against the pre-#1121 single-pass resolver, subscript typing is re-expressed in the new bidirectional
Synth/Checkarchitecture:Synth.subscript,Check.subscriptWrite) check the index againstTIntand the written/update value against the collection's element type, soa[0] := trueon anArray<int>is rejected at the Laurel layer with a clear message. A concrete non-collection target (5[0]) is rejected (expected a sequence or array); a gradualUnknown/TCoretarget flows silently, mirroring howFresh/ReferenceEqualstreat non-reference targets.Sequence.*/Array.*ops carry placeholderintsignatures the strict checker would reject, so they ride the same structural-inference escape hatch as the existingselect/update/constmap primitives inSynth.staticCall: the result type is inferred from the arguments (e.g.Sequence.build(s, v) : Seq<typeof v>, so[1, 2, 3] : Seq<int>) rather than checked against the placeholders.Array<T>is a surface type whose declaration is never rewritten, while its value lowers to$Array(afterSubscriptElim) thenComposite(afterTypeHierarchy). A narrow carve-out inisConsistent/isConsistentSubtype(LaurelAST.lean) keeps the surface and lowered forms interchangeable, plus an element-checkedSeq<U> → Array<T>initialization rule (var a: Array<int> := [1, 2, 3]).Bounds checking
Out-of-bounds access is a proof obligation, not a desugaring error.
Sequence.select/updatecarry0 ≤ i < lengthandtake/dropcarry0 ≤ n ≤ lengthpreconditions; the subscript sugar inherits them. Core'sPrecondElimgenerates a VC at every call site — imperative code and pure positions (requires/ensures, quantifier/function bodies) alike — classifiedoutOfBoundsAccessfor SARIF, the same treatment as division by zero.Validator diagnostics
ValidateSubscriptUsageflags misuses that are not expressible as a type error, before verification:a[i := v]onArray<T>(functional update on a mutable array).s[i] := vonSeq<T>(destructive write on an immutable sequence).Array.length(x)on a non-Array<T>, plus wrong arity forArray.length/Sequence.fromArray.Array<T>withT ≠ int(a Laurel-layer limitation, not SMT).Sequence.fromArray(x)on a non-Array<T>.The pipeline collects these alongside resolution errors; when a program already failed subscript validation, the post-pass StrataBug guard drops the expected downstream re-resolution noise rather than mislabeling it as a compiler bug.
Array<T>in datatypesSupported. An array is a heap reference, so a datatype storing one compares by reference on that field (two values built from the same array are equal; from distinct arrays, not — even with equal contents). For value semantics, store a
Seq<T>viaSequence.fromArray. (TheHeapParameterization==/!=fix this once carried standalone is now provided by #1349 in the base.)Design notes
SubscriptWritevsSubscript. Destructive writea[i] := vand functional updates[i := v]are distinct AST nodes (StmtExpr.SubscriptWritevsStmtExpr.Subscript … (some v)), set by the grammar translator. An earlier revision shared one node disambiguated by syntactic position; that coupledSubscriptElimandValidateSubscriptUsageand misclassified a functional update used directly as anif/while-branch value. The node split removes both the coupling and the bug.BoxSeqper-element-type names. A composite withSeqfields of different element types collided under a single sharedBoxSeqconstructor. Fixed with a per-element-type tag (BoxSeq_int,BoxSeq_bool, …), matching the existing per-primitiveBoxInt/BoxBoolapproach.Out of scope
Array<T>forT ≠ int(diagnostic 4) — would need per-element-type$Arrayinjection at the Laurel layer.Tests & docs
Examples/Fundamentals/T24_Sequences,T25_Arrays,T26_MixedSeqFields,T27_ArrayInDatatypecover positive use (all operations, contracts, quantifiers, aliasing, loops,modifies, snapshot semantics, array/seq in datatypes with reference/value-equality contrast), one negative case per validator diagnostic (substring-pinned), negative cases for the new index/element type checks and non-collection-target rejection, and end-to-end out-of-bounds verification failures.SubscriptElimConstantsTestpins the constants-walk.docs/verso/LaurelDoc.leandocuments the feature, the subscript typing rules, equality/aliasing semantics, and common mistakes.