diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0966d1..56e62ee 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -73,6 +73,37 @@ jobs: - name: Build run: lake build + # Self-run (DESIGN_LS_IN_LS.md §8): compile and verify the modules listed + # in LemmaScript-files.txt with this checkout's toolchain. + self-verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: Setup Node.js + uses: actions/setup-node@v6 + with: + node-version: '24' + + - name: Install tools dependencies + working-directory: tools + run: npm ci + + - uses: ./.github/actions/setup-dafny + + - name: Self-run + run: ./tools/check.sh dafny + + - name: Check self-run artifacts are up to date + run: | + git diff --exit-code -- 'tools/src/*.dfy.gen' 'tools/src/*.dfy' + untracked=$(git ls-files --others --exclude-standard -- 'tools/src/*.dfy.gen') + if [ -n "$untracked" ]; then + echo "ERROR: Untracked self-run .dfy.gen files:" + echo "$untracked" + exit 1 + fi + # Dafny verification of the examples, sharded across runners. The `tools` job # enforces drift + additions-only (regen --no-verify); this job runs the actual # `dafny verify` in parallel. Files are split by index (i % N == shard), so no diff --git a/DESIGN_LS_IN_LS.md b/DESIGN_LS_IN_LS.md index d2ab0e0..c9c6e38 100644 --- a/DESIGN_LS_IN_LS.md +++ b/DESIGN_LS_IN_LS.md @@ -499,21 +499,46 @@ regression gauntlet that grows with itself. 2. **Desugaring completeness** — after `narrow`, no `optChain`/`nullish` nodes remain (an `anyExpr`-style predicate, provable by the structural induction the prover does for us). Today enforced by "transform would - crash." -3. **Narrowing completeness** — if `resolve` narrowed a path for a branch, + crash." Note it is *not* unconditionally true: `ruleOptChain` fires only + for an optional-typed receiver and `ruleOptChainIndex` only for an array + index, so a chain with neither survives. Proving it therefore forces the + well-formedness predicate `resolve` is assumed to guarantee to be written + down — which is most of the value. The root case is already proved in + `narrow.dfy` (no optional-headed `optChain` survives a walk). +3. **The transform contract** — every `someMatch` `narrow` emits has a pure + access-path scrutinee, *or* a `someBody` that references the binder + directly. Half-enforced today: the statement form throws (`someMatch stmt + scrutinee must be a pure access path`), while the expression form takes + the no-substitution branch, so a body that was not pre-bound would + reference the scrutinee expression instead of the binder. Proving it + replaces a runtime throw plus one unchecked case with a single invariant. + Cheap now: `Inert`/`PurePath` and `binderHintFor(e).Some? ==> Inert(e)` + already exist in `narrow.dfy`. +4. **Narrowing completeness** — if `resolve` narrowed a path for a branch, `narrow` introduced the binder that unwraps it. The one property here - with a known violation: a rule may decline to fire and leave the branch + with known violations: a rule may decline to fire and leave the branch referencing the optional, which transform lowers into ill-typed backend code (TOOLS.md, known limitations). Nothing catches it but the backend's - type checker, and only if that shape is exercised. -4. **Backend name legality and injectivity** (§6.3). -5. **Arm purity** — after transform (Dafny mode), no impure call remains in + type checker, and only if that shape is exercised. A second, milder + instance — predating this branch, and present on `main` too — is that the + `&&`-drivers re-walk their residual with `walkStmt`, which applies + node-level rules only, so the discriminant rules never see it. + `if (x !== undefined && x.kind === "circle") return x.r` keeps the inner + test as an `if` and lowers the field read defensively, where the same read + without the presence check destructures in the pattern + (`ruleEarlyReturnOrChain` uses `walkStmts` and has no such gap). Both + backends still verify, so this one costs output quality rather than + correctness — but it also makes `narrow` non-idempotent, which is why + idempotence is worth proving only *after* it is fixed: the proof would + drive the change rather than decorate it. +5. **Backend name legality and injectivity** (§6.3). +6. **Arm purity** — after transform (Dafny mode), no impure call remains in a match-expression arm; a guard inside one rule becomes a postcondition of the pass. -6. **Match exhaustiveness** — every emitted `tagMatch` covers all variants +7. **Match exhaustiveness** — every emitted `tagMatch` covers all variants or has a fallthrough, checked against its `TypeDeclInfo`. -7. **Parser sanity** — `specparser` consumes the whole input or errors. -8. **Typing boundary** — successful `resolve` leaves no `unknown` where +8. **Parser sanity** — `specparser` consumes the whole input or errors. +9. **Typing boundary** — successful `resolve` leaves no `unknown` where later passes require a concrete type. Notably absent: "the generated code means the same as the TS" — that is @@ -551,6 +576,473 @@ Each is a generally useful feature independent of self-application: spec-carrying LS, pleasingly), no `JSON.stringify` equality, and check `flatMap`/`Object.entries` coverage against actual pass usage. +### 8.6 Porting doctrine: extend the toolchain, don't contort the source + +*Adopted 2026-07-22, out of the peephole port survey (§9 step 9).* + +When a module port hits a subset gap, the default is to extend the +toolchain, not rewrite the module: if the blocked idiom is one a +reasonable LemmaScript user would write, the port has found a missing +feature, and the fix belongs in the compiler — each such extension is a +generally useful feature independent of self-application (§8.5's own +test). A source rewrite is justified only when: + +- the design already condemns the idiom (module-level mutable state, + §6.1; hand-rolling a walker `ir.ts` already exports); +- the idiom is genuine parametricity, which the ecosystem's erasure + doctrine deliberately omits (§5.1); or +- verification irreducibly demands something no emitter strategy can + waive (a termination argument for non-structural recursion). + +This keeps §8.3 honest: the port grows the subset as a standing +gauntlet, rather than bending the codebase to fit today's subset. + +Empirical anchors from the survey (minimal repros, 2026-07-22): + +- **Imports-as-axioms is the P0 cross-module answer, and it already + works.** Imported functions emit as body-less `{:axiom}` functions; + imported union types re-synthesize as full datatypes. Each + self-applied module stays an independently checkable artifact. Real + linking (`include` of the already-generated `.dfy`) is the named + escalation path, triggered by the first P1 lemma that must see + through an import boundary. +- **Predicate-position HOFs verify because they lower to quantifiers** + (`.some(a => anyExpr(a, pred))` → `exists a :: a in args && …`), + whose termination Dafny discharges structurally — this is why + `ir.dfy`'s walkers verify. Rebuild-position `.map` lowers to + `Std.Collections.Seq.Map`, through which recursive calls do not + termination-check; the semantically identical seq comprehension + `seq(|s|, i requires 0 <= i < |s| => f(s[i]))` verifies. +- **Mutable-capture closures** (`let found = false` flipped inside a + lambda) are a genuine deep gap — effectful closures have no model in + the pure fragment. Not scheduled: every current use in the portable + core is a hand-rolled walker that `ir.ts` queries replace (R2 below). + +**Extension ledger** (each lands with a gauntlet run; E2 is a +deliberate output change, documented per §10.1): + +| # | Extension | Status | +|---|---|---| +| E1 | Imported named record types expand to structures (previously they synthesized opaque, so rebuilding an imported record — `{ ...a, body }` on a `MatchArm` — failed as a datatype update on an opaque type) | done (2026-07-22): the semantic import resolver stopped at anonymous union variants (`__type` symbols), so variant fields were never walked; it now recurses into them, with a compiler-`Type`-keyed visited set since recursive unions become reachable. Gauntlet byte-for-byte on both backends; one deliberate self-run artifact change: `ir.dfy`'s imported `Ty` upgraded from opaque to the full datatype, re-verified | +| E2 | Array `.map` lowers to a seq comprehension so recursive rebuild walkers termination-check (mirrors the `.some`/`.every` quantifier lowering); also drops the `Std` dependency for map | done (2026-07-22), Dafny only: one uniform lowering — `seq(\|s\|, i requires 0 <= i < \|s\| => …)` with a literal lambda beta-reduced through a `var` binding (an applied closure defeats the termination checker; verified empirically) and any other argument applied to `s[i]` directly; `Seq.Map` is gone. Index freshness is an IR-level `usesName` check (no string scanning), a local check until §6.3. Deliberate output change (§10.1): 7 gauntlet examples + `ir.dfy` re-lowered, all re-verified. Lean's `.map` is untouched — revisit with the deferred mutual-block work if Lean termination needs it | +| E3 | `const`-bound lambda aliases (`const r = (x) => walk(x, f)`) work when called from inside a nested lambda — direct calls and direct-position uses already work; only this composition breaks | done (2026-07-23), in two general halves: calls through fn-typed values (locals, parameters) classify pure — the fragment has no effectful closures, so lifting them to statement binds (which forced lambdas multi-statement) was never needed; and lambdas always take their return type from the checker, so unannotated lambdas carry a real fn type. Gauntlet byte-for-byte on both backends | +| E4 | `find` builtin: registry entry, both lowerings, matrix row (§10.2) | done (2026-07-24), back on the critical path via narrow's `typeofStringFact`: registry entry pre-existed; Dafny lowers to a `SeqFind` preamble helper with first-match ensures (mirrors `SeqFindLast`), Lean's `.find?` lowering pre-existed; pinned by `examples/arrayFind.ts` (find + optChain/nullish on the result) on both backends. The §10.2 matrix row waits with the matrix | +| E5 | Structured rejection instead of garbage output for function-valued consts and anonymous-record generic bounds (today: `type { pattern: MatchPattern; body: any }(==)` — invalid Dafny) | open — peephole's instance dissolved with R3; the general rejection is still owed. Related fixed en route (2026-07-24): a call through an indexed function value (`rules[i](e)`) crashed transform outright rather than rejecting | + +**Rewrite ledger** for `peephole.ts`, each justified by doctrine, not +subset-appeasement: + +| # | Rewrite | Justification | Status | +|---|---|---|---| +| R1 | `EXPR_RULES` mutable module global with per-backend reassignment → direct rule chain with a threaded backend flag | §6.1, already adopted | done (2026-07-23) — exactly as prescribed; a rules-array attempt confirmed the fragment has no function-valued collections (`rules[i](e)` is out), so the direct chain is what remains | +| R2 | private `mapExpr` + `containsVarRef*` → ir.ts `usesName`/`usesNameInStmts` (~100 lines deleted) | reuse-walkers agreement; also removes peephole's only mutable-capture closures | done (2026-07-23); also closed a latent hole — the private check didn't look inside lambda bodies before dropping a binding | +| R3 | `getSomeNoneArms` → two monomorphic helpers (`MatchArm` / `StmtMatchArm` arms) | genuine parametricity — its `body` is `Expr` in one instantiation, `Stmt[]` in the other; erasure doctrine (§5.1) has no honest single bound | done (2026-07-23), with direct two-arm checks (decoupled from E4) and named result records | +| R4 | fixed-point rewrite loop gets an explicit fuel parameter (`//@ decreases fuel`) | irreducible verification price; the `guard < 100` counter is fuel already. The lexicographic (match-count, if-count) measure — rules 1/6 drop match-count, rules 2–5 preserve it and drop if-count — is shelved as future P1 content | done (2026-07-24), stronger than planned: fuel rejected; the loops became plain recursion and real termination is proven Dafny-side (see the order note below). The lexicographic-count measure itself turned out unsound under rule duplication (`k in m … m[k]` copies the receiver); the proof instead weighs only the non-normal part of the term, which is immune to duplicating already-rewritten subtrees | + +Order: E1–E2 first (they gate every rebuild-style walker, not just +peephole), then E3, then E4–E5 as convenient; then R1–R4; then annotate +`peephole.ts` and add it to `LemmaScript-files.txt`. Done when peephole +is P0/T0 on Dafny in the self-run. + +*Outcome (2026-07-24): exceeded — peephole is in the self-run at P1/T0. +`peephole.dfy` (555 generated lines + ~1,400 hand-authored, additions-only +under the regen merge) machine-checks, per its `LemmaScript-files.txt` +entry (extra per-file Dafny flags after the timeout; the proof needs a Z3 +quantifier-instantiation throttle, `smt.qi.eager_threshold=30`):* + +- *termination of the whole rewrite engine — no fuel, no rule guard: the + measure weighs only the non-normal part of a term, so duplicated + receivers (already normalized by the bottom-up engine) weigh zero;* +- *normalization — engine output provably contains no firable rule and no + mergeable statement pair;* +- *idempotence — normal input provably passes through unchanged;* +- *per-rule strict decrease, and full normality of merged statements.* + +*The proof forced three engine fixes, each landed in TS gauntlet-clean: +the pair scan now iterates to a fixed point (a merge can expose a new +adjacent pair; each merge shortens the list, bounding the passes); +constructor arguments and map-literal entries joined the rebuild walk +(they were silently skipped); and `while`'s `decreasing`/`doneWith` spec +fields are documented as deliberately un-peepholed rather than silently +assumed normal. En route, two general toolchain features landed (both +gauntlet byte-for-byte): variant narrowing in resolve — positive +discriminant checks in `&&`-chains and negated checks in early-return +guards now narrow, and union field reads resolve against the narrowed +variant — and variant-aware destructor naming, where reads and datatype +updates of collision-renamed fields (`value_bool`, `body_let`) translate +correctly via ctor pinning (by field type, or match-arm context) and +union-qualified rename keys.* + +**Narrow port survey** (minimal repros, 2026-07-24; `narrow.ts` ports +together with `condition-facts.ts`, §8.3): + +- **Confirmed already-supported**, beyond the peephole precedents (spread + updates, `??` rule chains, mutual recursion, slice recursion): `Set` + locals lower to Dafny `set` with `{}`/`in`/`+` and verify; C-style `for` + loops in both directions, including non-null-asserted indexing (`xs[i]!`); + template literals — including number interpolation, which synthesizes + `NatToString`/`IntToString` (covers `freshOcBinder`'s `` `_oc${n}_val` ``). +- **Imported records with fn-valued fields synthesize opaque.** narrow reads + one bit through such a record (`builtinSpec(id).pure` in + `containsMethodCall`); the opaque `Spec(==)` synthesis makes the field + read a resolution error. Fix on the builtins side: export a scalar + accessor (`builtinPure(id): boolean`), which axiomatizes cleanly — a + first, tiny step of §8.3's "builtins.ts P1 after reshaping". +- **Three new garbage-output shapes for E5**, all avoided by the rewrites + below so none blocks the port: datatype-field assignment inside a match + arm emits `e.isDiscriminant := true;` (invalid); a generic record decl + leaks its free type var (`datatype Found = Found(check: C, …)`); + `Extract<>`/indexed-access aliases emit `type Chain["steps"](==)`. +- **`array.find` has a registry entry but no Dafny lowering** + ("Unsupported Dafny method call: .find()") — E4 returns to the critical + path via `typeofStringFact`'s `decl.variants?.find(...)`. +- **`freshName` needs no rewrite for the port**: it is deterministic + w.r.t. the module's seeded user-name set (same hint → same result), so + at P0/T0 it axiomatizes as a pure imported function. The §6.2 + `NameSupply` refactor is what the P1 *freshness contract* needs, not the + port. + +**Rewrite ledger** for `narrow.ts`/`condition-facts.ts`, per doctrine: + +| # | Rewrite | Justification | Status | +|---|---|---|---| +| N-R1 | `CondCtx.oc` mutable counter (`ctx.oc.n++` — extract rejects the expression outright) → thread it: `freshOcBinder` returns name + next ctx | §6.1/§6.2 already condemn mutation-through-reference; the first real NameSupply-shaped state | done (2026-07-25): every walker returns node + ctx (`ExprOut`/`StmtOut`/…), list walks are state-threaded recursive folds, rules take the post-recursion ctx and advance it only when they mint or re-walk; resolve's throwaway ctx site updated. Byte-for-byte — the threading provably reproduces the old mutation order | +| N-R2 | `restoreDiscriminantFlag`'s in-place `unwrapped.isDiscriminant = true` → return the rebuilt node | the pure fragment has no datatype mutation (today's emission for that shape is invalid Dafny) | done (2026-07-24) | +| N-R3 | `extractConjunct` with fn-valued `parse` → two monomorphic extractors (presence, isArray) | genuine parametricity; mirrors peephole R3 | done (2026-07-24): `leadingPresent`/`leadingIsArray` each own the tree surgery, named result records | +| N-R4 | `binderHintForMapAccess`'s regex `.replace(/_val$/…)`/`(/^_/…)` → `endsWith`/`slice` string helpers | §8.5 named exactly this; `string.replace` is not a builtin at all | done (2026-07-24) | +| N-R5 | `ruleDiscriminantChain`'s nested `function collectElse` closing over mutable `cases` → top-level recursion returning values | mutable-capture closure, the known deep gap (§8.6 anchors); extract rejects nested fn decls anyway | done (2026-07-24): `collectElseChain` returns `{cases, fallthrough}` | +| N-R6 | no-init `residualLeaves.reduce((a, b) => a \|\| b)` → recursive or-fold helper | the no-init form has no lowering (and an empty-list hazard besides) | done (2026-07-24): `orChain`, the inverse of `flattenOr` | +| N-R7 | `Extract` / `OptChain["chain"]` → use typedir's already-named `TChainStep[]` | type-level operators have no backend model; two-line change | done (2026-07-24) | +| N-R8 | narrow's `builtinSpec(id).pure` → `builtinPure(id)` scalar accessor in `builtins.ts` | fn-valued record fields don't cross the import boundary (repro above) | done (2026-07-24) | + +All eight landed gauntlet byte-for-byte on both backends, with the Dafny +self-run (typedir/ir/peephole) re-verified unchanged. + +**Proof outlook.** The engine re-walks freshly constructed terms (every +`&&`-driver re-walks its residual; `ruleEarlyReturnOrChain` duplicates the +terminating branch across None arms), so termination is again the +irreducible price, and peephole's active-weight architecture (§8.7) is the +template: weigh only un-narrowed condition material (presence checks, +`optChain`/`nullish` nodes, isArray conjuncts) — every rule strictly +consumes some of it, and duplicated already-walked branches weigh zero. +The flagship P1 property is catalog #2, desugaring completeness: engine +output provably contains no `optChain`/`nullish` node. + +Order: N-R7, N-R2, N-R4, N-R6, N-R8 first (small; each is a pure refactor +gated byte-for-byte); then E4; then N-R3, N-R5, N-R1; then annotate and +add to `LemmaScript-files.txt` (P0); then the termination + completeness +proofs (P1). *As of 2026-07-25 the rewrites and E4 are done; next is the +annotate + self-run step, then the proofs.* + +**Port progress (2026-07-25, second pass).** `condition-facts.ts` is in the +self-run at P0/T0 — 22 obligations, `LemmaScript-files.txt` entry, no hand +additions yet. `narrow.ts` generates cleanly and verifies everything except +the walker family's termination (32 verified / 13 termination errors — the +active-weight project per the proof outlook above); it joins the files list +when that proof lands. Toolchain gained en route, each gauntlet +byte-for-byte with the self-run re-verified unchanged: + +- `??` typing: the result stays optional when the right operand is optional + (rule-chain `ruleA(e) ?? ruleB(e) ?? null` — previously the chain lost + optionality one level up and narrowing couldn't rewrite it); +- expected-type propagation: union-variant literals get contextual field + types (the record case consults the discriminant-selected variant, not + just record decls); optional/array field types propagate into values; + optional-annotated lets and optional-of-array return positions propagate + through ternaries and array literals; +- bare-constructor shortcut only for truly field-less variants — a variant + with only optional fields gets its None-filled argument list + (`Ty.int_(None)`, not `Ty.int_`); +- statement-position switch arms stamp ctor pins on datatype updates of the + scrutinee (`stampScrutineeUpdates`), so collision-renamed destructors + translate in `{ ...e, body: … }` rebuild walkers — the statement twin of + the pure-switch path's pinning; +- import resolver: extern *signature* types (and the source declaration's + syntactic type nodes, which keep alias symbols) seed the imported-type + walk — previously a type reachable only through an imported function's + return (PresentFact et al.) synthesized opaque; alias extraction hoisted + above the visited-set guard (the same interned compilerType arrives with + and without its alias symbol); `NoTruncation` on extern type text (a + truncated string-literal-union expansion was unparseable); question-token + optionality normalized when the checker prints `undefined` first; +- assignment statements propagate the target's type into the RHS (mirroring + the annotated-let case), so union/array literals assigned to a declared + local resolve to their named datatypes; +- `names.ts`: `freshName(base)` + `freshNameWhere(base, taken)` — a default + parameter doesn't cross the axiom boundary; the 1-arg form axiomatizes; +- `typedecls.ts`: `TypeDecls` is no longer `readonly` (the modifier has no + backend model and made the alias synthesize opaque); +- transform's surviving-optChain/nullish crashes now print the node. + +**Termination blueprint (narrow).** All remaining errors are one SCC: +`walkExpr`/`recurseExpr`/`walkStmt`/`walkStmts`/the list folds/the +re-walking rules. The proof is peephole's architecture, hand-authored in +`narrow.dfy` under the additions-only convention: + +- a structural size family (`SE`/`SS` + padded list sizes) over the + module's `TExpr`/`TStmt` datatypes, with single-structural-step helpers + per §8.7 — *landed and verified (2026-07-25)*; +- conjunct counts (`PC`/`AC`/`DET` — presence, isArray/typeof, and + None-detector atoms in `&&`/`||` trees) and an active-weight family + (`AWE`/`AWS`/`AWSs` + per-shape helpers) summing root charges over the + term — *structure landed and verified (2026-07-25); the root charges + `WE`/`WS` are flat over-approximations and MUST be revised to exact + positional firability before wiring*. The reason charges must be exact: + rule outputs duplicate **walked** material — `presentMatchStmts`'s falsy + gate carries `none` both as the gate's else and as the None arm — and an + over-approximated charge (e.g. every `if_` charging regardless of + firability, or a block-final early-return shape whose rule only fires + with a non-empty tail) gives duplicated walked statements weight, + breaking AW-monotonicity. Exact charges make walked output provably + weightless (positional normality, in weight form), so duplicating it is + free. Consequences: the pure-function rules give exact firability as + `rule(e, ctx).Some?` directly; method rules need hand-restated guards + *including their bail conditions* (the or-chain rule's detector-key + uniqueness, `ruleIfOptionalSimple`'s non-empty-Some-branch); and the + list weight must be position-aware (the head's charge sees the tail, + for the early-return/discriminant list rules); +- walker ensures: weight-non-increase plus weight-equality-implies- + identity (inserted into the generated signatures and proven inductively + with termination); each `&&`-driver's residual re-walk loses one + conjunct. The shrink facts for the imported extractors + (`leadingPresent`, `leadingIsArray`, `noneDetector` residual bounds, + `presentMatchStmts`/`Expr` output shapes, `presentFact` requiring an + optional type) live as ensures on their `{:axiom}` signatures — a + deliberate widening of the imports-as-axioms trust surface, to be + discharged in `condition-facts.dfy` when real cross-module linking + lands (§8.6 anchors). Ctx-invariance axioms (detection reads only + `ctx.decls`) are landed; +- 4-component decreases tuples (active weight, size, list length, tier), + walkers tiered above their recurse halves; subterm walks need only + weight-monotonicity (size strictly drops), constructed-node re-walks + need weight-strictness; +- prerequisite in the source (landed 2026-07-25): `ruleEarlyReturnOrChain` + pre-walks its pieces and duplicates only walked statements into the None + arms — the measure treats walked output as inert, and duplicating + un-walked material has no decreasing measure. The other consumed rules + construct from distinct un-walked pieces (no duplication) and stay + construct-then-walk. + +**Walk stability: the load-bearing lemma (2026-07-25, second sitting).** +Exact charges create an obligation the blueprint above did not name: a rule +must never become firable *because of* the walk, or `recurseExpr`'s +rebuild could carry a charge its input never paid for. It doesn't, and the +reason is one predicate. The condition detectors match only **inert** +shapes — access paths and the boolean spines built over them (`Inert` / +`InertEs`: var/num/str/bool/havoc, field, unop, binop, index, call) — +while every rule outputs a `someMatch`, `tagMatch` or guarded ternary, +which no detector reads, and the recursive rebuild keeps the head. So: + +> **`Inert(res.expr) ==> res.expr == e`** — an inert walker result is the +> untouched input. Walking can remove a redex; it can never arm one. + +Everything else is a corollary, bundled as `Tame(e2, e, decls)`: binop-head +preservation, `PC`/`AC`/`DET` non-increase, `leadingPresent` / +`leadingIsArray` firability non-increase (via defining-equation axioms that +push firability through the `&&` spine conjunct by conjunct), and +`containsMethodCall` monotonicity — calls only accumulate, since the rules +re-embed every call they move (chain steps become real calls) and drop only +call-free path checks. Two facts feed it that the earlier list missed: +`binderHintFor(e).Some? ==> Inert(e)` and `exprEqual(a, b) ==> Inert(a) && +Inert(b)`, both honest readings of `condition-facts.ts`. + +Three corrections to the charge design fell out, each a real defect: + +- **`AWE`/`AWS` must not weigh a `someMatch` scrutinee.** The walkers + descend into the arms only (mirroring `narrow.ts`), so a weighty + scrutinee made `AWE(recurse(e)) == WE(recurse(e))` false outright. +- **The ternary charges must be merged, not summed.** Several driver + shapes hold at once — a `!Array.isArray(p)` cond is both a + presence-shaped unop and an isArray check — and a per-driver sum exceeds + what the fired rule's residual re-walk can pay back. One merged charge + `2 + 2*PC(cond) + 2*AC(cond) + AWE(then) + AWE(else)` covers the four + cond-reading drivers; the two that copy their branches rather than + re-walking them take a flat charge beside it. `AC` weighs double so that + the isArray residual's one-conjunct drop outweighs a flat charge + reappearing. +- **A rule that only ever shrinks the term can lean on the size + component**, but then weight-equality no longer implies identity, so the + walker ensures weakens to `AWE(e) == 0 ==> SE(res.expr) <= SE(e)`. + +The statement side mirrors it, with one addition: `walkStmt` must expose +enough of a surviving `if`'s children (cond tameness, branch emptiness, +branch termination, and the cond's *operands*) for `walkStmts` to prove +`HC(walked, tail) == 0` — that the walked head arms no list rule. Two +supporting facts: `isTerminatorKind` only holds of `return`/`break`/ +`continue`/`throw`, and the list walk preserves emptiness and +non-termination. `ruleOptionalIndexBinding` had to be given a charge (it +grows the term, wrapping the initializer in a bounds-guarded ternary) — a +charge of **1**, not 2, precisely so that a retyped receiver, which always +costs at least 2, can never make the charge appear in the +weight-equality case. + +**Type preservation, and why it needs no extra charge.** `ruleConditionalInMap` +is the one rule that **retypes** (`Option` → `V`), so the guards that +read a type — its own else-branch test, and the optChain drivers' receiver +test — are not walk-stable by shape alone. The closing observation is that +a retype can only come from a rule firing at the **root**: the recursive +rebuild preserves the type, so the charge that paid for the retype sits at +the root and the children are therefore weightless. Hence + +```dafny +ensures res.expr.ty == e.ty + || (AWE(e, ctx.decls) >= 2 + && (AWE(e, ctx.decls) == 2 ==> SE(res.expr) < SE(e))) +``` + +with the same clause for an `index` node's receiver. At total weight 2 the +children are unchanged in size, so `SE(recurse(e)) <= SE(e)`, and in-map's +own shrink then dominates. The `==>` rules also retype (to bool) but can +never hit the `== 2` case: `FImplOptional` forces `PC >= 1` and +`FImplArrayIsArray` forces `AC >= 1`, so their charge is at least 3. Note +what this did *not* need: charging the two discriminant list rules to +restore `AWE == 0 ==> unchanged`. That was the obvious route and it is a +trap — `FDiscChain` is tail-independent, so it charges a block-final `if`, +and a walked discriminant `if` legitimately survives inside a `someMatch` +arm (`ruleIfAndOptional` installs its inner `if` without going back through +`walkStmts`), which would break weightless-output. The size ensures is +enough. + +**The consumed sites: a multiplicative charge.** Both re-walk a +construction holding an **un-walked** copy of the terminating branch under +a **residual guard** that carries charges of its own, so the inner node's +head charge re-bills what the outer one already paid. A constant charge +cannot fix that — the inner charge has the same coefficient and cancels. +The measure was simply *additive* where a duplicating rewrite needs a +*multiplicative* one. Both sites take the same shape, scaled by a count +the rewrite provably reduces: + +```dafny +// bound-optional early return: the rewrite trades the optChain node for +// its application, so SE(innerGuard) + 1 <= SE(cond) +SE(cond) * (12 + 4 * AWSs(then_)) +// or-chain: it drops a detector, and each dropped disjunct takes at least +// two units of guard size with it +(DET(cond) + SE(cond)) * (16 + 4 * AWSs(then_)) +``` + +The or-chain's `+ SE(cond)` is what pays for the residual guard's *own* +charges in the single-leaf case, where the guard is that leaf and so can be +an `&&` chain: `PC` and `AC` are bounded by `SE`, and the guard shrinks by +at least two because the consumed detector's disjunct is gone. + +**Nonlinear arithmetic is the tax.** Three rules, learned the hard way: +a *product of two* counts is tolerable but a *product of three* poisons +Z3 — asserts that verified for weeks started timing out, so +`DET * SE * (…)` had to become `(DET + SE) * (…)`. Every multiplication +step needs an explicit `MulMonotone`/`MulStrict`/`MulAtLeast`, and the +final inequality of each site lemma must be lifted into a lemma over +**bare nats** (`OrChainArith`, `OrChainMultiArith`) so the solver has no +datatypes in scope. Name each product in a `ghost var` — left inline, Z3 +re-expands it at every use and the combining assert never closes. + +*Status (2026-07-26): **the module verifies with no assumptions** — +499 obligations, 0 errors, no timeouts, and `grep -E "^[[:space:]]*assume"` +over `narrow.dfy` is empty. The only remaining trust surface is the +imports-as-axioms boundary (facts about `condition-facts.ts`'s extractors), +to be discharged when real cross-module linking lands. It is in +`LemmaScript-files.txt` as* + +``` +tools/src/narrow.ts 300 --boogie /proverOpt:O:smt.qi.eager_threshold=30 +``` + +*A per-symbol limit above 60s means normal CI gen-checks it and +`check.sh dafny-slow` verifies it in full. The whole self-run is green: +typedir 17, ir 7, peephole 551, condition-facts 22, narrow 499. +`narrow.ts` is unchanged — the entire proof is hand additions to the +generated `.dfy`, and the additions-only gate reports zero deletions.* + +*Splitting the or-chain method was worth doing for its own sake: the +timeout had been masking three genuine unproven obligations — two loop +invariants and a missing `FOrChain` witness. Their fix also simplified the +rule, since the `firstDet` ghost bookkeeping turned out to be redundant +given the detector-count invariant, and it surfaced one more honest fact +about the imported detectors: `noneDetector`'s scrutinee always has a +binder hint, so the rule's keyless-detector branch is dead. A fourth +obligation, `WalkedHeadDeclines` (a walked head arms no list rule), needed +one missing clause — `walkStmt` exposed `isTerminating` for an `if`'s +**then** branch but not its **else**, and the early-return rule reads +whichever branch the check negates — and then had to be proved one rule at +a time; all three together do not converge.* + +Iterate with `dafny verify --filter-symbol=` — seconds per symbol +against ~25 minutes for the file, and the only practical way to work a +mutually recursive SCC this size. + +Two proof-world findings, binding for later ports: **Dafny 4.11's +translator asserts (process abort) on match-expressions as `&&`/`||` +operands in statement wellformedness** — and the `x === undefined` → match +lowering makes that easy to hit, so keep optional tests out of compound +statement guards (nested ifs / early returns); a Dafny-side `.None?` +ctor-test lowering is the named future fix (it needs an IR node, which +ripples into peephole's proof — deliberately deferred). And **`&&`-chain +leading-fact extraction can hoist a partial destructor read above its shape +guards** — total in TS where absent fields read `undefined`, partial in the +proof (`.isDiscriminant` on a non-field). Source discipline until narrow's +own completeness proof addresses it: read optional flags inside the +shape-guarded branch (see `variantFact`), bind-first for literal reads, and +split presence guards from dependent reads (resolve narrows per early +return, but not within `||` evaluation or optChain-compare guards). + +### 8.7 Proof-carrying self-run modules: conventions + +*Distilled from the peephole proof (2026-07-24); binding for later ports +(`narrow` next). `peephole.dfy` is the exemplar.* + +**Layering.** A proof-carrying `.dfy` is the generated text plus hand +additions, nothing else — `dafnyCheckDiff` enforces additions-only and the +three-way regen merge preserves the additions. Hand lines may be inserted +*between* generated lines, including inside function bodies (assert and +lemma-call statement-expressions before a case's result expression) and +between a generated signature and its `{` (requires/ensures/decreases). +Never modify a generated line, and keep the generated case *order*: when a +source change reorders the emitted match arms, relocate the hand blocks to +follow. Prefer standalone lemmas over in-body scaffolding where possible — +they cannot conflict in the merge. + +**Verifier flags.** `LemmaScript-files.txt` entries are +`filepath [timeout_seconds] [extra dafny flags…]`; a timeout above 60s +degrades the entry to gen-check in CI unless `--slow`. Peephole needs +`--boogie /proverOpt:O:smt.qi.eager_threshold=30`: the mutually recursive +predicate/measure families induce a Z3 quantifier-matching loop that makes +even trivial assertions diverge, and the threshold tames it. Expect the +same for future proof-carrying modules. + +**Dafny idioms that recur** (each cost an iteration to discover): + +- Seq-of-datatype recursion terminates only in single structural steps: + the element hop (`ss[0]`) and the field hop (`.body`) must be separate + functions — a combined `f(ss[0].body)` defeats the rank axioms. +- Definitional unfolding at a symbolic term often needs a + constructor-reconstruction assert first: + `assert e == Expr.match_(e.scrutinee, e.arms);`. A walk of an + `Option`-typed child needs the reconstruction *and* both option-hop + unfolds before its `decreases` goes through + (`assert AWOE(opt, decls) == AWE(v, decls); assert SOE(opt) == 1 + SE(v);`) + — that pattern alone cleared five `decreases` failures in `narrow.dfy`. +- A lemma invoked from inside a recursive group must keep function + applications out of its `requires` (pass pointwise facts instead), or it + joins the call graph and needs its own aligned `decreases`. +- Same-argument mutual definitions (a predicate and its kids-predicate) + need explicit tier decreases: `decreases x, 1` / `decreases x, 0`. +- Elem-bound lemmas state forall-ensures and bridge slices with + `assert forall j | 1 <= j < |es| :: es[j] == es[1..][j-1];`. + +**Peephole's proof architecture**, for orientation: a normality-predicate +family mirrors exactly what the engine can fire on (per-node rules, list +pair-redexes matching the scan, and a deliberate exclusion — `while`'s +`decreasing`/`doneWith` spec fields are never rewritten, so normality does +not claim them). The termination measure is *active weight*: normal +subtrees weigh zero — which is what makes rule-duplicated receivers +harmless with no rule guard — with root charges ordering the rule chain +(match/let 3, if 2, other 1) and a padded list weight (`PSs`) dominating +elementwise-normalized lists. Engine decreases are 4-component nat tuples +(active weight, structural size, list length, tier). Rule lemmas prove +each fired rule strictly shrinks the weight; the engine's ensures give +normalization (output is normal) and idempotence (normal input is +returned unchanged). + ## 9. Roadmap Ordered so the mechanical, immediately-paying work leads. The baseline for @@ -623,8 +1115,91 @@ with no annotation is not started.* 7. **Backend name allocator** (§6.3) with collision tests. 8. **Self-apply the leaves:** `names.ts`, portable type helpers/`typedir`, IR walkers; P1 contracts (freshness, keywords); CI self-run targets. + — *partial (2026-07-21): `typedir.ts` is the first self-compiled module — + P0/T0 on Dafny (`lsc check` on the live source: generation + Dafny + verification clean, including the mutually recursive `TExpr`/`TStmt` + datatypes and the `tyEqual`/`tysEqual` mutual recursion, whose + termination Dafny proves from default rank measures — explicit + `decreases` pragmas are unnecessary and in fact break the joint + measure). Enabling toolchain work, all gauntlet byte-for-byte: extract + recovers declared type text where the semantic printer degrades + self-referential aliases to `__type`; transform generates per-union + discriminator functions (`Ty_kind`) for surviving `.kind`-as-value + reads; both emitters sanitize constructor names from source strings + (`"spec-pure"` → Dafny `spec_pure`, Lean `«spec-pure»`). Source-side: + typedir's inline payload records are named (`TRecordField` etc. — + anonymous record types have no backend model) and `tyEqual`'s + indexed-`every` calls became recursive `tysEqual`/`stringsEqual`. + Lean self-compilation stays blocked on the deferred mutual-block + emission (step 1). Self-run wired (2026-07-22) via the ecosystem's own + convention: a root `LemmaScript-files.txt` lists the self-applied + modules (compiler only — the `examples/` gauntlet is fixtures with its + own driver, not the repo's verified surface), sources carry + `//@ backend dafny` until Lean un-defers, and `./tools/check.sh dafny` + with no arguments is the self-run. The `.dfy` artifacts are checked in + beside the sources, ready to host hand-authored P1 lemmas via the + regen merge. CI: a dedicated `self-verify` job in `ci.yml` runs the + same batch (`check.sh dafny`) with the checkout's own toolchain. + Deliberately not the reusable `verify.yml`: that workflow means + "verify against an external LemmaScript ref" (right for case studies, + and its clone path collides with the compiler repo's own workspace); + the self-run means "gate the change with its own code" — a real + semantic difference, kept visible rather than special-cased. Also + deliberately not a case-studies-matrix entry: an entry would verify + main's sources/artifacts with the PR's toolchain, going red exactly on + PRs that legitimately regenerate the self-run artifacts in-PR. This is + T0 per §8.2: the translator is trusted — the proofs are about what + this compiler generated from itself, not compiler correctness. + First P1 content (2026-07-22), lifting `typedir` to P1(partial)/T0: + **`tyEqual` is an equivalence relation** — reflexivity, symmetry, and + transitivity, each with its `tysEqual`/`stringsEqual` companion — the + property the compiler implicitly leans on wherever it dedupes or + compares types. Convention (quorum-style): each lemma is *stated as a + TypeScript function* in `typedir.ts` (`tyEqualTrans(a, b, c)` with + `//@ requires`/`//@ ensures \result === true`), so what holds is + readable without leaving TS; the inductive proof is hand-authored + inside the generated `_ensures` lemma body in `typedir.dfy`, preserved + by the regen merge, verified by the self-run (17 obligations, all + discharged; `tysEqual` also carries an auto-proved length-preservation + ensures). Note the mode rule this surfaced: once any function in a + module carries `//@ verify`, generation is opt-in — every function of + a self-applied module must be annotated. + `ir.ts` (2026-07-22): P0 on Dafny — self-compiles and verifies (the + full `Expr`/`Stmt`/decl datatype family, the mutually recursive + walkers, fn-type aliases, rest params). The last blocker was + `Match.scrutinee: string | Expr` (a bare string as shorthand for a var + scrutinee); it is now `Expr` everywhere — *type the IR seams: a string + standing in for a node at a layer boundary makes every consumer pay a + `typeof` toll, and such unions are unmodelable in the subset*. The + refactor was byte-for-byte (Dafny's scrutinee printer already escaped + both forms identically) and deleted the string branch from every match + consumer in transform, peephole, and both emitters. + Toolchain gained along the way (all gauntlet byte-for-byte): + undeclared user types synthesize opaque decls (imported types are + opaque by default — the coarse first half of the cross-module story, + matching the `_synthOpaque` doctrine); `CTOR_MAP` lookups are + `Object.hasOwn`-guarded (a variant literally named `constructor` hit + `Object.prototype.constructor`); collision-suffixed destructor names + sanitize. Source-side IR cleanups: `constructor.args` is required + (`Expr[]`, not optional — the shared-field-name/different-optionality + clash misled variant-blind field typing); ir's inline payload records + are named (`Param`, `RecordField`, `MapEntry`, `CtorInfo`, + `EmitOption`). Still ahead: the scrutinee PR (completes ir P0), + `names.ts` after its §6 refactor (first P1 freshness target), full + cross-module imports (§8.5).* 9. **`peephole`, then `narrow`** in-subset with completeness + freshness - contracts. + contracts. — *peephole done (2026-07-24), at P1/T0 with proofs beyond + the plan: machine-checked termination (fuel-free), normalization, and + idempotence; ledger outcomes and the three proof-driven engine fixes + are annotated in §8.6. `narrow` in progress: port survey done + (2026-07-24) — repro findings and the N-R1–N-R8 rewrite ledger are in + §8.6; all eight rewrites plus E4 landed (2026-07-24/25), gauntlet + byte-for-byte, self-run re-verified. Second pass (2026-07-25): + `condition-facts.ts` is P0/T0 in the self-run (22 obligations); + `narrow.ts` verifies everything but walker-family termination (32/13) — + toolchain gains and proof-world findings in §8.6. Remaining: the + termination proof (active-weight, §8.7 template), then narrow joins + `LemmaScript-files.txt`; then the desugaring-completeness contract (P1).* 10. **Portable `resolve` core, `specparser`, emitter cores** — transform ports stage-by-stage here, which is when §7's split naturally happens. 11. **First T1 experiment:** execute one generated verified pass. diff --git a/LemmaScript-files.txt b/LemmaScript-files.txt new file mode 100644 index 0000000..0f059ea --- /dev/null +++ b/LemmaScript-files.txt @@ -0,0 +1,5 @@ +tools/src/typedir.ts +tools/src/ir.ts +tools/src/peephole.ts 60 --boogie /proverOpt:O:smt.qi.eager_threshold=30 +tools/src/condition-facts.ts +tools/src/narrow.ts 300 --boogie /proverOpt:O:smt.qi.eager_threshold=30 diff --git a/tools/src/builtins.ts b/tools/src/builtins.ts index 27f4364..3ceeb00 100644 --- a/tools/src/builtins.ts +++ b/tools/src/builtins.ts @@ -153,3 +153,10 @@ export function recognizeBuiltin(objTy: Ty, method: string): BuiltinId | null { export function builtinSpec(id: BuiltinId): BuiltinSpec { return BUILTINS[id]; } + +/** The one classification bit narrow reads, as a scalar: fn-valued + * `BuiltinSpec` fields don't cross the self-run import boundary (§8.6), + * so the self-applied narrow imports this instead of `builtinSpec`. */ +export function builtinPure(id: BuiltinId): boolean { + return BUILTINS[id].pure; +} diff --git a/tools/src/condition-facts.dfy b/tools/src/condition-facts.dfy new file mode 100644 index 0000000..e86fe2a --- /dev/null +++ b/tools/src/condition-facts.dfy @@ -0,0 +1,730 @@ +// Generated by lsc from condition-facts.ts + +datatype Option = None | Some(value: T) + +function SeqFind(s: seq, p: T -> bool): Option + ensures SeqFind(s, p).Some? ==> p(SeqFind(s, p).value) + ensures SeqFind(s, p).Some? ==> SeqFind(s, p).value in s + ensures SeqFind(s, p).Some? ==> + exists i: nat :: i < |s| && s[i] == SeqFind(s, p).value && p(s[i]) && + (forall j: nat :: j < i ==> !p(s[j])) + ensures SeqFind(s, p).None? ==> forall i :: 0 <= i < |s| ==> !p(s[i]) + decreases |s| +{ + if |s| == 0 then None + else if p(s[0]) then Some(s[0]) + else SeqFind(s[1..], p) +} + +function SeqJoin(s: seq, sep: string): string + decreases |s| +{ + if |s| == 0 then "" + else if |s| == 1 then s[0] + else s[0] + sep + SeqJoin(s[1..], sep) +} + +function NatToString(n: nat): string + decreases n +{ + var digit := ('0' as int + n % 10) as char; + if n < 10 then [digit] + else NatToString(n / 10) + [digit] +} + +function IntToString(n: int): string +{ + if n < 0 then "-" + NatToString(-n) else NatToString(n) +} + +function {:axiom} freshName(base: string): string + +function {:axiom} discriminantOf(decls: TypeDecls, ty: Ty): Option + +function {:axiom} unionDeclOfTy(decls: TypeDecls, ty: Ty): Option + +datatype CondCtx = CondCtx(decls: seq, ocN: int) + +datatype MintedBinder = MintedBinder(name: string, ctx: CondCtx) + +datatype PresentFact = PresentFact(scrutinee: TExpr, innerTy: Ty, negated: bool, binder: string, truthiness: bool) + +datatype LeadingPresent = LeadingPresent(check: PresentFact, restCond: TExpr) + +datatype NoneDetector = NoneDetector(scrutinee: TExpr, innerTy: Ty, binder: string, residual: Option) + +datatype VariantFact = VariantFact(scrutinee: TExpr, scrutineeName: string, typeName: string, variant: string) + +datatype IsArrayFact = IsArrayFact(scrutinee: TExpr, typeName: string, variant: string) + +datatype LeadingIsArray = LeadingIsArray(check: IsArrayFact, restCond: TExpr) + +datatype TExpr = var_(name: string, ty: Ty) | num(value_num: int, ty: Ty) | bigint(value_bigint: string, ty: Ty) | str(value_str: string, ty: Ty) | bool_(value_bool: bool, ty: Ty) | binop(op: string, left: TExpr, right: TExpr, ty: Ty) | unop(op: string, expr: TExpr, ty: Ty) | call(fn: TExpr, args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(obj: TExpr, idx: TExpr, ty: Ty) | field(obj: TExpr, field: string, ty: Ty, isDiscriminant: Option, ofVariant: Option) | record(spread: Option, fields: seq, ty: Ty) | arrayLiteral(elems: seq, ty: Ty) | lambda(params: seq, body_lambda: seq, ty: Ty) | conditional(cond: TExpr, then_: TExpr, else_: TExpr, ty: Ty) | optChain(obj: TExpr, chain: seq, ty: Ty) | nullish(left: TExpr, right: TExpr, ty: Ty) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: TExpr, noneBody: TExpr, ty: Ty) | tagMatch(scrutinee: TExpr, typeName: string, cases: seq, fallthrough: Option, ty: Ty) | forall_(var_: string, varTy: Ty, body_forall: TExpr, ty: Ty) | exists_(var_: string, varTy: Ty, body_exists: TExpr, ty: Ty) | havoc(ty: Ty) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +datatype CallKind = unknown | pure | method_ | spec_pure + +datatype TParam = TParam(name: string, ty: Ty) + +datatype TStmt = let(name: string, ty: Ty, mutable: bool, init: TExpr) | assign(target: string, value: TExpr) | return_(value: TExpr) | break_ | continue_ | expr(expr: TExpr) | if_(cond: TExpr, then_: seq, else_: seq) | while_(cond: TExpr, invariants: seq, decreases_: Option, doneWith: Option, body: seq) | switch(expr: TExpr, discriminant: string, cases_switch: seq, defaultBody: seq) | forof(names: seq, nameTypes: seq, iterable: TExpr, invariants: seq, doneWith: Option, body: seq) | throw | ghostLet(name: string, ty: Ty, init: TExpr) | ghostAssign(target: string, value: TExpr) | assert_(expr: TExpr, assumed: Option) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: seq, noneBody: seq) | tagMatch(scrutinee: TExpr, typeName: string, cases_tagMatch: seq, fallthrough: seq) + +datatype TSwitchCase = TSwitchCase(label_: string, body: seq) + +datatype TStmtCase = TStmtCase(variant: string, body: seq) + +datatype TChainStep = field(name: string, ty: Ty) | call(args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(idx: TExpr, ty: Ty) + +datatype TExprCase = TExprCase(variant: string, body: TExpr) + +datatype TRecordField = TRecordField(name: string, value: TExpr) + +type TypeDecls = seq + +datatype TypeDeclInfo = TypeDeclInfo(name: string, typeParams: Option>, kind: string, values: Option>, discriminant: Option, variants: Option>, fields: Option>, aliasOf: Option, aliasOfTy: Option) + +datatype VariantInfo = VariantInfo(name: string, fields: seq) + +datatype FieldInfo = FieldInfo(name: string, tsType: string, type_: Option) + +function TExpr_kind(t: TExpr): string +{ + match t { + case var_(i_0, i_1) => + "var" + case num(i_0, i_1) => + "num" + case bigint(i_0, i_1) => + "bigint" + case str(i_0, i_1) => + "str" + case bool_(i_0, i_1) => + "bool" + case binop(i_0, i_1, i_2, i_3) => + "binop" + case unop(i_0, i_1, i_2) => + "unop" + case call(i_0, i_1, i_2, i_3, i_4) => + "call" + case index(i_0, i_1, i_2) => + "index" + case field(i_0, i_1, i_2, i_3, i_4) => + "field" + case record(i_0, i_1, i_2) => + "record" + case arrayLiteral(i_0, i_1) => + "arrayLiteral" + case lambda(i_0, i_1, i_2) => + "lambda" + case conditional(i_0, i_1, i_2, i_3) => + "conditional" + case optChain(i_0, i_1, i_2) => + "optChain" + case nullish(i_0, i_1, i_2) => + "nullish" + case someMatch(i_0, i_1, i_2, i_3, i_4, i_5) => + "someMatch" + case tagMatch(i_0, i_1, i_2, i_3, i_4) => + "tagMatch" + case forall_(i_0, i_1, i_2, i_3) => + "forall" + case exists_(i_0, i_1, i_2, i_3) => + "exists" + case havoc(i_0) => + "havoc" + } +} + +function Ty_kind(t: Ty): string +{ + match t { + case bool_ => + "bool" + case nat_(i_0) => + "nat" + case int_(i_0) => + "int" + case real_ => + "real" + case string_(i_0) => + "string" + case void => + "void" + case array_(i_0) => + "array" + case tuple(i_0) => + "tuple" + case map_(i_0, i_1) => + "map" + case set_(i_0) => + "set" + case optional(i_0) => + "optional" + case user(i_0) => + "user" + case fn(i_0, i_1) => + "fn" + case unknown => + "unknown" + } +} + +function freshOcBinder(ctx: CondCtx): MintedBinder +{ + MintedBinder(freshName((("_oc" + IntToString(ctx.ocN)) + "_val")), ctx.(ocN := (ctx.ocN + 1))) +} + +function flattenOr(e: TExpr): seq +{ + if (e.binop? && (e.op == "||")) then + (flattenOr(e.left) + flattenOr(e.right)) + else + [e] +} + +function restoreDiscriminantFlag(unwrapped: TExpr, decls: seq): TExpr +{ + if (unwrapped.field? && (match discriminantOf(decls, unwrapped.obj.ty) { case Some(i_value) => (i_value == unwrapped.field) case None => false })) then + unwrapped.(isDiscriminant := Some(true)) + else + unwrapped +} + +function arrayBoundsCond(arr: TExpr, idx: TExpr): TExpr +{ + var len := TExpr.field(arr, "length", int_(None), None, None); + var lo := binop("<=", num(0, int_(None)), idx, Ty.bool_); + var hi := binop("<", idx, len, Ty.bool_); + binop("&&", lo, hi, Ty.bool_) +} + +function exprEqual(a: TExpr, b: TExpr): bool +{ + if (TExpr_kind(a) != TExpr_kind(b)) then + false + else + if (a.var_? && b.var_?) then + (a.name == b.name) + else + if (a.field? && b.field?) then + ((a.field == b.field) && exprEqual(a.obj, b.obj)) + else + ((a.index? && b.index?) && (exprEqual(a.obj, b.obj) && exprEqual(a.idx, b.idx))) +} + +function stripValSuffix(s: string): string +{ + if (|s| >= |"_val"| && s[|s|-|"_val"|..] == "_val") then + s[0..(|s| - 4)] + else + s +} + +function stripLeadingUnderscore(s: string): string +{ + if (|s| >= |"_"| && s[..|"_"|] == "_") then + s[1..] + else + s +} + +function isNarrowablePath(e: TExpr): bool +{ + match e { + case var_(i_e_name, i_e_ty) => + true + case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => + isNarrowablePath(i_e_obj) + case _ => + false + } +} + +function isArrayFact(call: TExpr, ctx: CondCtx): Option +{ + match call { + case call(i_call_fn, i_call_args, i_call_ty, i_call_callKind, i_call_builtinId) => + if ((!i_call_fn.field?) || (i_call_fn.field != "isArray")) then + None + else + if ((!i_call_fn.obj.var_?) || (i_call_fn.obj.name != "Array")) then + None + else + if (|i_call_args| != 1) then + None + else + var arg := i_call_args[0]; + if (!(isNarrowablePath(arg)) || (!arg.ty.user?)) then + None + else + var decl := unionDeclOfTy(ctx.decls, arg.ty); + match decl { + case Some(i_decl_val) => + if (match i_decl_val.discriminant { case Some(i_value) => (i_value != "__isArray__") case None => true }) then + None + else + Some(IsArrayFact(arg, arg.ty.name, "ArrayBranch")) + case None => + None + } + case _ => + None + } +} + +function typeofStringFact(e: TExpr, ctx: CondCtx): Option +{ + if ((!e.binop?) || (e.op != "===")) then + None + else + var tof := (if (e.left.unop? && (e.left.op == "typeof")) then Option.Some(e.left.expr) else (if (e.right.unop? && (e.right.op == "typeof")) then Option.Some(e.right.expr) else Option.None)); + match tof { + case Some(i_tof_val) => + var strOnLeft := e.left.str?; + var litE := (if strOnLeft then e.left else e.right); + match litE { + case str(i_litE_value, i_litE_ty) => + if (i_litE_value != "string") then + None + else + if (!(isNarrowablePath(i_tof_val)) || (!i_tof_val.ty.user?)) then + None + else + var decl := unionDeclOfTy(ctx.decls, i_tof_val.ty); + match decl { + case Some(i_decl_val) => + if (match i_decl_val.discriminant { case Some(i_value) => (i_value != "__isArray__") case None => true }) then + None + else + match i_decl_val.variants { + case Some(i_decl_variants_val) => + var nab := SeqFind(i_decl_variants_val, (v: VariantInfo) => (v.name == "NonArrayBranch")); + match nab { + case Some(i_nab_val) => + var valField := SeqFind(i_nab_val.fields, (f: FieldInfo) => (f.name == "val")); + match valField { + case Some(i_valField_val) => + var valTy := i_valField_val.type_; + match valTy { + case Some(i_valTy_val) => + match i_valTy_val { + case string_(i__valTy_val_values) => + Some(IsArrayFact(i_tof_val, i_tof_val.ty.name, "NonArrayBranch")) + case _ => + None + } + case None => + None + } + case None => + None + } + case None => + None + } + case None => + None + } + case None => + None + } + case _ => + None + } + case None => + None + } +} + +function leadingIsArray(cond: TExpr, ctx: CondCtx): Option +{ + if ((!cond.binop?) || (cond.op != "&&")) then + None + else + var left := isArrayFact(cond.left, ctx); + match left { + case Some(i_left_val) => + Some(LeadingIsArray(i_left_val, cond.right)) + case None => + var right := isArrayFact(cond.right, ctx); + match right { + case Some(i_right_val) => + Some(LeadingIsArray(i_right_val, cond.left)) + case None => + if (cond.left.binop? && (cond.left.op == "&&")) then + var inner := leadingIsArray(cond.left, ctx); + match inner { + case Some(i_inner_val) => + Some(LeadingIsArray(i_inner_val.check, cond.(left := i_inner_val.restCond))) + case None => + if (cond.right.binop? && (cond.right.op == "&&")) then + var inner := leadingIsArray(cond.right, ctx); + match inner { + case Some(i_inner_val) => + Some(LeadingIsArray(i_inner_val.check, cond.(right := i_inner_val.restCond))) + case None => + None + } + else + None + } + else + if (cond.right.binop? && (cond.right.op == "&&")) then + var inner := leadingIsArray(cond.right, ctx); + match inner { + case Some(i_inner_val) => + Some(LeadingIsArray(i_inner_val.check, cond.(right := i_inner_val.restCond))) + case None => + None + } + else + None + } + } +} + +function negVariantFact(cond: TExpr, ctx: CondCtx): Option +{ + if ((((cond.binop? && (cond.op == "!==")) && cond.left.field?) && cond.left.obj.var_?) && cond.left.obj.ty.user?) then + var disc := cond.left.isDiscriminant; + match disc { + case Some(i_disc_val) => + if i_disc_val then + var lit := cond.right; + match lit { + case str(i_lit_value, i_lit_ty) => + Some(VariantFact(cond.left.obj, cond.left.obj.name, cond.left.obj.ty.name, i_lit_value)) + case _ => + None + } + else + None + case None => + None + } + else + if (cond.unop? && (cond.op == "!")) then + var arrCheck := isArrayFact(cond.expr, ctx); + match arrCheck { + case Some(i_arrCheck_val) => + if i_arrCheck_val.scrutinee.var_? then + Some(VariantFact(i_arrCheck_val.scrutinee, i_arrCheck_val.scrutinee.name, i_arrCheck_val.typeName, "NonArrayBranch")) + else + None + case None => + None + } + else + None +} + +function presentMatchStmts(f: PresentFact, some: seq, none: seq): TStmt +{ + TStmt.someMatch(f.scrutinee, f.binder, f.innerTy, (if canBeFalsy(f) then [if_(bound(f), some, none)] else some), none) +} + +function presentMatchExpr(f: PresentFact, some: TExpr, none: TExpr, ty: Ty): TExpr +{ + TExpr.someMatch(f.scrutinee, f.binder, f.innerTy, (if canBeFalsy(f) then conditional(bound(f), some, none, ty) else some), none, ty) +} + +function isFalsyCapableTy(ty: Ty): bool +{ + (Ty_kind(ty) in ["int", "nat", "string", "bool"]) +} + +function canBeFalsy(f: PresentFact): bool +{ + (f.truthiness && isFalsyCapableTy(f.innerTy)) +} + +function bound(f: PresentFact): TExpr +{ + var_(f.binder, f.innerTy) +} + +method binderHintFor(e: TExpr) returns (res: Option) +{ + var fields: seq := []; + var cur := e; + while cur.field? + decreases cur + { + fields := ([cur.field] + fields); + cur := cur.obj; + } + match cur { + case var_(i_cur_name, i_cur_ty) => + var root := (if (i_cur_name == "\\result") then "result" else i_cur_name); + return Some((if (|fields| == 0) then (("_" + root) + "_val") else (((("_" + root) + "_") + SeqJoin(fields, "_")) + "_val"))); + case _ => + return None; + } +} + +method presentFact(cond: TExpr) returns (res: Option) +{ + if ((cond.unop? && (cond.op == "!")) && cond.expr.ty.optional?) { + var e := cond.expr; + var innerTy := cond.expr.ty.inner; + var i_t0 := binderHintFor(e); + var hint := i_t0; + match hint { + case Some(i_hint_val) => + return Some(PresentFact(e, innerTy, true, freshName(i_hint_val), true)); + case None => + return None; + } + } + if ((!cond.binop?) || ((cond.op != "!==") && (cond.op != "==="))) { + if cond.ty.optional? { + var i_t1 := binderHintFor(cond); + var hint := i_t1; + match hint { + case Some(i_hint_val) => + return Some(PresentFact(cond, cond.ty.inner, false, freshName(i_hint_val), true)); + case None => + return None; + } + } + return None; + } + var e := None; + if (cond.right.var_? && (cond.right.name == "undefined")) { + e := Some(cond.left); + } + if (cond.left.var_? && (cond.left.name == "undefined")) { + e := Some(cond.right); + } + match e { + case Some(i_e_val) => + if (!i_e_val.ty.optional?) { + return None; + } + var i_t2 := binderHintFor(i_e_val); + var hint := i_t2; + match hint { + case Some(i_hint_val) => + return Some(PresentFact(i_e_val, i_e_val.ty.inner, (cond.op == "==="), freshName(i_hint_val), false)); + case None => + return None; + } + case None => + return None; + } +} + +method positivePresent(e: TExpr) returns (res: Option) +{ + var i_t3 := presentFact(e); + var f := i_t3; + match f { + case Some(i_f_val) => + if i_f_val.negated { + return None; + } + return Some(i_f_val); + case None => + return None; + } +} + +method leadingPresent(cond: TExpr) returns (res: Option) +{ + if ((!cond.binop?) || (cond.op != "&&")) { + return None; + } + var i_t4 := positivePresent(cond.left); + var left := i_t4; + match left { + case Some(i_left_val) => + return Some(LeadingPresent(i_left_val, cond.right)); + case None => + + } + var i_t5 := positivePresent(cond.right); + var right := i_t5; + match right { + case Some(i_right_val) => + return Some(LeadingPresent(i_right_val, cond.left)); + case None => + + } + if (cond.left.binop? && (cond.left.op == "&&")) { + var i_t6 := leadingPresent(cond.left); + var inner := i_t6; + match inner { + case Some(i_inner_val) => + return Some(LeadingPresent(i_inner_val.check, cond.(left := i_inner_val.restCond))); + case None => + + } + } + if (cond.right.binop? && (cond.right.op == "&&")) { + var i_t7 := leadingPresent(cond.right); + var inner := i_t7; + match inner { + case Some(i_inner_val) => + return Some(LeadingPresent(i_inner_val.check, cond.(right := i_inner_val.restCond))); + case None => + + } + } + return None; +} + +method noneDetector(leaf: TExpr, ctx: CondCtx) returns (res: Option) +{ + if (leaf.binop? && (leaf.op == "!==")) { + var ocOnLeft := leaf.left.optChain?; + var oc := (if ocOnLeft then Option.Some(leaf.left) else (if leaf.right.optChain? then Option.Some(leaf.right) else Option.None)); + match oc { + case Some(i_oc_val) => + if (i_oc_val.optChain? && i_oc_val.obj.ty.optional?) { + var i_t8 := binderHintFor(i_oc_val.obj); + var hint := i_t8; + match hint { + case Some(i_hint_val) => + var binder := freshName(i_hint_val); + var i_t10 := applyChain(var_(binder, i_oc_val.obj.ty.inner), i_oc_val.chain); + var i_t9 := restoreDiscriminantFlag(i_t10, ctx.decls); + var unwrapped := i_t9; + var lit := (if ocOnLeft then leaf.right else leaf.left); + return Some(NoneDetector(i_oc_val.obj, i_oc_val.obj.ty.inner, binder, Some(binop("!==", unwrapped, lit, Ty.bool_)))); + case None => + return None; + } + } + case None => + + } + } + var i_t11 := presentFact(leaf); + var f := i_t11; + match f { + case Some(i_f_val) => + if i_f_val.negated { + var i_t12 := canBeFalsy(i_f_val); + var residual := (if i_t12 then Option.Some(unop("!", var_(i_f_val.binder, i_f_val.innerTy), Ty.bool_)) else Option.None); + return Some(NoneDetector(i_f_val.scrutinee, i_f_val.innerTy, i_f_val.binder, residual)); + } + case None => + + } + return None; +} + +method applyChain(body: TExpr, chain: seq) returns (res: TExpr) +{ + var acc := body; + var i_step_idx := 0; + while i_step_idx < |chain| + invariant (i_step_idx <= |chain|) + { + var step := chain[i_step_idx]; + match step { + case field(i_step_name, i_step_ty) => + acc := TExpr.field(acc, i_step_name, i_step_ty, None, None); + case index(i_step_idx, i_step_ty) => + acc := TExpr.index(acc, i_step_idx, i_step_ty); + case call(i_step_args, i_step_ty, i_step_callKind, i_step_builtinId) => + match i_step_builtinId { + case Some(i_step_builtinId_val) => + acc := TExpr.call(acc, i_step_args, i_step_ty, i_step_callKind, Some(i_step_builtinId_val)); + case None => + acc := TExpr.call(acc, i_step_args, i_step_ty, i_step_callKind, None); + } + } + i_step_idx := i_step_idx + 1; + } + return acc; +} + +method binderHintForMapAccess(m: TExpr, k: TExpr, ctx: CondCtx) returns (res: MintedBinder) +{ + var i_t13 := binderHintFor(m); + var mHint := i_t13; + var i_t14 := binderHintFor(k); + var kHint := i_t14; + match mHint { + case Some(i_mHint_val) => + if (|i_mHint_val| > 0) { + match kHint { + case Some(i_kHint_val) => + if (|i_kHint_val| > 0) { + var i_t15 := stripValSuffix(i_mHint_val); + var mStem := i_t15; + var i_t17 := stripLeadingUnderscore(i_kHint_val); + var i_t16 := stripValSuffix(i_t17); + var kStem := i_t16; + return MintedBinder(freshName((((("" + mStem) + "_") + kStem) + "_val")), ctx); + } + case None => + + } + } + case None => + + } + var i_t18 := freshOcBinder(ctx); + return i_t18; +} + +method variantFact(cond: TExpr, ctx: CondCtx) returns (res: Option) +{ + if ((((cond.binop? && (cond.op == "===")) && cond.left.field?) && cond.left.obj.var_?) && cond.left.obj.ty.user?) { + var disc := cond.left.isDiscriminant; + match disc { + case Some(i_disc_val) => + if i_disc_val { + var lit := cond.right; + match lit { + case str(i_lit_value, i_lit_ty) => + return Some(VariantFact(cond.left.obj, cond.left.obj.name, cond.left.obj.ty.name, i_lit_value)); + case _ => + return None; + } + } else { + return None; + } + case None => + return None; + } + } + if (((cond.binop? && (cond.op == "in")) && cond.right.var_?) && cond.right.ty.user?) { + var keyLit := cond.left; + match keyLit { + case str(i_keyLit_value, i_keyLit_ty) => + var key := i_keyLit_value; + var typeName := cond.right.ty.name; + var decl := unionDeclOfTy(ctx.decls, cond.right.ty); + match decl { + case Some(i_decl_val) => + match i_decl_val.variants { + case Some(i_decl_variants_val) => + var matches := Std.Collections.Seq.Filter((v: VariantInfo) => (exists f :: f in v.fields && (f.name == key)), i_decl_variants_val); + if (|matches| == 1) { + return Some(VariantFact(cond.right, cond.right.name, typeName, matches[0].name)); + } + case None => + + } + case None => + + } + case _ => + return None; + } + } + var i_t19 := isArrayFact(cond, ctx); + var arrCheck := i_t19; + match arrCheck { + case Some(i_arrCheck_val) => + if i_arrCheck_val.scrutinee.var_? { + return Some(VariantFact(i_arrCheck_val.scrutinee, i_arrCheck_val.scrutinee.name, i_arrCheck_val.typeName, i_arrCheck_val.variant)); + } + case None => + + } + return None; +} diff --git a/tools/src/condition-facts.dfy.gen b/tools/src/condition-facts.dfy.gen new file mode 100644 index 0000000..e86fe2a --- /dev/null +++ b/tools/src/condition-facts.dfy.gen @@ -0,0 +1,730 @@ +// Generated by lsc from condition-facts.ts + +datatype Option = None | Some(value: T) + +function SeqFind(s: seq, p: T -> bool): Option + ensures SeqFind(s, p).Some? ==> p(SeqFind(s, p).value) + ensures SeqFind(s, p).Some? ==> SeqFind(s, p).value in s + ensures SeqFind(s, p).Some? ==> + exists i: nat :: i < |s| && s[i] == SeqFind(s, p).value && p(s[i]) && + (forall j: nat :: j < i ==> !p(s[j])) + ensures SeqFind(s, p).None? ==> forall i :: 0 <= i < |s| ==> !p(s[i]) + decreases |s| +{ + if |s| == 0 then None + else if p(s[0]) then Some(s[0]) + else SeqFind(s[1..], p) +} + +function SeqJoin(s: seq, sep: string): string + decreases |s| +{ + if |s| == 0 then "" + else if |s| == 1 then s[0] + else s[0] + sep + SeqJoin(s[1..], sep) +} + +function NatToString(n: nat): string + decreases n +{ + var digit := ('0' as int + n % 10) as char; + if n < 10 then [digit] + else NatToString(n / 10) + [digit] +} + +function IntToString(n: int): string +{ + if n < 0 then "-" + NatToString(-n) else NatToString(n) +} + +function {:axiom} freshName(base: string): string + +function {:axiom} discriminantOf(decls: TypeDecls, ty: Ty): Option + +function {:axiom} unionDeclOfTy(decls: TypeDecls, ty: Ty): Option + +datatype CondCtx = CondCtx(decls: seq, ocN: int) + +datatype MintedBinder = MintedBinder(name: string, ctx: CondCtx) + +datatype PresentFact = PresentFact(scrutinee: TExpr, innerTy: Ty, negated: bool, binder: string, truthiness: bool) + +datatype LeadingPresent = LeadingPresent(check: PresentFact, restCond: TExpr) + +datatype NoneDetector = NoneDetector(scrutinee: TExpr, innerTy: Ty, binder: string, residual: Option) + +datatype VariantFact = VariantFact(scrutinee: TExpr, scrutineeName: string, typeName: string, variant: string) + +datatype IsArrayFact = IsArrayFact(scrutinee: TExpr, typeName: string, variant: string) + +datatype LeadingIsArray = LeadingIsArray(check: IsArrayFact, restCond: TExpr) + +datatype TExpr = var_(name: string, ty: Ty) | num(value_num: int, ty: Ty) | bigint(value_bigint: string, ty: Ty) | str(value_str: string, ty: Ty) | bool_(value_bool: bool, ty: Ty) | binop(op: string, left: TExpr, right: TExpr, ty: Ty) | unop(op: string, expr: TExpr, ty: Ty) | call(fn: TExpr, args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(obj: TExpr, idx: TExpr, ty: Ty) | field(obj: TExpr, field: string, ty: Ty, isDiscriminant: Option, ofVariant: Option) | record(spread: Option, fields: seq, ty: Ty) | arrayLiteral(elems: seq, ty: Ty) | lambda(params: seq, body_lambda: seq, ty: Ty) | conditional(cond: TExpr, then_: TExpr, else_: TExpr, ty: Ty) | optChain(obj: TExpr, chain: seq, ty: Ty) | nullish(left: TExpr, right: TExpr, ty: Ty) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: TExpr, noneBody: TExpr, ty: Ty) | tagMatch(scrutinee: TExpr, typeName: string, cases: seq, fallthrough: Option, ty: Ty) | forall_(var_: string, varTy: Ty, body_forall: TExpr, ty: Ty) | exists_(var_: string, varTy: Ty, body_exists: TExpr, ty: Ty) | havoc(ty: Ty) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +datatype CallKind = unknown | pure | method_ | spec_pure + +datatype TParam = TParam(name: string, ty: Ty) + +datatype TStmt = let(name: string, ty: Ty, mutable: bool, init: TExpr) | assign(target: string, value: TExpr) | return_(value: TExpr) | break_ | continue_ | expr(expr: TExpr) | if_(cond: TExpr, then_: seq, else_: seq) | while_(cond: TExpr, invariants: seq, decreases_: Option, doneWith: Option, body: seq) | switch(expr: TExpr, discriminant: string, cases_switch: seq, defaultBody: seq) | forof(names: seq, nameTypes: seq, iterable: TExpr, invariants: seq, doneWith: Option, body: seq) | throw | ghostLet(name: string, ty: Ty, init: TExpr) | ghostAssign(target: string, value: TExpr) | assert_(expr: TExpr, assumed: Option) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: seq, noneBody: seq) | tagMatch(scrutinee: TExpr, typeName: string, cases_tagMatch: seq, fallthrough: seq) + +datatype TSwitchCase = TSwitchCase(label_: string, body: seq) + +datatype TStmtCase = TStmtCase(variant: string, body: seq) + +datatype TChainStep = field(name: string, ty: Ty) | call(args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(idx: TExpr, ty: Ty) + +datatype TExprCase = TExprCase(variant: string, body: TExpr) + +datatype TRecordField = TRecordField(name: string, value: TExpr) + +type TypeDecls = seq + +datatype TypeDeclInfo = TypeDeclInfo(name: string, typeParams: Option>, kind: string, values: Option>, discriminant: Option, variants: Option>, fields: Option>, aliasOf: Option, aliasOfTy: Option) + +datatype VariantInfo = VariantInfo(name: string, fields: seq) + +datatype FieldInfo = FieldInfo(name: string, tsType: string, type_: Option) + +function TExpr_kind(t: TExpr): string +{ + match t { + case var_(i_0, i_1) => + "var" + case num(i_0, i_1) => + "num" + case bigint(i_0, i_1) => + "bigint" + case str(i_0, i_1) => + "str" + case bool_(i_0, i_1) => + "bool" + case binop(i_0, i_1, i_2, i_3) => + "binop" + case unop(i_0, i_1, i_2) => + "unop" + case call(i_0, i_1, i_2, i_3, i_4) => + "call" + case index(i_0, i_1, i_2) => + "index" + case field(i_0, i_1, i_2, i_3, i_4) => + "field" + case record(i_0, i_1, i_2) => + "record" + case arrayLiteral(i_0, i_1) => + "arrayLiteral" + case lambda(i_0, i_1, i_2) => + "lambda" + case conditional(i_0, i_1, i_2, i_3) => + "conditional" + case optChain(i_0, i_1, i_2) => + "optChain" + case nullish(i_0, i_1, i_2) => + "nullish" + case someMatch(i_0, i_1, i_2, i_3, i_4, i_5) => + "someMatch" + case tagMatch(i_0, i_1, i_2, i_3, i_4) => + "tagMatch" + case forall_(i_0, i_1, i_2, i_3) => + "forall" + case exists_(i_0, i_1, i_2, i_3) => + "exists" + case havoc(i_0) => + "havoc" + } +} + +function Ty_kind(t: Ty): string +{ + match t { + case bool_ => + "bool" + case nat_(i_0) => + "nat" + case int_(i_0) => + "int" + case real_ => + "real" + case string_(i_0) => + "string" + case void => + "void" + case array_(i_0) => + "array" + case tuple(i_0) => + "tuple" + case map_(i_0, i_1) => + "map" + case set_(i_0) => + "set" + case optional(i_0) => + "optional" + case user(i_0) => + "user" + case fn(i_0, i_1) => + "fn" + case unknown => + "unknown" + } +} + +function freshOcBinder(ctx: CondCtx): MintedBinder +{ + MintedBinder(freshName((("_oc" + IntToString(ctx.ocN)) + "_val")), ctx.(ocN := (ctx.ocN + 1))) +} + +function flattenOr(e: TExpr): seq +{ + if (e.binop? && (e.op == "||")) then + (flattenOr(e.left) + flattenOr(e.right)) + else + [e] +} + +function restoreDiscriminantFlag(unwrapped: TExpr, decls: seq): TExpr +{ + if (unwrapped.field? && (match discriminantOf(decls, unwrapped.obj.ty) { case Some(i_value) => (i_value == unwrapped.field) case None => false })) then + unwrapped.(isDiscriminant := Some(true)) + else + unwrapped +} + +function arrayBoundsCond(arr: TExpr, idx: TExpr): TExpr +{ + var len := TExpr.field(arr, "length", int_(None), None, None); + var lo := binop("<=", num(0, int_(None)), idx, Ty.bool_); + var hi := binop("<", idx, len, Ty.bool_); + binop("&&", lo, hi, Ty.bool_) +} + +function exprEqual(a: TExpr, b: TExpr): bool +{ + if (TExpr_kind(a) != TExpr_kind(b)) then + false + else + if (a.var_? && b.var_?) then + (a.name == b.name) + else + if (a.field? && b.field?) then + ((a.field == b.field) && exprEqual(a.obj, b.obj)) + else + ((a.index? && b.index?) && (exprEqual(a.obj, b.obj) && exprEqual(a.idx, b.idx))) +} + +function stripValSuffix(s: string): string +{ + if (|s| >= |"_val"| && s[|s|-|"_val"|..] == "_val") then + s[0..(|s| - 4)] + else + s +} + +function stripLeadingUnderscore(s: string): string +{ + if (|s| >= |"_"| && s[..|"_"|] == "_") then + s[1..] + else + s +} + +function isNarrowablePath(e: TExpr): bool +{ + match e { + case var_(i_e_name, i_e_ty) => + true + case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => + isNarrowablePath(i_e_obj) + case _ => + false + } +} + +function isArrayFact(call: TExpr, ctx: CondCtx): Option +{ + match call { + case call(i_call_fn, i_call_args, i_call_ty, i_call_callKind, i_call_builtinId) => + if ((!i_call_fn.field?) || (i_call_fn.field != "isArray")) then + None + else + if ((!i_call_fn.obj.var_?) || (i_call_fn.obj.name != "Array")) then + None + else + if (|i_call_args| != 1) then + None + else + var arg := i_call_args[0]; + if (!(isNarrowablePath(arg)) || (!arg.ty.user?)) then + None + else + var decl := unionDeclOfTy(ctx.decls, arg.ty); + match decl { + case Some(i_decl_val) => + if (match i_decl_val.discriminant { case Some(i_value) => (i_value != "__isArray__") case None => true }) then + None + else + Some(IsArrayFact(arg, arg.ty.name, "ArrayBranch")) + case None => + None + } + case _ => + None + } +} + +function typeofStringFact(e: TExpr, ctx: CondCtx): Option +{ + if ((!e.binop?) || (e.op != "===")) then + None + else + var tof := (if (e.left.unop? && (e.left.op == "typeof")) then Option.Some(e.left.expr) else (if (e.right.unop? && (e.right.op == "typeof")) then Option.Some(e.right.expr) else Option.None)); + match tof { + case Some(i_tof_val) => + var strOnLeft := e.left.str?; + var litE := (if strOnLeft then e.left else e.right); + match litE { + case str(i_litE_value, i_litE_ty) => + if (i_litE_value != "string") then + None + else + if (!(isNarrowablePath(i_tof_val)) || (!i_tof_val.ty.user?)) then + None + else + var decl := unionDeclOfTy(ctx.decls, i_tof_val.ty); + match decl { + case Some(i_decl_val) => + if (match i_decl_val.discriminant { case Some(i_value) => (i_value != "__isArray__") case None => true }) then + None + else + match i_decl_val.variants { + case Some(i_decl_variants_val) => + var nab := SeqFind(i_decl_variants_val, (v: VariantInfo) => (v.name == "NonArrayBranch")); + match nab { + case Some(i_nab_val) => + var valField := SeqFind(i_nab_val.fields, (f: FieldInfo) => (f.name == "val")); + match valField { + case Some(i_valField_val) => + var valTy := i_valField_val.type_; + match valTy { + case Some(i_valTy_val) => + match i_valTy_val { + case string_(i__valTy_val_values) => + Some(IsArrayFact(i_tof_val, i_tof_val.ty.name, "NonArrayBranch")) + case _ => + None + } + case None => + None + } + case None => + None + } + case None => + None + } + case None => + None + } + case None => + None + } + case _ => + None + } + case None => + None + } +} + +function leadingIsArray(cond: TExpr, ctx: CondCtx): Option +{ + if ((!cond.binop?) || (cond.op != "&&")) then + None + else + var left := isArrayFact(cond.left, ctx); + match left { + case Some(i_left_val) => + Some(LeadingIsArray(i_left_val, cond.right)) + case None => + var right := isArrayFact(cond.right, ctx); + match right { + case Some(i_right_val) => + Some(LeadingIsArray(i_right_val, cond.left)) + case None => + if (cond.left.binop? && (cond.left.op == "&&")) then + var inner := leadingIsArray(cond.left, ctx); + match inner { + case Some(i_inner_val) => + Some(LeadingIsArray(i_inner_val.check, cond.(left := i_inner_val.restCond))) + case None => + if (cond.right.binop? && (cond.right.op == "&&")) then + var inner := leadingIsArray(cond.right, ctx); + match inner { + case Some(i_inner_val) => + Some(LeadingIsArray(i_inner_val.check, cond.(right := i_inner_val.restCond))) + case None => + None + } + else + None + } + else + if (cond.right.binop? && (cond.right.op == "&&")) then + var inner := leadingIsArray(cond.right, ctx); + match inner { + case Some(i_inner_val) => + Some(LeadingIsArray(i_inner_val.check, cond.(right := i_inner_val.restCond))) + case None => + None + } + else + None + } + } +} + +function negVariantFact(cond: TExpr, ctx: CondCtx): Option +{ + if ((((cond.binop? && (cond.op == "!==")) && cond.left.field?) && cond.left.obj.var_?) && cond.left.obj.ty.user?) then + var disc := cond.left.isDiscriminant; + match disc { + case Some(i_disc_val) => + if i_disc_val then + var lit := cond.right; + match lit { + case str(i_lit_value, i_lit_ty) => + Some(VariantFact(cond.left.obj, cond.left.obj.name, cond.left.obj.ty.name, i_lit_value)) + case _ => + None + } + else + None + case None => + None + } + else + if (cond.unop? && (cond.op == "!")) then + var arrCheck := isArrayFact(cond.expr, ctx); + match arrCheck { + case Some(i_arrCheck_val) => + if i_arrCheck_val.scrutinee.var_? then + Some(VariantFact(i_arrCheck_val.scrutinee, i_arrCheck_val.scrutinee.name, i_arrCheck_val.typeName, "NonArrayBranch")) + else + None + case None => + None + } + else + None +} + +function presentMatchStmts(f: PresentFact, some: seq, none: seq): TStmt +{ + TStmt.someMatch(f.scrutinee, f.binder, f.innerTy, (if canBeFalsy(f) then [if_(bound(f), some, none)] else some), none) +} + +function presentMatchExpr(f: PresentFact, some: TExpr, none: TExpr, ty: Ty): TExpr +{ + TExpr.someMatch(f.scrutinee, f.binder, f.innerTy, (if canBeFalsy(f) then conditional(bound(f), some, none, ty) else some), none, ty) +} + +function isFalsyCapableTy(ty: Ty): bool +{ + (Ty_kind(ty) in ["int", "nat", "string", "bool"]) +} + +function canBeFalsy(f: PresentFact): bool +{ + (f.truthiness && isFalsyCapableTy(f.innerTy)) +} + +function bound(f: PresentFact): TExpr +{ + var_(f.binder, f.innerTy) +} + +method binderHintFor(e: TExpr) returns (res: Option) +{ + var fields: seq := []; + var cur := e; + while cur.field? + decreases cur + { + fields := ([cur.field] + fields); + cur := cur.obj; + } + match cur { + case var_(i_cur_name, i_cur_ty) => + var root := (if (i_cur_name == "\\result") then "result" else i_cur_name); + return Some((if (|fields| == 0) then (("_" + root) + "_val") else (((("_" + root) + "_") + SeqJoin(fields, "_")) + "_val"))); + case _ => + return None; + } +} + +method presentFact(cond: TExpr) returns (res: Option) +{ + if ((cond.unop? && (cond.op == "!")) && cond.expr.ty.optional?) { + var e := cond.expr; + var innerTy := cond.expr.ty.inner; + var i_t0 := binderHintFor(e); + var hint := i_t0; + match hint { + case Some(i_hint_val) => + return Some(PresentFact(e, innerTy, true, freshName(i_hint_val), true)); + case None => + return None; + } + } + if ((!cond.binop?) || ((cond.op != "!==") && (cond.op != "==="))) { + if cond.ty.optional? { + var i_t1 := binderHintFor(cond); + var hint := i_t1; + match hint { + case Some(i_hint_val) => + return Some(PresentFact(cond, cond.ty.inner, false, freshName(i_hint_val), true)); + case None => + return None; + } + } + return None; + } + var e := None; + if (cond.right.var_? && (cond.right.name == "undefined")) { + e := Some(cond.left); + } + if (cond.left.var_? && (cond.left.name == "undefined")) { + e := Some(cond.right); + } + match e { + case Some(i_e_val) => + if (!i_e_val.ty.optional?) { + return None; + } + var i_t2 := binderHintFor(i_e_val); + var hint := i_t2; + match hint { + case Some(i_hint_val) => + return Some(PresentFact(i_e_val, i_e_val.ty.inner, (cond.op == "==="), freshName(i_hint_val), false)); + case None => + return None; + } + case None => + return None; + } +} + +method positivePresent(e: TExpr) returns (res: Option) +{ + var i_t3 := presentFact(e); + var f := i_t3; + match f { + case Some(i_f_val) => + if i_f_val.negated { + return None; + } + return Some(i_f_val); + case None => + return None; + } +} + +method leadingPresent(cond: TExpr) returns (res: Option) +{ + if ((!cond.binop?) || (cond.op != "&&")) { + return None; + } + var i_t4 := positivePresent(cond.left); + var left := i_t4; + match left { + case Some(i_left_val) => + return Some(LeadingPresent(i_left_val, cond.right)); + case None => + + } + var i_t5 := positivePresent(cond.right); + var right := i_t5; + match right { + case Some(i_right_val) => + return Some(LeadingPresent(i_right_val, cond.left)); + case None => + + } + if (cond.left.binop? && (cond.left.op == "&&")) { + var i_t6 := leadingPresent(cond.left); + var inner := i_t6; + match inner { + case Some(i_inner_val) => + return Some(LeadingPresent(i_inner_val.check, cond.(left := i_inner_val.restCond))); + case None => + + } + } + if (cond.right.binop? && (cond.right.op == "&&")) { + var i_t7 := leadingPresent(cond.right); + var inner := i_t7; + match inner { + case Some(i_inner_val) => + return Some(LeadingPresent(i_inner_val.check, cond.(right := i_inner_val.restCond))); + case None => + + } + } + return None; +} + +method noneDetector(leaf: TExpr, ctx: CondCtx) returns (res: Option) +{ + if (leaf.binop? && (leaf.op == "!==")) { + var ocOnLeft := leaf.left.optChain?; + var oc := (if ocOnLeft then Option.Some(leaf.left) else (if leaf.right.optChain? then Option.Some(leaf.right) else Option.None)); + match oc { + case Some(i_oc_val) => + if (i_oc_val.optChain? && i_oc_val.obj.ty.optional?) { + var i_t8 := binderHintFor(i_oc_val.obj); + var hint := i_t8; + match hint { + case Some(i_hint_val) => + var binder := freshName(i_hint_val); + var i_t10 := applyChain(var_(binder, i_oc_val.obj.ty.inner), i_oc_val.chain); + var i_t9 := restoreDiscriminantFlag(i_t10, ctx.decls); + var unwrapped := i_t9; + var lit := (if ocOnLeft then leaf.right else leaf.left); + return Some(NoneDetector(i_oc_val.obj, i_oc_val.obj.ty.inner, binder, Some(binop("!==", unwrapped, lit, Ty.bool_)))); + case None => + return None; + } + } + case None => + + } + } + var i_t11 := presentFact(leaf); + var f := i_t11; + match f { + case Some(i_f_val) => + if i_f_val.negated { + var i_t12 := canBeFalsy(i_f_val); + var residual := (if i_t12 then Option.Some(unop("!", var_(i_f_val.binder, i_f_val.innerTy), Ty.bool_)) else Option.None); + return Some(NoneDetector(i_f_val.scrutinee, i_f_val.innerTy, i_f_val.binder, residual)); + } + case None => + + } + return None; +} + +method applyChain(body: TExpr, chain: seq) returns (res: TExpr) +{ + var acc := body; + var i_step_idx := 0; + while i_step_idx < |chain| + invariant (i_step_idx <= |chain|) + { + var step := chain[i_step_idx]; + match step { + case field(i_step_name, i_step_ty) => + acc := TExpr.field(acc, i_step_name, i_step_ty, None, None); + case index(i_step_idx, i_step_ty) => + acc := TExpr.index(acc, i_step_idx, i_step_ty); + case call(i_step_args, i_step_ty, i_step_callKind, i_step_builtinId) => + match i_step_builtinId { + case Some(i_step_builtinId_val) => + acc := TExpr.call(acc, i_step_args, i_step_ty, i_step_callKind, Some(i_step_builtinId_val)); + case None => + acc := TExpr.call(acc, i_step_args, i_step_ty, i_step_callKind, None); + } + } + i_step_idx := i_step_idx + 1; + } + return acc; +} + +method binderHintForMapAccess(m: TExpr, k: TExpr, ctx: CondCtx) returns (res: MintedBinder) +{ + var i_t13 := binderHintFor(m); + var mHint := i_t13; + var i_t14 := binderHintFor(k); + var kHint := i_t14; + match mHint { + case Some(i_mHint_val) => + if (|i_mHint_val| > 0) { + match kHint { + case Some(i_kHint_val) => + if (|i_kHint_val| > 0) { + var i_t15 := stripValSuffix(i_mHint_val); + var mStem := i_t15; + var i_t17 := stripLeadingUnderscore(i_kHint_val); + var i_t16 := stripValSuffix(i_t17); + var kStem := i_t16; + return MintedBinder(freshName((((("" + mStem) + "_") + kStem) + "_val")), ctx); + } + case None => + + } + } + case None => + + } + var i_t18 := freshOcBinder(ctx); + return i_t18; +} + +method variantFact(cond: TExpr, ctx: CondCtx) returns (res: Option) +{ + if ((((cond.binop? && (cond.op == "===")) && cond.left.field?) && cond.left.obj.var_?) && cond.left.obj.ty.user?) { + var disc := cond.left.isDiscriminant; + match disc { + case Some(i_disc_val) => + if i_disc_val { + var lit := cond.right; + match lit { + case str(i_lit_value, i_lit_ty) => + return Some(VariantFact(cond.left.obj, cond.left.obj.name, cond.left.obj.ty.name, i_lit_value)); + case _ => + return None; + } + } else { + return None; + } + case None => + return None; + } + } + if (((cond.binop? && (cond.op == "in")) && cond.right.var_?) && cond.right.ty.user?) { + var keyLit := cond.left; + match keyLit { + case str(i_keyLit_value, i_keyLit_ty) => + var key := i_keyLit_value; + var typeName := cond.right.ty.name; + var decl := unionDeclOfTy(ctx.decls, cond.right.ty); + match decl { + case Some(i_decl_val) => + match i_decl_val.variants { + case Some(i_decl_variants_val) => + var matches := Std.Collections.Seq.Filter((v: VariantInfo) => (exists f :: f in v.fields && (f.name == key)), i_decl_variants_val); + if (|matches| == 1) { + return Some(VariantFact(cond.right, cond.right.name, typeName, matches[0].name)); + } + case None => + + } + case None => + + } + case _ => + return None; + } + } + var i_t19 := isArrayFact(cond, ctx); + var arrCheck := i_t19; + match arrCheck { + case Some(i_arrCheck_val) => + if i_arrCheck_val.scrutinee.var_? { + return Some(VariantFact(i_arrCheck_val.scrutinee, i_arrCheck_val.scrutinee.name, i_arrCheck_val.typeName, i_arrCheck_val.variant)); + } + case None => + + } + return None; +} diff --git a/tools/src/condition-facts.ts b/tools/src/condition-facts.ts index 3f9b881..28a14b7 100644 --- a/tools/src/condition-facts.ts +++ b/tools/src/condition-facts.ts @@ -19,20 +19,27 @@ * State is explicit (§6.1): detection that mints binders or consults type * declarations takes a `CondCtx`; nothing module-level. */ +//@ backend dafny -import type { TExpr, TStmt, Ty } from "./typedir.js"; +import type { TExpr, TStmt, Ty, TChainStep } from "./typedir.js"; import { freshName } from "./names.js"; import type { TypeDecls } from "./typedecls.js"; +import type { VariantInfo, FieldInfo } from "./types.js"; import { unionDeclOfTy, discriminantOf } from "./typedecls.js"; -/** Explicit context: type declarations plus the optChain binder counter. */ +/** Explicit context: type declarations plus the next optChain binder index. + * Threaded, never mutated (§6.1/§6.2): minting returns the advanced ctx, + * and the walkers pass it along. */ export interface CondCtx { decls: TypeDecls; - oc: { n: number }; + ocN: number; } -export function freshOcBinder(ctx: CondCtx): string { - return freshName(`_oc${ctx.oc.n++}_val`); +/** A minted binder plus the ctx that reserves it. */ +export interface MintedBinder { name: string; ctx: CondCtx } + +export function freshOcBinder(ctx: CondCtx): MintedBinder { + return { name: freshName(`_oc${ctx.ocN}_val`), ctx: { ...ctx, ocN: ctx.ocN + 1 } }; } // ── Presence facts ────────────────────────────────────────── @@ -57,7 +64,11 @@ export interface PresentFact { export function binderHintFor(e: TExpr): string | null { const fields: string[] = []; let cur = e; - while (cur.kind === "field") { fields.unshift(cur.field); cur = cur.obj; } + while (cur.kind === "field") { + //@ decreases cur + fields.unshift(cur.field); + cur = cur.obj; + } if (cur.kind !== "var") return null; // \result is stored as the IR var name "\\result"; sanitize for a valid identifier. const root = cur.name === "\\result" ? "result" : cur.name; @@ -93,7 +104,8 @@ export function presentFact(cond: TExpr): PresentFact | null { let e: TExpr | null = null; if (cond.right.kind === "var" && cond.right.name === "undefined") e = cond.left; if (cond.left.kind === "var" && cond.left.name === "undefined") e = cond.right; - if (!e || e.ty.kind !== "optional") return null; + if (e === null) return null; + if (e.ty.kind !== "optional") return null; const hint = binderHintFor(e); if (hint === null) return null; return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binder: freshName(hint), truthiness: false }; @@ -116,46 +128,51 @@ export const bound = (f: PresentFact): TExpr => // ── `&&`-chain analysis: one leading fact + residual ──────── -/** Pull one `parse`-matching conjunct out of an `&&` tree, returning it plus - * the tree with that conjunct removed: +/** The `leading*` extractors pull one matching conjunct out of an `&&` + * tree, returning it plus the tree with that conjunct removed: * `(x !== undefined && b) && c` → { check, restCond: b && c }. * * Search order is shallowest-first, left-biased — both immediate operands * before either nested chain — so the conjunct found is not necessarily the - * source-leftmost one. That is sound because every `parse` passed here - * (`leadingPresent`, `leadingIsArray`) matches only pure, total checks on an - * already-typed path, so hoisting one above the others is observationally - * neutral. The one thing it does change: the residual now sits inside the - * match arm, so a conjunct that would have run before a failing check no - * longer runs at all. - * Harmless while conditions stay pure — revisit if that ever stops holding. */ -export function extractConjunct(cond: TExpr, parse: (e: TExpr) => C | null): { check: C; restCond: TExpr } | null { + * source-leftmost one. That is sound because both extractors match only + * pure, total checks on an already-typed path, so hoisting one above the + * others is observationally neutral. The one thing it does change: the + * residual now sits inside the match arm, so a conjunct that would have run + * before a failing check no longer runs at all. + * Harmless while conditions stay pure — revisit if that ever stops holding. + * + * Two monomorphic copies of the tree surgery, deliberately: a shared + * generic `extractConjunct(cond, parse)` is parametricity with a + * fn-valued parameter, and neither has a model in the subset's erasure + * doctrine (§8.6). */ +export interface LeadingPresent { check: PresentFact; restCond: TExpr } + +function positivePresent(e: TExpr): PresentFact | null { + const f = presentFact(e); + if (f === null || f.negated) return null; + return f; +} + +/** The positive presence fact an `&&` chain contributes, plus the residual. + * "Leading" describes the rewrite, not the source: this fact becomes the + * outermost match, wherever in the chain it was written. */ +export function leadingPresent(cond: TExpr): LeadingPresent | null { if (cond.kind !== "binop" || cond.op !== "&&") return null; - const left = parse(cond.left); + const left = positivePresent(cond.left); if (left) return { check: left, restCond: cond.right }; - const right = parse(cond.right); + const right = positivePresent(cond.right); if (right) return { check: right, restCond: cond.left }; if (cond.left.kind === "binop" && cond.left.op === "&&") { - const inner = extractConjunct(cond.left, parse); - if (inner) return { check: inner.check, restCond: { ...cond, left: inner.restCond } as TExpr }; + const inner = leadingPresent(cond.left); + if (inner) return { check: inner.check, restCond: { ...cond, left: inner.restCond } }; } if (cond.right.kind === "binop" && cond.right.op === "&&") { - const inner = extractConjunct(cond.right, parse); - if (inner) return { check: inner.check, restCond: { ...cond, right: inner.restCond } as TExpr }; + const inner = leadingPresent(cond.right); + if (inner) return { check: inner.check, restCond: { ...cond, right: inner.restCond } }; } return null; } -/** The positive presence fact an `&&` chain contributes, plus the residual. - * "Leading" describes the rewrite, not the source: this fact becomes the - * outermost match, wherever in the chain it was written. */ -export function leadingPresent(cond: TExpr): { check: PresentFact; restCond: TExpr } | null { - return extractConjunct(cond, e => { - const f = presentFact(e); - return f && !f.negated ? f : null; - }); -} - // ── `||`-chain analysis (De Morgan): None-detectors ───────── /** Flatten a nested `||` chain into its leaf conditions. */ @@ -173,14 +190,15 @@ export type NoneDetector = { scrutinee: TExpr; innerTy: Ty; binder: string; resi export function noneDetector(leaf: TExpr, ctx: CondCtx): NoneDetector | null { // `x?.chain !== lit` — `undefined !== lit` is true when x is None. if (leaf.kind === "binop" && leaf.op === "!==") { - const oc = leaf.left.kind === "optChain" ? leaf.left : leaf.right.kind === "optChain" ? leaf.right : null; + const ocOnLeft = leaf.left.kind === "optChain"; + const oc = ocOnLeft ? leaf.left : leaf.right.kind === "optChain" ? leaf.right : null; if (oc && oc.kind === "optChain" && oc.obj.ty.kind === "optional") { const hint = binderHintFor(oc.obj); if (hint === null) return null; const binder = freshName(hint); - const unwrapped = applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain); - restoreDiscriminantFlag(unwrapped, ctx.decls); - const lit = leaf.left === oc ? leaf.right : leaf.left; + const unwrapped = restoreDiscriminantFlag( + applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain), ctx.decls); + const lit = ocOnLeft ? leaf.right : leaf.left; return { scrutinee: oc.obj, innerTy: oc.obj.ty.inner, binder, residual: { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } } }; } } @@ -197,28 +215,32 @@ export function noneDetector(leaf: TExpr, ctx: CondCtx): NoneDetector | null { // ── Optional chains ───────────────────────────────────────── -type OptChain = Extract; - /** Apply an optional chain's steps (field / index / call) to a base expr. */ -export function applyChain(body: TExpr, chain: OptChain["chain"]): TExpr { +export function applyChain(body: TExpr, chain: TChainStep[]): TExpr { + let acc = body; for (const step of chain) { - if (step.kind === "field") body = { kind: "field", obj: body, field: step.name, ty: step.ty }; - else if (step.kind === "index") body = { kind: "index", obj: body, idx: step.idx, ty: step.ty }; - else body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind, - ...(step.builtinId ? { builtinId: step.builtinId } : {}) }; + if (step.kind === "field") acc = { kind: "field", obj: acc, field: step.name, ty: step.ty }; + else if (step.kind === "index") acc = { kind: "index", obj: acc, idx: step.idx, ty: step.ty }; + else if (step.builtinId !== undefined) { + acc = { kind: "call", fn: acc, args: step.args, ty: step.ty, callKind: step.callKind, + builtinId: step.builtinId }; + } else { + acc = { kind: "call", fn: acc, args: step.args, ty: step.ty, callKind: step.callKind }; + } } - return body; + return acc; } /** `applyChain` rebuilds a field access without the `isDiscriminant` flag * resolve sets on a direct `x.disc`; restore it when the unwrapped access * is the binder union's discriminant, so the guard feeds discriminant * narrowing. (The single home of a fixup formerly duplicated per rule.) */ -export function restoreDiscriminantFlag(unwrapped: TExpr, decls: TypeDecls): void { +export function restoreDiscriminantFlag(unwrapped: TExpr, decls: TypeDecls): TExpr { if (unwrapped.kind === "field" && discriminantOf(decls, unwrapped.obj.ty) === unwrapped.field) { - unwrapped.isDiscriminant = true; + return { ...unwrapped, isDiscriminant: true }; } + return unwrapped; } // ── In-bounds facts ───────────────────────────────────────── @@ -246,25 +268,37 @@ export function exprEqual(a: TExpr, b: TExpr): boolean { return false; } +function stripValSuffix(s: string): string { + return s.endsWith("_val") ? s.slice(0, s.length - 4) : s; +} + +function stripLeadingUnderscore(s: string): string { + return s.startsWith("_") ? s.slice(1) : s; +} + /** Produce a reader-friendly binder hint for `m[k]` when both m and k are * access-path shaped (var / field chain). Falls back to a generic counter * name for computed keys. */ -export function binderHintForMapAccess(m: TExpr, k: TExpr, ctx: CondCtx): string { +export function binderHintForMapAccess(m: TExpr, k: TExpr, ctx: CondCtx): MintedBinder { const mHint = binderHintFor(m); const kHint = binderHintFor(k); if (mHint && kHint) { // mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`. - const mStem = mHint.replace(/_val$/, ""); - const kStem = kHint.replace(/^_/, "").replace(/_val$/, ""); - return freshName(`${mStem}_${kStem}_val`); + const mStem = stripValSuffix(mHint); + const kStem = stripValSuffix(stripLeadingUnderscore(kHint)); + return { name: freshName(`${mStem}_${kStem}_val`), ctx }; } return freshOcBinder(ctx); } // ── Variant facts (discriminants and synth array-unions) ──── +/** `scrutinee` is always a bare var (the rules only match that shape); + * `scrutineeName` carries its name so consumers need no shape refinement — + * intersection types like `TExpr & { kind: "var" }` have no backend model. */ export interface VariantFact { - scrutinee: TExpr & { kind: "var" }; + scrutinee: TExpr; + scrutineeName: string; typeName: string; variant: string; } @@ -285,8 +319,9 @@ export function isNarrowablePath(e: TExpr): boolean { } /** Detect `Array.isArray()` where `` is a narrowable path whose - * type is a synthesized array-union (discriminant `"__isArray__"`). */ -export function isArrayFact(call: TExpr, ctx: CondCtx): (IsArrayFact & { variant: "ArrayBranch" }) | null { + * type is a synthesized array-union (discriminant `"__isArray__"`). Always + * returns the `ArrayBranch` variant. */ +export function isArrayFact(call: TExpr, ctx: CondCtx): IsArrayFact | null { if (call.kind !== "call") return null; if (call.fn.kind !== "field" || call.fn.field !== "isArray") return null; if (call.fn.obj.kind !== "var" || call.fn.obj.name !== "Array") return null; @@ -294,7 +329,8 @@ export function isArrayFact(call: TExpr, ctx: CondCtx): (IsArrayFact & { variant const arg = call.args[0]; if (!isNarrowablePath(arg) || arg.ty.kind !== "user") return null; const decl = unionDeclOfTy(ctx.decls, arg.ty); - if (decl?.discriminant !== "__isArray__") return null; + if (decl === undefined) return null; + if (decl.discriminant !== "__isArray__") return null; return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" }; } @@ -303,47 +339,88 @@ export function isArrayFact(call: TExpr, ctx: CondCtx): (IsArrayFact & { variant * The runtime `=== "string"` test matches that branch only when `U` is string — * for any other non-array payload (`number | T[]`, …) it never holds, so we must * NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */ -export function typeofStringFact(e: TExpr, ctx: CondCtx): (IsArrayFact & { variant: "NonArrayBranch" }) | null { +export function typeofStringFact(e: TExpr, ctx: CondCtx): IsArrayFact | null { if (e.kind !== "binop" || e.op !== "===") return null; const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr : e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null; - const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null; - if (!tof || lit !== "string") return null; + if (tof === null) return null; + // Bind-first for the literal read, as in variantFact. + const strOnLeft = e.left.kind === "str"; + const litE = strOnLeft ? e.left : e.right; + if (litE.kind !== "str") return null; + if (litE.value !== "string") return null; if (!isNarrowablePath(tof) || tof.ty.kind !== "user") return null; const decl = unionDeclOfTy(ctx.decls, tof.ty); - if (decl?.discriminant !== "__isArray__") return null; - const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type; - if (valTy?.kind !== "string") return null; // guard: the non-array branch must actually be `string` + if (decl === undefined) return null; + if (decl.discriminant !== "__isArray__") return null; + // Guard: the non-array branch's payload must actually be `string`. + if (decl.variants === undefined) return null; + const nab = decl.variants.find((v: VariantInfo) => v.name === "NonArrayBranch"); + if (nab === undefined) return null; + const valField = nab.fields.find((f: FieldInfo) => f.name === "val"); + if (valField === undefined) return null; + const valTy = valField.type; + if (valTy === undefined) return null; + if (valTy.kind !== "string") return null; return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" }; } +export interface LeadingIsArray { check: IsArrayFact; restCond: TExpr } + /** Leading `Array.isArray(path)` fact of an `&&` chain (positive form only — a * negated `!Array.isArray(...)` would narrow to the wrong variant for then-body - * consumers, so those are left to the untouched-conditional path). */ -export function leadingIsArray(cond: TExpr, ctx: CondCtx) { - return extractConjunct(cond, e => isArrayFact(e, ctx)); + * consumers, so those are left to the untouched-conditional path). + * Monomorphic sibling of `leadingPresent` — see the note there. */ +export function leadingIsArray(cond: TExpr, ctx: CondCtx): LeadingIsArray | null { + if (cond.kind !== "binop" || cond.op !== "&&") return null; + const left = isArrayFact(cond.left, ctx); + if (left) return { check: left, restCond: cond.right }; + const right = isArrayFact(cond.right, ctx); + if (right) return { check: right, restCond: cond.left }; + if (cond.left.kind === "binop" && cond.left.op === "&&") { + const inner = leadingIsArray(cond.left, ctx); + if (inner) return { check: inner.check, restCond: { ...cond, left: inner.restCond } }; + } + if (cond.right.kind === "binop" && cond.right.op === "&&") { + const inner = leadingIsArray(cond.right, ctx); + if (inner) return { check: inner.check, restCond: { ...cond, right: inner.restCond } }; + } + return null; } /** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth * array-union) as a positive discriminant check on a bare-var scrutinee. */ export function variantFact(cond: TExpr, ctx: CondCtx): VariantFact | null { - // Pattern: x.discriminant === "variant" - if (cond.kind === "binop" && cond.op === "===" && cond.right.kind === "str" && - cond.left.kind === "field" && cond.left.isDiscriminant && + // Pattern: x.discriminant === "variant". Bind-first: the literal read + // (`lit.value`) sits inside the `lit.kind !== "str"` guard's narrowed + // scope, so the self-run's variant-aware destructor naming can pin it. + // The `isDiscriminant` read sits inside the shape-guarded branch: in the + // proof, `.isDiscriminant` is a partial destructor (legal only on `field` + // nodes), and a truthiness conjunct in the `&&`-chain gets hoisted above + // the shape guards by the leading-fact rewrite. + if (cond.kind === "binop" && cond.op === "===" && + cond.left.kind === "field" && cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") { - return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value }; + const disc = cond.left.isDiscriminant; + if (!disc) return null; + const lit = cond.right; + if (lit.kind !== "str") return null; + return { scrutinee: cond.left.obj, scrutineeName: cond.left.obj.name, + typeName: cond.left.obj.ty.name, variant: lit.value }; } // Pattern: 'key' in x — narrows x to the unique variant containing `key`. if (cond.kind === "binop" && cond.op === "in" && - cond.left.kind === "str" && cond.right.kind === "var" && - cond.right.ty.kind === "user") { - const key = cond.left.value; + cond.right.kind === "var" && cond.right.ty.kind === "user") { + const keyLit = cond.left; + if (keyLit.kind !== "str") return null; + const key = keyLit.value; const typeName = cond.right.ty.name; const decl = unionDeclOfTy(ctx.decls, cond.right.ty); - if (decl?.variants) { - const matches = decl.variants.filter(v => v.fields.some(f => f.name === key)); + if (decl !== undefined && decl.variants !== undefined) { + const matches = decl.variants.filter((v: VariantInfo) => v.fields.some(f => f.name === key)); if (matches.length === 1) { - return { scrutinee: cond.right, typeName, variant: matches[0].name }; + return { scrutinee: cond.right, scrutineeName: cond.right.name, + typeName, variant: matches[0].name }; } } } @@ -356,7 +433,8 @@ export function variantFact(cond: TExpr, ctx: CondCtx): VariantFact | null { // conditional/expression tagMatch drivers. const arrCheck = isArrayFact(cond, ctx); if (arrCheck && arrCheck.scrutinee.kind === "var") { - return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: arrCheck.variant }; + return { scrutinee: arrCheck.scrutinee, scrutineeName: arrCheck.scrutinee.name, + typeName: arrCheck.typeName, variant: arrCheck.variant }; } return null; } @@ -364,17 +442,24 @@ export function variantFact(cond: TExpr, ctx: CondCtx): VariantFact | null { /** Detect `x.kind !== "variant"` (negative discriminant check) or * `!Array.isArray(x)` (synth array-union, narrows to NonArrayBranch). */ export function negVariantFact(cond: TExpr, ctx: CondCtx): VariantFact | null { - if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" && - cond.left.kind === "field" && cond.left.isDiscriminant && + // Same bind-first + guarded-flag-read shape as variantFact — see the note there. + if (cond.kind === "binop" && cond.op === "!==" && + cond.left.kind === "field" && cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") { - return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value }; + const disc = cond.left.isDiscriminant; + if (!disc) return null; + const lit = cond.right; + if (lit.kind !== "str") return null; + return { scrutinee: cond.left.obj, scrutineeName: cond.left.obj.name, + typeName: cond.left.obj.ty.name, variant: lit.value }; } // Pattern: !Array.isArray(x) — narrows x to the NonArrayBranch variant. // Same var-scrutinee restriction as variantFact. if (cond.kind === "unop" && cond.op === "!") { const arrCheck = isArrayFact(cond.expr, ctx); if (arrCheck && arrCheck.scrutinee.kind === "var") { - return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: "NonArrayBranch" }; + return { scrutinee: arrCheck.scrutinee, scrutineeName: arrCheck.scrutinee.name, + typeName: arrCheck.typeName, variant: "NonArrayBranch" }; } } return null; diff --git a/tools/src/dafny-emit.ts b/tools/src/dafny-emit.ts index 5ec8d98..e01d57e 100644 --- a/tools/src/dafny-emit.ts +++ b/tools/src/dafny-emit.ts @@ -284,15 +284,15 @@ function emitExpr(e: Expr): string { if (e.method === "map") { // Always a seq comprehension: Seq.Map would hide the element access // behind a closure, defeating Dafny's termination checker for - // recursive rebuild walkers. A literal lambda argument is + // recursive rebuild walkers (§8.6 E2). A literal lambda argument is // beta-reduced through a `var` binding — an applied closure defeats // the checker too; any other argument is applied to the element // directly. A non-variable receiver is bound once up front: the // comprehension mentions it three times, and splicing would make // any closure literal inside it (e.g. a Filter predicate) three // distinct closures, unprovably equal through an opaque callee. - // Name freshness is a local IR-level check until a backend name - // allocator exists. + // Name freshness is a local IR-level check until the §6.3 backend + // name allocator exists. const lam = e.args[0]; const fresh = (base: string, taken: (n: string) => boolean): string => { let name = base; diff --git a/tools/src/ir.dfy b/tools/src/ir.dfy new file mode 100644 index 0000000..646b75e --- /dev/null +++ b/tools/src/ir.dfy @@ -0,0 +1,155 @@ +// Generated by lsc from ir.ts + +datatype Option = None | Some(value: T) + +function {:axiom} exactIntegerLiteral(e: Expr): Option + +datatype Expr = var_(name: string) | num(value_num: int) | bigint(value_bigint: string) | bool_(value_bool: bool) | str(value_str: string) | constructor_(name: string, type_constructor: Option, args: seq) | binop(op: string, left: Expr, right: Expr) | unop(op: string, expr: Expr) | app(fn: string, args: seq, ctorOf: Option) | field(obj: Expr, field: string, fromUnion: Option, ctor: Option, datatypeField: Option) | toNat(expr: Expr) | toReal(expr: Expr) | index(arr: Expr, idx: Expr) | tupleLiteral(elems: seq) | tupleProj(obj: Expr, index: int, arity: int) | record(spread: Option, fields: seq, ctor: Option, ctorOf: Option) | arrayLiteral(elems: seq) | emptyMap | emptySet | mapLiteral(entries: seq) | methodCall(obj: Expr, objTy: Ty, method_: string, args: seq, monadic: bool) | lambda(params: seq, body_lambda: seq) | if_(cond: Expr, then_: Expr, else_: Expr) | match_(scrutinee: Expr, arms: seq) | forall_(var_: string, type_forall: Ty, body_forall: Expr) | exists_(var_: string, type_exists: Ty, body_exists: Expr) | implies(premises: seq, conclusion: Expr) | let(name: string, value_let: Expr, body_let: Expr) | havoc(type_havoc: Ty) | default(type_default: Ty) + +datatype MatchPattern = wild | ctor(ctor: string, binders: seq) + +datatype Param = Param(name: string, type_: Ty) + +datatype RecordField = RecordField(name: string, value: Expr) + +datatype MapEntry = MapEntry(key: Expr, value: Expr) + +datatype CtorInfo = CtorInfo(name: string, fields: seq) + +datatype EmitOption = EmitOption(key: string, value: string) + +datatype MatchArm = MatchArm(pattern: MatchPattern, body: Expr) + +datatype Stmt = let(name: string, type_: Ty, mutable: bool, value: Expr) | assign(target: string, value: Expr) | bind(target: string, value: Expr) | let_bind(name: string, value: Expr) | return_(value: Expr) | break_ | continue_ | if_(cond: Expr, then_: seq, else_: seq) | match_(scrutinee: Expr, arms: seq) | while_(cond: Expr, invariants: seq, decreasing: Option, doneWith: Option, body: seq) | forin(idx: string, bound: Expr, invariants: seq, body: seq) | ghostLet(name: string, type_: Ty, value: Expr) | ghostAssign(target: string, value: Expr) | assert_(expr: Expr, assumed: Option) + +datatype StmtMatchArm = StmtMatchArm(pattern: MatchPattern, body: seq) + +datatype Inductive = Inductive(kind: string, name: string, typeParams: Option>, constructors: seq, deriving: seq) + +datatype Structure = Structure(kind: string, name: string, typeParams: Option>, fields: seq, deriving: seq) + +datatype FnDef = FnDef(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: Expr) + +datatype FnDefByMethod = FnDefByMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) + +datatype FnMethod = FnMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: seq) + +datatype Namespace = Namespace(kind: string, name: string, decls: seq) + +datatype ClassDecl = ClassDecl(kind: string, name: string, fields: seq, methods: seq) + +datatype ConstDecl = ConstDecl(kind: string, name: string, type_: Ty, value: Expr) + +datatype TypeAlias = TypeAlias(kind: string, name: string, target: Ty) + +datatype OpaqueType = OpaqueType(kind: string, name: string) + +datatype ExternDecl = ExternDecl(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype Decl = inductive_(name: string, typeParams_inductive: Option>, constructors: seq, deriving: seq) | structure(name: string, typeParams_structure: Option>, fields: seq, deriving: seq) | def(name: string, typeParams_def: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_def: Expr) | def_by_method(name: string, typeParams_def_by_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) | method_(name: string, typeParams_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_method: seq) | namespace(name: string, decls: seq) | class_(name: string, fields: seq, methods: seq) | const_(name: string, type_: Ty, value: Expr) | type_alias(name: string, target: Ty) | opaque_type(name: string) | extern(name: string, typeParams_extern: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype Module = Module(comment: string, imports: seq, options: seq, decls: seq) + +type ExprPred = (Expr) -> bool + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +function patternBinders(p: MatchPattern): seq +{ + if p.ctor? then + p.binders + else + [] +} + +function patternCtor(p: MatchPattern): Option +{ + if p.ctor? then + Option.Some(p.ctor) + else + Option.None +} + +function patternBinds(p: MatchPattern, name: string): bool +{ + (name in patternBinders(p)) +} + +function anyExpr(e: Expr, pred: ExprPred): bool +{ + (pred(e) || (match e { case var_(i_e_name) => false case num(i_e_value) => false case bigint(i_e_value) => false case bool_(i_e_value) => false case str(i_e_value) => false case emptyMap => false case emptySet => false case havoc(i_e_type) => false case default(i_e_type) => false case constructor_(i_e_name, i_e_type, i_e_args) => (exists a :: a in i_e_args && anyExpr(a, pred)) case binop(i_e_op, i_e_left, i_e_right) => (anyExpr(i_e_left, pred) || anyExpr(i_e_right, pred)) case unop(i_e_op, i_e_expr) => anyExpr(i_e_expr, pred) case toNat(i_e_expr) => anyExpr(i_e_expr, pred) case toReal(i_e_expr) => anyExpr(i_e_expr, pred) case app(i_e_fn, i_e_args, i_e_ctorOf) => (exists a :: a in i_e_args && anyExpr(a, pred)) case field(i_e_obj, i_e_field, i_e_fromUnion, i_e_ctor, i_e_datatypeField) => anyExpr(i_e_obj, pred) case index(i_e_arr, i_e_idx) => (anyExpr(i_e_arr, pred) || anyExpr(i_e_idx, pred)) case tupleLiteral(i_e_elems) => (exists x :: x in i_e_elems && anyExpr(x, pred)) case tupleProj(i_e_obj, i_e_index, i_e_arity) => anyExpr(i_e_obj, pred) case implies(i_e_premises, i_e_conclusion) => ((exists p :: p in i_e_premises && anyExpr(p, pred)) || anyExpr(i_e_conclusion, pred)) case record(i_e_spread, i_e_fields, i_e_ctor, i_e_ctorOf) => ((match i_e_spread { case Some(i_e_spread_val) => anyExpr(i_e_spread_val, pred) case None => false }) || (exists f :: f in i_e_fields && anyExpr(f.value, pred))) case arrayLiteral(i_e_elems) => (exists x :: x in i_e_elems && anyExpr(x, pred)) case mapLiteral(i_e_entries) => (exists en :: en in i_e_entries && (anyExpr(en.key, pred) || anyExpr(en.value, pred))) case methodCall(i_e_obj, i_e_objTy, i_e_method, i_e_args, i_e_monadic) => (anyExpr(i_e_obj, pred) || (exists a :: a in i_e_args && anyExpr(a, pred))) case lambda(i_e_params, i_e_body) => (exists s :: s in i_e_body && anyExprInStmt(s, pred)) case if_(i_e_cond, i_e_then, i_e_else) => ((anyExpr(i_e_cond, pred) || anyExpr(i_e_then, pred)) || anyExpr(i_e_else, pred)) case match_(i_e_scrutinee, i_e_arms) => (anyExpr(i_e_scrutinee, pred) || (exists a :: a in i_e_arms && anyExpr(a.body, pred))) case forall_(i_e_var, i_e_type, i_e_body) => anyExpr(i_e_body, pred) case exists_(i_e_var, i_e_type, i_e_body) => anyExpr(i_e_body, pred) case let(i_e_name, i_e_value, i_e_body) => (anyExpr(i_e_value, pred) || anyExpr(i_e_body, pred)) })) +} + +function anyExprInStmt(s: Stmt, pred: ExprPred): bool +{ + match s { + case let(i_s_name, i_s_type, i_s_mutable, i_s_value) => + anyExpr(i_s_value, pred) + case assign(i_s_target, i_s_value) => + anyExpr(i_s_value, pred) + case bind(i_s_target, i_s_value) => + anyExpr(i_s_value, pred) + case let_bind(i_s_name, i_s_value) => + anyExpr(i_s_value, pred) + case return_(i_s_value) => + anyExpr(i_s_value, pred) + case ghostLet(i_s_name, i_s_type, i_s_value) => + anyExpr(i_s_value, pred) + case ghostAssign(i_s_target, i_s_value) => + anyExpr(i_s_value, pred) + case assert_(i_s_expr, i_s_assumed) => + anyExpr(i_s_expr, pred) + case break_ => + false + case continue_ => + false + case if_(i_s_cond, i_s_then, i_s_else) => + ((anyExpr(i_s_cond, pred) || anyExprInStmts(i_s_then, pred)) || anyExprInStmts(i_s_else, pred)) + case match_(i_s_scrutinee, i_s_arms) => + (anyExpr(i_s_scrutinee, pred) || (exists a :: a in i_s_arms && anyExprInStmts(a.body, pred))) + case while_(i_s_cond, i_s_invariants, i_s_decreasing, i_s_doneWith, i_s_body) => + ((((anyExpr(i_s_cond, pred) || (exists i :: i in i_s_invariants && anyExpr(i, pred))) || (match i_s_decreasing { case Some(i_s_decreasing_val) => anyExpr(i_s_decreasing_val, pred) case None => false })) || (match i_s_doneWith { case Some(i_s_doneWith_val) => anyExpr(i_s_doneWith_val, pred) case None => false })) || anyExprInStmts(i_s_body, pred)) + case forin(i_s_idx, i_s_bound, i_s_invariants, i_s_body) => + ((anyExpr(i_s_bound, pred) || (exists i :: i in i_s_invariants && anyExpr(i, pred))) || anyExprInStmts(i_s_body, pred)) + } +} + +function anyExprInStmts(stmts: seq, pred: ExprPred): bool +{ + (exists s :: s in stmts && anyExprInStmt(s, pred)) +} + +function usesName(e: Expr, name: string): bool +{ + anyExpr(e, i_refsName(name)) +} + +function usesNameInStmts(stmts: seq, name: string): bool +{ + anyExprInStmts(stmts, i_refsName(name)) +} + +function bindsNameInStmts(stmts: seq, name: string): bool +{ + (exists s :: s in stmts && (match s { case let(i_s_name, i_s_type, i_s_mutable, i_s_value) => (i_s_name == name) case let_bind(i_s_name, i_s_value) => (i_s_name == name) case ghostLet(i_s_name, i_s_type, i_s_value) => (i_s_name == name) case assign(i_s_target, i_s_value) => (i_s_target == name) case bind(i_s_target, i_s_value) => (i_s_target == name) case ghostAssign(i_s_target, i_s_value) => (i_s_target == name) case forin(i_s_idx, i_s_bound, i_s_invariants, i_s_body) => ((i_s_idx == name) || bindsNameInStmts(i_s_body, name)) case if_(i_s_cond, i_s_then, i_s_else) => (bindsNameInStmts(i_s_then, name) || bindsNameInStmts(i_s_else, name)) case while_(i_s_cond, i_s_invariants, i_s_decreasing, i_s_doneWith, i_s_body) => bindsNameInStmts(i_s_body, name) case match_(i_s_scrutinee, i_s_arms) => (exists a :: a in i_s_arms && (patternBinds(a.pattern, name) || bindsNameInStmts(a.body, name))) case _ => false })) +} + +function usesNameInDecl(requires_: seq, ensures_: seq, body: seq, name: string): bool +{ + ((((exists e :: e in requires_ && usesName(e, name)) || (exists e :: e in ensures_ && usesName(e, name))) || usesNameInStmts(body, name)) || bindsNameInStmts(body, name)) +} + +function pWild(): MatchPattern +{ + MatchPattern.wild +} + +function pCtor(c: string, binders: seq): MatchPattern +{ + ctor(c, binders) +} + +function i_refsName(name: string): ExprPred +{ + (e: Expr) => (((e.var_? && (e.name == name)) || (e.app? && (e.fn == name))) || (e.constructor_? && (e.name == name))) +} diff --git a/tools/src/ir.dfy.gen b/tools/src/ir.dfy.gen new file mode 100644 index 0000000..646b75e --- /dev/null +++ b/tools/src/ir.dfy.gen @@ -0,0 +1,155 @@ +// Generated by lsc from ir.ts + +datatype Option = None | Some(value: T) + +function {:axiom} exactIntegerLiteral(e: Expr): Option + +datatype Expr = var_(name: string) | num(value_num: int) | bigint(value_bigint: string) | bool_(value_bool: bool) | str(value_str: string) | constructor_(name: string, type_constructor: Option, args: seq) | binop(op: string, left: Expr, right: Expr) | unop(op: string, expr: Expr) | app(fn: string, args: seq, ctorOf: Option) | field(obj: Expr, field: string, fromUnion: Option, ctor: Option, datatypeField: Option) | toNat(expr: Expr) | toReal(expr: Expr) | index(arr: Expr, idx: Expr) | tupleLiteral(elems: seq) | tupleProj(obj: Expr, index: int, arity: int) | record(spread: Option, fields: seq, ctor: Option, ctorOf: Option) | arrayLiteral(elems: seq) | emptyMap | emptySet | mapLiteral(entries: seq) | methodCall(obj: Expr, objTy: Ty, method_: string, args: seq, monadic: bool) | lambda(params: seq, body_lambda: seq) | if_(cond: Expr, then_: Expr, else_: Expr) | match_(scrutinee: Expr, arms: seq) | forall_(var_: string, type_forall: Ty, body_forall: Expr) | exists_(var_: string, type_exists: Ty, body_exists: Expr) | implies(premises: seq, conclusion: Expr) | let(name: string, value_let: Expr, body_let: Expr) | havoc(type_havoc: Ty) | default(type_default: Ty) + +datatype MatchPattern = wild | ctor(ctor: string, binders: seq) + +datatype Param = Param(name: string, type_: Ty) + +datatype RecordField = RecordField(name: string, value: Expr) + +datatype MapEntry = MapEntry(key: Expr, value: Expr) + +datatype CtorInfo = CtorInfo(name: string, fields: seq) + +datatype EmitOption = EmitOption(key: string, value: string) + +datatype MatchArm = MatchArm(pattern: MatchPattern, body: Expr) + +datatype Stmt = let(name: string, type_: Ty, mutable: bool, value: Expr) | assign(target: string, value: Expr) | bind(target: string, value: Expr) | let_bind(name: string, value: Expr) | return_(value: Expr) | break_ | continue_ | if_(cond: Expr, then_: seq, else_: seq) | match_(scrutinee: Expr, arms: seq) | while_(cond: Expr, invariants: seq, decreasing: Option, doneWith: Option, body: seq) | forin(idx: string, bound: Expr, invariants: seq, body: seq) | ghostLet(name: string, type_: Ty, value: Expr) | ghostAssign(target: string, value: Expr) | assert_(expr: Expr, assumed: Option) + +datatype StmtMatchArm = StmtMatchArm(pattern: MatchPattern, body: seq) + +datatype Inductive = Inductive(kind: string, name: string, typeParams: Option>, constructors: seq, deriving: seq) + +datatype Structure = Structure(kind: string, name: string, typeParams: Option>, fields: seq, deriving: seq) + +datatype FnDef = FnDef(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: Expr) + +datatype FnDefByMethod = FnDefByMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) + +datatype FnMethod = FnMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: seq) + +datatype Namespace = Namespace(kind: string, name: string, decls: seq) + +datatype ClassDecl = ClassDecl(kind: string, name: string, fields: seq, methods: seq) + +datatype ConstDecl = ConstDecl(kind: string, name: string, type_: Ty, value: Expr) + +datatype TypeAlias = TypeAlias(kind: string, name: string, target: Ty) + +datatype OpaqueType = OpaqueType(kind: string, name: string) + +datatype ExternDecl = ExternDecl(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype Decl = inductive_(name: string, typeParams_inductive: Option>, constructors: seq, deriving: seq) | structure(name: string, typeParams_structure: Option>, fields: seq, deriving: seq) | def(name: string, typeParams_def: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_def: Expr) | def_by_method(name: string, typeParams_def_by_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) | method_(name: string, typeParams_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_method: seq) | namespace(name: string, decls: seq) | class_(name: string, fields: seq, methods: seq) | const_(name: string, type_: Ty, value: Expr) | type_alias(name: string, target: Ty) | opaque_type(name: string) | extern(name: string, typeParams_extern: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype Module = Module(comment: string, imports: seq, options: seq, decls: seq) + +type ExprPred = (Expr) -> bool + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +function patternBinders(p: MatchPattern): seq +{ + if p.ctor? then + p.binders + else + [] +} + +function patternCtor(p: MatchPattern): Option +{ + if p.ctor? then + Option.Some(p.ctor) + else + Option.None +} + +function patternBinds(p: MatchPattern, name: string): bool +{ + (name in patternBinders(p)) +} + +function anyExpr(e: Expr, pred: ExprPred): bool +{ + (pred(e) || (match e { case var_(i_e_name) => false case num(i_e_value) => false case bigint(i_e_value) => false case bool_(i_e_value) => false case str(i_e_value) => false case emptyMap => false case emptySet => false case havoc(i_e_type) => false case default(i_e_type) => false case constructor_(i_e_name, i_e_type, i_e_args) => (exists a :: a in i_e_args && anyExpr(a, pred)) case binop(i_e_op, i_e_left, i_e_right) => (anyExpr(i_e_left, pred) || anyExpr(i_e_right, pred)) case unop(i_e_op, i_e_expr) => anyExpr(i_e_expr, pred) case toNat(i_e_expr) => anyExpr(i_e_expr, pred) case toReal(i_e_expr) => anyExpr(i_e_expr, pred) case app(i_e_fn, i_e_args, i_e_ctorOf) => (exists a :: a in i_e_args && anyExpr(a, pred)) case field(i_e_obj, i_e_field, i_e_fromUnion, i_e_ctor, i_e_datatypeField) => anyExpr(i_e_obj, pred) case index(i_e_arr, i_e_idx) => (anyExpr(i_e_arr, pred) || anyExpr(i_e_idx, pred)) case tupleLiteral(i_e_elems) => (exists x :: x in i_e_elems && anyExpr(x, pred)) case tupleProj(i_e_obj, i_e_index, i_e_arity) => anyExpr(i_e_obj, pred) case implies(i_e_premises, i_e_conclusion) => ((exists p :: p in i_e_premises && anyExpr(p, pred)) || anyExpr(i_e_conclusion, pred)) case record(i_e_spread, i_e_fields, i_e_ctor, i_e_ctorOf) => ((match i_e_spread { case Some(i_e_spread_val) => anyExpr(i_e_spread_val, pred) case None => false }) || (exists f :: f in i_e_fields && anyExpr(f.value, pred))) case arrayLiteral(i_e_elems) => (exists x :: x in i_e_elems && anyExpr(x, pred)) case mapLiteral(i_e_entries) => (exists en :: en in i_e_entries && (anyExpr(en.key, pred) || anyExpr(en.value, pred))) case methodCall(i_e_obj, i_e_objTy, i_e_method, i_e_args, i_e_monadic) => (anyExpr(i_e_obj, pred) || (exists a :: a in i_e_args && anyExpr(a, pred))) case lambda(i_e_params, i_e_body) => (exists s :: s in i_e_body && anyExprInStmt(s, pred)) case if_(i_e_cond, i_e_then, i_e_else) => ((anyExpr(i_e_cond, pred) || anyExpr(i_e_then, pred)) || anyExpr(i_e_else, pred)) case match_(i_e_scrutinee, i_e_arms) => (anyExpr(i_e_scrutinee, pred) || (exists a :: a in i_e_arms && anyExpr(a.body, pred))) case forall_(i_e_var, i_e_type, i_e_body) => anyExpr(i_e_body, pred) case exists_(i_e_var, i_e_type, i_e_body) => anyExpr(i_e_body, pred) case let(i_e_name, i_e_value, i_e_body) => (anyExpr(i_e_value, pred) || anyExpr(i_e_body, pred)) })) +} + +function anyExprInStmt(s: Stmt, pred: ExprPred): bool +{ + match s { + case let(i_s_name, i_s_type, i_s_mutable, i_s_value) => + anyExpr(i_s_value, pred) + case assign(i_s_target, i_s_value) => + anyExpr(i_s_value, pred) + case bind(i_s_target, i_s_value) => + anyExpr(i_s_value, pred) + case let_bind(i_s_name, i_s_value) => + anyExpr(i_s_value, pred) + case return_(i_s_value) => + anyExpr(i_s_value, pred) + case ghostLet(i_s_name, i_s_type, i_s_value) => + anyExpr(i_s_value, pred) + case ghostAssign(i_s_target, i_s_value) => + anyExpr(i_s_value, pred) + case assert_(i_s_expr, i_s_assumed) => + anyExpr(i_s_expr, pred) + case break_ => + false + case continue_ => + false + case if_(i_s_cond, i_s_then, i_s_else) => + ((anyExpr(i_s_cond, pred) || anyExprInStmts(i_s_then, pred)) || anyExprInStmts(i_s_else, pred)) + case match_(i_s_scrutinee, i_s_arms) => + (anyExpr(i_s_scrutinee, pred) || (exists a :: a in i_s_arms && anyExprInStmts(a.body, pred))) + case while_(i_s_cond, i_s_invariants, i_s_decreasing, i_s_doneWith, i_s_body) => + ((((anyExpr(i_s_cond, pred) || (exists i :: i in i_s_invariants && anyExpr(i, pred))) || (match i_s_decreasing { case Some(i_s_decreasing_val) => anyExpr(i_s_decreasing_val, pred) case None => false })) || (match i_s_doneWith { case Some(i_s_doneWith_val) => anyExpr(i_s_doneWith_val, pred) case None => false })) || anyExprInStmts(i_s_body, pred)) + case forin(i_s_idx, i_s_bound, i_s_invariants, i_s_body) => + ((anyExpr(i_s_bound, pred) || (exists i :: i in i_s_invariants && anyExpr(i, pred))) || anyExprInStmts(i_s_body, pred)) + } +} + +function anyExprInStmts(stmts: seq, pred: ExprPred): bool +{ + (exists s :: s in stmts && anyExprInStmt(s, pred)) +} + +function usesName(e: Expr, name: string): bool +{ + anyExpr(e, i_refsName(name)) +} + +function usesNameInStmts(stmts: seq, name: string): bool +{ + anyExprInStmts(stmts, i_refsName(name)) +} + +function bindsNameInStmts(stmts: seq, name: string): bool +{ + (exists s :: s in stmts && (match s { case let(i_s_name, i_s_type, i_s_mutable, i_s_value) => (i_s_name == name) case let_bind(i_s_name, i_s_value) => (i_s_name == name) case ghostLet(i_s_name, i_s_type, i_s_value) => (i_s_name == name) case assign(i_s_target, i_s_value) => (i_s_target == name) case bind(i_s_target, i_s_value) => (i_s_target == name) case ghostAssign(i_s_target, i_s_value) => (i_s_target == name) case forin(i_s_idx, i_s_bound, i_s_invariants, i_s_body) => ((i_s_idx == name) || bindsNameInStmts(i_s_body, name)) case if_(i_s_cond, i_s_then, i_s_else) => (bindsNameInStmts(i_s_then, name) || bindsNameInStmts(i_s_else, name)) case while_(i_s_cond, i_s_invariants, i_s_decreasing, i_s_doneWith, i_s_body) => bindsNameInStmts(i_s_body, name) case match_(i_s_scrutinee, i_s_arms) => (exists a :: a in i_s_arms && (patternBinds(a.pattern, name) || bindsNameInStmts(a.body, name))) case _ => false })) +} + +function usesNameInDecl(requires_: seq, ensures_: seq, body: seq, name: string): bool +{ + ((((exists e :: e in requires_ && usesName(e, name)) || (exists e :: e in ensures_ && usesName(e, name))) || usesNameInStmts(body, name)) || bindsNameInStmts(body, name)) +} + +function pWild(): MatchPattern +{ + MatchPattern.wild +} + +function pCtor(c: string, binders: seq): MatchPattern +{ + ctor(c, binders) +} + +function i_refsName(name: string): ExprPred +{ + (e: Expr) => (((e.var_? && (e.name == name)) || (e.app? && (e.fn == name))) || (e.constructor_? && (e.name == name))) +} diff --git a/tools/src/ir.ts b/tools/src/ir.ts index 5dceb4e..92ffdd6 100644 --- a/tools/src/ir.ts +++ b/tools/src/ir.ts @@ -5,6 +5,11 @@ * The emit phase pretty-prints them to backend syntax (Lean or Dafny). */ +// Self-application (DESIGN_LS_IN_LS.md §8): this module is inside the +// LemmaScript subset and is compiled by lsc itself (LemmaScript-files.txt). +// Dafny-only until the Lean emitter learns mutual blocks (§9 step 1). +//@ backend dafny + import type { Ty } from "./typedir.js"; // ── Expressions ────────────────────────────────────────────── @@ -51,9 +56,10 @@ export type MatchPattern = export const pWild = (): MatchPattern => ({ kind: "wild" }); export const pCtor = (c: string, ...binders: string[]): MatchPattern => ({ kind: "ctor", ctor: c, binders }); -// Named payload records for the shapes repeated across IR nodes. Purely -// structural — identical to the object-literal types they replace, so -// construction sites are unaffected. +// Named payload records. Naming these (rather than inlining object-literal +// types) keeps the module inside the LemmaScript subset: inline anonymous +// record types have no backend model. Purely structural — construction +// sites are unaffected. export interface Param { name: string; type: Ty } export interface RecordField { name: string; value: Expr } export interface MapEntry { key: Expr; value: Expr } @@ -222,7 +228,11 @@ export interface Module { * literal into arithmetic stays exact: JS bitwise operators truncate to 32 bits * and `Math.pow(2, n)` is a double, both of which lie past 2^53. A `num` * outside the safe-integer range has already lost precision, so it is not a - * usable answer — hence null. */ + * usable answer — hence null. + * + * Outside LS's verification model (`Number.isSafeInteger`), so it is an + * extern: signature axiomatized, body skipped. */ +//@ extern export function exactIntegerLiteral(e: Expr): bigint | null { if (e.kind === "bigint") return BigInt(e.value); if (e.kind === "num") return Number.isSafeInteger(e.value) ? BigInt(e.value) : null; diff --git a/tools/src/names.ts b/tools/src/names.ts index 063d44e..3c1d30e 100644 --- a/tools/src/names.ts +++ b/tools/src/names.ts @@ -46,7 +46,9 @@ export function userNames(): readonly string[] { /** A toolchain-internal name: `base` verbatim, primed on collision against * user-written names anywhere in the module. The common form — deterministic - * per module. */ + * per module, so the self-run can axiomatize it as a pure function (a default + * parameter would synthesize a two-argument axiom no portable caller can + * satisfy). */ export function freshName(base: string): string { return freshNameWhere(base, isUserName); } diff --git a/tools/src/narrow.dfy b/tools/src/narrow.dfy new file mode 100644 index 0000000..b48c72a --- /dev/null +++ b/tools/src/narrow.dfy @@ -0,0 +1,3969 @@ +// Generated by lsc from narrow.ts + +datatype Option = None | Some(value: T) + +function {:axiom} isTerminatorKind(kind: string): bool + ensures isTerminatorKind(kind) ==> + kind == "return" || kind == "break" || kind == "continue" || kind == "throw" + +function {:axiom} presentFact(cond: TExpr): Option + ensures presentFact(cond).Some? ==> + ((cond.unop? && cond.op == "!") || cond.var_? || cond.field? || + (cond.binop? && (cond.op == "===" || cond.op == "!=="))) + ensures presentFact(cond).Some? && (cond.var_? || cond.field?) ==> cond.ty.optional? + ensures presentFact(cond).Some? && presentFact(cond).value.truthiness && FalsyCapable(presentFact(cond).value.innerTy) ==> + !presentFact(cond).value.innerTy.optional? + ensures presentFact(cond).Some? ==> Inert(cond) + ensures presentFact(cond).Some? ==> !containsMethodCall(cond) + +function {:axiom} freshName(base: string): string + +function {:axiom} presentMatchStmts(f: PresentFact, some: seq, none: seq): TStmt + ensures presentMatchStmts(f, some, none) == + TStmt.someMatch(f.scrutinee, f.binder, f.innerTy, + if f.truthiness && FalsyCapable(f.innerTy) + then [TStmt.if_(TExpr.var_(f.binder, f.innerTy), some, none)] + else some, + none) + +function {:axiom} flattenOr(e: TExpr): seq + ensures !(e.binop? && e.op == "||") ==> flattenOr(e) == [e] + ensures e.binop? && e.op == "||" ==> flattenOr(e) == flattenOr(e.left) + flattenOr(e.right) + +function {:axiom} noneDetector(leaf: TExpr, ctx: CondCtx): Option + ensures noneDetector(leaf, ctx).Some? ==> + binderHintFor(noneDetector(leaf, ctx).value.scrutinee).Some? + ensures noneDetector(leaf, ctx).Some? ==> + (presentFact(leaf).Some? && presentFact(leaf).value.negated) || + (leaf.binop? && leaf.op == "!==" && + ((leaf.left.optChain? && leaf.left.obj.ty.optional?) || + (leaf.right.optChain? && leaf.right.obj.ty.optional?))) + +function {:axiom} binderHintFor(e: TExpr): Option + ensures binderHintFor(e).Some? ==> Inert(e) + +function {:axiom} restoreDiscriminantFlag(unwrapped: TExpr, decls: TypeDecls): TExpr + +function {:axiom} discriminantOf(decls: TypeDecls, ty: Ty): Option + +function {:axiom} applyChain(body: TExpr, chain: seq): TExpr + +function {:axiom} presentMatchExpr(f: PresentFact, some: TExpr, none: TExpr, ty: Ty): TExpr + ensures presentMatchExpr(f, some, none, ty) == + TExpr.someMatch(f.scrutinee, f.binder, f.innerTy, + if f.truthiness && FalsyCapable(f.innerTy) + then TExpr.conditional(TExpr.var_(f.binder, f.innerTy), some, none, ty) + else some, + none, ty) + +function {:axiom} leadingPresent(cond: TExpr): Option + ensures leadingPresent(cond).Some? ==> cond.binop? && cond.op == "&&" + +function {:axiom} freshOcBinder(ctx: CondCtx): MintedBinder + ensures freshOcBinder(ctx).ctx.decls == ctx.decls + +function {:axiom} arrayBoundsCond(arr: TExpr, idx: TExpr): TExpr + ensures arrayBoundsCond(arr, idx).binop? && arrayBoundsCond(arr, idx).op == "&&" + ensures arrayBoundsCond(arr, idx).ty.bool_? + +function {:axiom} exprEqual(a: TExpr, b: TExpr): bool + ensures exprEqual(a, b) ==> Inert(a) && Inert(b) + ensures exprEqual(a, b) ==> !containsMethodCall(a) && !containsMethodCall(b) + +function {:axiom} binderHintForMapAccess(m: TExpr, k: TExpr, ctx: CondCtx): MintedBinder + ensures binderHintForMapAccess(m, k, ctx).ctx.decls == ctx.decls + +function {:axiom} builtinPure(id: string): bool + +function {:axiom} isArrayFact(call: TExpr, ctx: CondCtx): Option + ensures isArrayFact(call, ctx).Some? ==> Inert(call) + ensures isArrayFact(call, ctx).Some? ==> !containsMethodCall(call) + +function {:axiom} unionDeclOfTy(decls: TypeDecls, ty: Ty): Option + +function {:axiom} typeofStringFact(e: TExpr, ctx: CondCtx): Option + ensures typeofStringFact(e, ctx).Some? ==> Inert(e) + ensures typeofStringFact(e, ctx).Some? ==> !containsMethodCall(e) + +function {:axiom} leadingIsArray(cond: TExpr, ctx: CondCtx): Option + ensures leadingIsArray(cond, ctx).Some? ==> cond.binop? && cond.op == "&&" + +function {:axiom} variantFact(cond: TExpr, ctx: CondCtx): Option + ensures variantFact(cond, ctx).Some? ==> + ((cond.binop? && (cond.op == "===" || cond.op == "in")) || cond.call?) + ensures variantFact(cond, ctx).Some? ==> variantFact(cond, ctx).value.scrutinee.var_? + +function {:axiom} negVariantFact(cond: TExpr, ctx: CondCtx): Option + ensures negVariantFact(cond, ctx).Some? ==> + ((cond.binop? && cond.op == "!==") || (cond.unop? && cond.op == "!")) + ensures negVariantFact(cond, ctx).Some? ==> negVariantFact(cond, ctx).value.scrutinee.var_? + +datatype ExprOut = ExprOut(expr: TExpr, ctx: CondCtx) + +datatype ExprsOut = ExprsOut(exprs: seq, ctx: CondCtx) + +datatype StmtOut = StmtOut(stmt: TStmt, ctx: CondCtx) + +datatype StmtsOut = StmtsOut(stmts: seq, ctx: CondCtx) + +datatype StepsOut = StepsOut(steps: seq, ctx: CondCtx) + +datatype RecordFieldsOut = RecordFieldsOut(fields: seq, ctx: CondCtx) + +datatype ExprCasesOut = ExprCasesOut(cases: seq, ctx: CondCtx) + +datatype StmtCasesOut = StmtCasesOut(cases: seq, ctx: CondCtx) + +datatype SwitchCasesOut = SwitchCasesOut(cases: seq, ctx: CondCtx) + +datatype ElseChain = ElseChain(cases: seq, fallthrough: seq) + +datatype ConsumedRewrite = ConsumedRewrite(stmt: TStmt, consumed: int) + +datatype FunctionOut = FunctionOut(fn: TFunction, ctx: CondCtx) + +datatype ConstantsOut = ConstantsOut(constants: seq, ctx: CondCtx) + +datatype FunctionsOut = FunctionsOut(functions: seq, ctx: CondCtx) + +datatype ClassesOut = ClassesOut(classes: seq, ctx: CondCtx) + +datatype TExpr = var_(name: string, ty: Ty) | num(value_num: int, ty: Ty) | bigint(value_bigint: string, ty: Ty) | str(value_str: string, ty: Ty) | bool_(value_bool: bool, ty: Ty) | binop(op: string, left: TExpr, right: TExpr, ty: Ty) | unop(op: string, expr: TExpr, ty: Ty) | call(fn: TExpr, args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(obj: TExpr, idx: TExpr, ty: Ty) | field(obj: TExpr, field: string, ty: Ty, isDiscriminant: Option, ofVariant: Option) | record(spread: Option, fields: seq, ty: Ty) | arrayLiteral(elems: seq, ty: Ty) | lambda(params: seq, body_lambda: seq, ty: Ty) | conditional(cond: TExpr, then_: TExpr, else_: TExpr, ty: Ty) | optChain(obj: TExpr, chain: seq, ty: Ty) | nullish(left: TExpr, right: TExpr, ty: Ty) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: TExpr, noneBody: TExpr, ty: Ty) | tagMatch(scrutinee: TExpr, typeName: string, cases: seq, fallthrough: Option, ty: Ty) | forall_(var_: string, varTy: Ty, body_forall: TExpr, ty: Ty) | exists_(var_: string, varTy: Ty, body_exists: TExpr, ty: Ty) | havoc(ty: Ty) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +datatype CallKind = unknown | pure | method_ | spec_pure + +datatype TParam = TParam(name: string, ty: Ty) + +datatype TStmt = let(name: string, ty: Ty, mutable: bool, init: TExpr) | assign(target: string, value: TExpr) | return_(value: TExpr) | break_ | continue_ | expr(expr: TExpr) | if_(cond: TExpr, then_: seq, else_: seq) | while_(cond: TExpr, invariants: seq, decreases_: Option, doneWith: Option, body: seq) | switch(expr: TExpr, discriminant: string, cases_switch: seq, defaultBody: seq) | forof(names: seq, nameTypes: seq, iterable: TExpr, invariants: seq, doneWith: Option, body: seq) | throw | ghostLet(name: string, ty: Ty, init: TExpr) | ghostAssign(target: string, value: TExpr) | assert_(expr: TExpr, assumed: Option) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: seq, noneBody: seq) | tagMatch(scrutinee: TExpr, typeName: string, cases_tagMatch: seq, fallthrough: seq) + +datatype TSwitchCase = TSwitchCase(label_: string, body: seq) + +datatype TStmtCase = TStmtCase(variant: string, body: seq) + +datatype TChainStep = field(name: string, ty: Ty) | call(args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(idx: TExpr, ty: Ty) + +datatype TExprCase = TExprCase(variant: string, body: TExpr) + +datatype TRecordField = TRecordField(name: string, value: TExpr) + +datatype CondCtx = CondCtx(decls: seq, ocN: int) + +type TypeDecls = seq + +datatype TypeDeclInfo = TypeDeclInfo(name: string, typeParams: Option>, kind: string, values: Option>, discriminant: Option, variants: Option>, fields: Option>, aliasOf: Option, aliasOfTy: Option) + +datatype VariantInfo = VariantInfo(name: string, fields: seq) + +datatype FieldInfo = FieldInfo(name: string, tsType: string, type_: Option) + +datatype TFunction = TFunction(name: string, typeParams: seq, params: seq, returnTy: Ty, requires_: seq, ensures_: seq, decreases_: Option, isPure: bool, forcePure: bool, autohavoc: bool, body: seq) + +datatype TConst = TConst(name: string, ty: Ty, value: TExpr) + +datatype TClass = TClass(name: string, fields: seq, methods: seq) + +datatype TModule = TModule(file: string, typeDecls: seq, externs: seq, constants: seq, functions: seq, classes: seq) + +datatype TExtern = TExtern(qualified: string, flat: string, typeParams: seq, params: seq, returnTy: Ty, requires_: seq, ensures_: seq) + +datatype PresentFact = PresentFact(scrutinee: TExpr, innerTy: Ty, negated: bool, binder: string, truthiness: bool) + +datatype NoneDetector = NoneDetector(scrutinee: TExpr, innerTy: Ty, binder: string, residual: Option) + +datatype LeadingPresent = LeadingPresent(check: PresentFact, restCond: TExpr) + +datatype MintedBinder = MintedBinder(name: string, ctx: CondCtx) + +datatype IsArrayFact = IsArrayFact(scrutinee: TExpr, typeName: string, variant: string) + +datatype LeadingIsArray = LeadingIsArray(check: IsArrayFact, restCond: TExpr) + +datatype VariantFact = VariantFact(scrutinee: TExpr, scrutineeName: string, typeName: string, variant: string) + +function TStmt_kind(t: TStmt): string +{ + match t { + case let(i_0, i_1, i_2, i_3) => + "let" + case assign(i_0, i_1) => + "assign" + case return_(i_0) => + "return" + case break_ => + "break" + case continue_ => + "continue" + case expr(i_0) => + "expr" + case if_(i_0, i_1, i_2) => + "if" + case while_(i_0, i_1, i_2, i_3, i_4) => + "while" + case switch(i_0, i_1, i_2, i_3) => + "switch" + case forof(i_0, i_1, i_2, i_3, i_4, i_5) => + "forof" + case throw => + "throw" + case ghostLet(i_0, i_1, i_2) => + "ghostLet" + case ghostAssign(i_0, i_1) => + "ghostAssign" + case assert_(i_0, i_1) => + "assert" + case someMatch(i_0, i_1, i_2, i_3, i_4) => + "someMatch" + case tagMatch(i_0, i_1, i_2, i_3) => + "tagMatch" + } +} + +// ── Hand-authored (termination measure): structural sizes ─── +// Padded list sizes (1 per element) keep slice/concat arithmetic linear. +// Element and field hops live in separate functions (rank-axiom limits). + +function SE(e: TExpr): nat + decreases e +{ + match e { + case var_(_, _) => 1 + case num(_, _) => 1 + case bigint(_, _) => 1 + case str(_, _) => 1 + case bool_(_, _) => 1 + case havoc(_) => 1 + case binop(_, l, r, _) => 1 + SE(l) + SE(r) + case unop(_, x, _) => 1 + SE(x) + case call(f, args, _, _, _) => 1 + SE(f) + SEs(args) + case index(o, i, _) => 1 + SE(o) + SE(i) + case field(o, _, _, _, _) => 1 + SE(o) + case record(sp, fs, _) => 1 + SOE(sp) + SRFs(fs) + case arrayLiteral(es, _) => 1 + SEs(es) + case lambda(_, b, _) => 1 + SSs(b) + case conditional(c, t, e2, _) => 1 + SE(c) + SE(t) + SE(e2) + case optChain(o, ch, _) => 1 + SE(o) + SSteps(ch) + case nullish(l, r, _) => 1 + SE(l) + SE(r) + case someMatch(sc, _, _, sb, nb, _) => 1 + SE(sc) + SE(sb) + SE(nb) + case tagMatch(sc, _, cs, ft, _) => 1 + SE(sc) + SECs(cs) + SOE(ft) + case forall_(_, _, b, _) => 1 + SE(b) + case exists_(_, _, b, _) => 1 + SE(b) + } +} + +function SOE(o: Option): nat + decreases o +{ + match o { case Some(x) => 1 + SE(x) case None => 0 } +} + +function SEs(es: seq): nat + decreases es +{ + if |es| == 0 then 0 else 1 + SE(es[0]) + SEs(es[1..]) +} + +function SStep(c: TChainStep): nat + decreases c +{ + match c { + case field(_, _) => 1 + case call(args, _, _, _) => 1 + SEs(args) + case index(i, _) => 1 + SE(i) + } +} + +function SSteps(cs: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else 1 + SStep(cs[0]) + SSteps(cs[1..]) +} + +function SRF(f: TRecordField): nat + decreases f +{ + 1 + SE(f.value) +} + +function SRFs(fs: seq): nat + decreases fs +{ + if |fs| == 0 then 0 else SRF(fs[0]) + SRFs(fs[1..]) +} + +function SEC(c: TExprCase): nat + decreases c +{ + 1 + SE(c.body) +} + +function SECs(cs: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else SEC(cs[0]) + SECs(cs[1..]) +} + +function SSC(c: TStmtCase): nat + decreases c +{ + 1 + SSs(c.body) +} + +function SSCs(cs: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else SSC(cs[0]) + SSCs(cs[1..]) +} + +function SWC(c: TSwitchCase): nat + decreases c +{ + 1 + SSs(c.body) +} + +function SWCs(cs: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else SWC(cs[0]) + SWCs(cs[1..]) +} + +function SS(s: TStmt): nat + decreases s +{ + match s { + case let(_, _, _, init) => 1 + SE(init) + case assign(_, v) => 1 + SE(v) + case return_(v) => 1 + SE(v) + case break_ => 1 + case continue_ => 1 + case throw => 1 + case expr(x) => 1 + SE(x) + case if_(c, t, e2) => 1 + SE(c) + SSs(t) + SSs(e2) + case while_(c, invs, dec, dw, b) => 1 + SE(c) + SEs(invs) + SOE(dec) + SOE(dw) + SSs(b) + case switch(x, _, cs, db) => 1 + SE(x) + SWCs(cs) + SSs(db) + case forof(_, _, it, invs, dw, b) => 1 + SE(it) + SEs(invs) + SOE(dw) + SSs(b) + case ghostLet(_, _, init) => 1 + SE(init) + case ghostAssign(_, v) => 1 + SE(v) + case assert_(x, _) => 1 + SE(x) + case someMatch(sc, _, _, sb, nb) => 1 + SE(sc) + SSs(sb) + SSs(nb) + case tagMatch(sc, _, cs, ft) => 1 + SE(sc) + SSCs(cs) + SSs(ft) + } +} + +function SSs(ss: seq): nat + decreases ss +{ + if |ss| == 0 then 0 else 1 + SS(ss[0]) + SSs(ss[1..]) +} + +// ── Hand-authored (termination measure): redex counts + active weight ── +// PC/AC/DET count the condition atoms the `&&`/`||` drivers can extract +// (positive-or-negated presence, isArray/typeof, None-detectors). W* are +// root charges sized so each rule firing consumes strictly more weight +// than it constructs; AW* sum charges over the term. The ctx argument +// only feeds detection through ctx.decls (invariance axioms below). + +// Local mirror of condition-facts' isFalsyCapableTy (int/nat/string/bool). +function FalsyCapable(ty: Ty): bool +{ + ty.int_? || ty.nat_? || ty.string_? || ty.bool_? +} + +// ── Hand-authored (walk stability): inert shapes ──────────── +// The condition detectors match only access paths and the boolean spines +// built over them — never a `someMatch`/`tagMatch`/`conditional`, which is +// all any rule outputs. Since the recursive rebuild keeps the head, an +// inert walker result is the untouched input, so walking never *arms* a +// detector: every count below (PC/AC/DET) and every firability guard is +// non-increasing across the walk. +function Inert(e: TExpr): bool + decreases e +{ + match e { + case var_(_, _) => true + case num(_, _) => true + case bigint(_, _) => true + case str(_, _) => true + case bool_(_, _) => true + case havoc(_) => true + case field(o, _, _, _, _) => Inert(o) + case unop(_, x, _) => Inert(x) + case binop(_, l, r, _) => Inert(l) && Inert(r) + case index(o, i, _) => Inert(o) && Inert(i) + case call(f, args, _, _, _) => Inert(f) && InertEs(args) + case _ => false + } +} + +function InertEs(es: seq): bool + decreases es +{ + if |es| == 0 then true else Inert(es[0]) && InertEs(es[1..]) +} + +function PC(c: TExpr): nat + decreases c +{ + if c.binop? && c.op == "&&" then PC(c.left) + PC(c.right) + else if presentFact(c).Some? then 1 + else 0 +} + +function AC(c: TExpr, decls: seq): nat + decreases c +{ + if c.binop? && c.op == "&&" then AC(c.left, decls) + AC(c.right, decls) + else if isArrayFact(c, CondCtx(decls, 0)).Some? || typeofStringFact(c, CondCtx(decls, 0)).Some? then 1 + else if c.unop? && c.op == "!" && isArrayFact(c.expr, CondCtx(decls, 0)).Some? then 1 + else 0 +} + +function DET(c: TExpr, decls: seq): nat + decreases c +{ + if c.binop? && c.op == "||" then DET(c.left, decls) + DET(c.right, decls) + else if noneDetector(c, CondCtx(decls, 0)).Some? then 1 + else 0 +} + +function DETs(leaves: seq, decls: seq): nat + decreases leaves +{ + if |leaves| == 0 then 0 + else (if noneDetector(leaves[0], CondCtx(decls, 0)).Some? then 1 else 0) + DETs(leaves[1..], decls) +} + +lemma DETsZero(leaves: seq, decls: seq) + requires DETs(leaves, decls) == 0 + ensures forall k :: 0 <= k < |leaves| ==> noneDetector(leaves[k], CondCtx(decls, 0)).None? + decreases leaves +{ + if |leaves| > 0 { + DETsZero(leaves[1..], decls); + assert forall k :: 1 <= k < |leaves| ==> leaves[1..][k-1] == leaves[k]; + } +} + +lemma DETsWitness(leaves: seq, decls: seq) + requires DETs(leaves, decls) >= 1 + ensures exists k :: 0 <= k < |leaves| && noneDetector(leaves[k], CondCtx(decls, 0)).Some? +{ + if !(exists k :: 0 <= k < |leaves| && noneDetector(leaves[k], CondCtx(decls, 0)).Some?) { + DETsAllNone(leaves, decls); + } +} + +lemma DETsAllNone(leaves: seq, decls: seq) + requires forall k :: 0 <= k < |leaves| ==> noneDetector(leaves[k], CondCtx(decls, 0)).None? + ensures DETs(leaves, decls) == 0 + decreases leaves +{ + if |leaves| > 0 { + assert forall k :: 1 <= k < |leaves| ==> leaves[1..][k-1] == leaves[k]; + DETsAllNone(leaves[1..], decls); + } +} + +lemma DETsPositive(leaves: seq, k: int, decls: seq) + requires 0 <= k < |leaves| + requires noneDetector(leaves[k], CondCtx(decls, 0)).Some? + ensures DETs(leaves, decls) >= 1 + decreases k +{ + if k > 0 { DETsPositive(leaves[1..], k - 1, decls); } +} + +lemma SSsSplit(ss: seq, k: int) + requires 0 <= k <= |ss| + ensures SSs(ss) == SSs(ss[..k]) + SSs(ss[k..]) + decreases k +{ + if k == 0 { + assert ss[..0] == []; + assert ss[0..] == ss; + } else { + SSsSplit(ss[1..], k - 1); + assert ss[..k][1..] == ss[1..][..k-1]; + assert ss[k..] == ss[1..][k-1..]; + } +} + +lemma HCTailIrrelevant(head: TStmt, t1: seq, t2: seq, decls: seq) + requires (|t1| >= 1) == (|t2| >= 1) + ensures HC(head, t1, decls) == HC(head, t2, decls) +{} + +lemma WEConditionalBound(c: TExpr, t: TExpr, e2: TExpr, ty: Ty, decls: seq) + ensures WE(TExpr.conditional(c, t, e2, ty), decls) + <= 4 + 2*PC(c) + 2*AC(c, decls) + AWE(t, decls) + AWE(e2, decls) +{ +} + +lemma {:vcs_split_on_every_assert} AWSsPrefixSuffix(ss: seq, k: int, decls: seq) + requires 0 <= k <= |ss| + ensures AWSs(ss, decls) >= AWSs(ss[..k], decls) + AWSs(ss[k..], decls) + decreases k +{ + if k == 0 { + assert ss[..0] == []; + assert ss[0..] == ss; + } else { + AWSsPrefixSuffix(ss[1..], k - 1, decls); + assert ss[..k][1..] == ss[1..][..k-1]; + assert ss[k..] == ss[1..][k-1..]; + assert ss[..k][0] == ss[0]; + if |ss[..k][1..]| >= 1 == |ss[1..]| >= 1 { + HCTailIrrelevant(ss[0], ss[..k][1..], ss[1..], decls); + } + } +} + +// A firable or-chain head carries detector weight, so a weightless list +// can never reach that rule. +lemma OrChainHasWeight(s: TStmt, rest: seq, decls: seq) + requires FOrChain(s, rest, decls) + ensures HC(s, rest, decls) >= 1 +{ + // Name the product: left inline, Z3 re-expands it at every use and the + // nonlinear step stops converging. + var a := DET(s.cond, decls) + SE(s.cond); + var m := 16 + 4 * AWSs(s.then_, decls); + assert SE(s.cond) >= 1 && a >= 1; + MulAtLeast(a, m); + assert HC(s, rest, decls) >= a * m >= m >= 1; +} + +// The early-return rules build a match whose arms are the block's own +// pieces; the head charge covers the one arm that gets duplicated (the +// terminating branch appears both under the falsy gate and as the None +// arm), with 2 to spare so the re-walk strictly shrinks. +lemma ConsumeSiteShrinks(s: TStmt, rest: seq, out: TStmt, decls: seq) + requires ruleEarlyReturnConsume(s, rest).Some? && out == ruleEarlyReturnConsume(s, rest).value + ensures AWS(out, decls) + 2 <= HC(s, rest, decls) + AWS(s, decls) + AWSs(rest, decls) +{ + var chk := presentFact(s.cond).value; + var nb := if chk.negated then s.then_ else s.else_; + presentFactScrutineeWeightless(s.cond); + assert s == TStmt.if_(s.cond, s.then_, s.else_); + assert AWS(s, decls) == WS(s, decls) + AWE(s.cond, decls) + AWSs(s.then_, decls) + AWSs(s.else_, decls); + assert out == presentMatchStmts(chk, rest, nb); + if chk.truthiness && FalsyCapable(chk.innerTy) { + var gate := TStmt.if_(TExpr.var_(chk.binder, chk.innerTy), rest, nb); + leadingIsArrayDeclsOnly(gate.cond, CondCtx(decls, 0), CondCtx(decls, 0)); + assert AWE(gate.cond, decls) == WE(gate.cond, decls) == 0; + assert WS(gate, decls) == 0; + assert HC(gate, [], decls) == 0; + assert AWSs([gate], decls) == AWSs(rest, decls) + AWSs(nb, decls); + } + assert AWS(out, decls) <= AWSs(rest, decls) + 2 * AWSs(nb, decls); +} + +// ── TEMPORARY: the one open obligation, assumed ───────────── +// The lemma below has `assume {:axiom} false;` for a body — the only +// assumption in this file that is not a fact about an imported function. +// It assumes part of the theorem, so the module's verification is green +// modulo exactly this. Grep for `assume` to find it. Both are the same +// charge-design defect: each rule re-walks a construction holding an +// un-walked copy of the terminating branch under a residual guard that +// carries charges of its own, while the head charge is a flat count. +// Raising a constant does not close it — the inner charge has the same +// coefficient and cancels. The fix is a head charge that scales with the +// redexes left in the guard: +// DET(cond) * (A + B*AWSs(then_) + C*PCs(flattenOr(cond)) + D*ACs(flattenOr(cond))) +// with new leafwise PCs/ACs sums, plus PC/AC bounds added to +// `noneDetectorResidualBounds` (it bounds SE and AWE only today). +// Until then, DO NOT read this module's P1 claim as discharged. + +// Or-chain with exactly one residual leaf. Two or more leaves make the +// guard a `||`, which no if-position driver reads — that case is proved, +// in `OrChainInnerShrinks`. With one leaf the guard IS that leaf, so it +// can be an `&&` chain, and then `WS(innerIf)` charges `PC`/`AC` of it. +// The multiplicative treatment that closed the sibling site +// (`SiteCharge`, scaling by the guard's size) does not transfer: there, +// the rewrite provably shrinks the guard; here the residual can be the +// same size as the disjunct it replaces, and the residual's `PC`/`AC` are +// bounded by nothing in the original `||` cond — `PC` of a `||` is 0 by +// definition, so the cond carries no budget for them. Closing this wants +// leafwise `PCs`/`ACs` sums over `flattenOr(cond)` as measure components, +// with residual bounds on them added to `noneDetectorResidualBounds` +// (which bounds `SE` and `AWE` only). +lemma FlattenOrSingleton(e: TExpr, xs: seq) + requires flattenOr(e) == xs && |xs| == 1 + ensures !(e.binop? && e.op == "||") +{ + if e.binop? && e.op == "||" { + flattenOrSize(e.left); + flattenOrSize(e.right); + } +} + +// What the residual guard can cost: its own root charge plus the two +// early-return rules it can arm, each bounded by the guard's size. +lemma {:vcs_split_on_every_assert} OrChainInnerBound(oc: TExpr, thenS: seq, rest: seq, decls: seq) + requires !(oc.binop? && oc.op == "||") + ensures AWSs([TStmt.if_(oc, thenS, [])] + rest, decls) + <= 4 + 3 * SE(oc) + 3 * AWSs(thenS, decls) + + SE(oc) * (12 + 4 * AWSs(thenS, decls)) + + AWE(oc, decls) + AWSs(rest, decls) +{ + var innerIf := TStmt.if_(oc, thenS, []); + var A := AWSs(thenS, decls); + PCleSE(oc); + ACleSE(oc, decls); + leadingIsArrayDeclsOnly(oc, CondCtx(decls, 0), CondCtx(decls, 0)); + assert !FOrChain(innerIf, rest, decls); + assert WS(innerIf, decls) <= 2 + PC(oc) + AC(oc, decls) + A; + assert HC(innerIf, rest, decls) <= 2 + PC(oc) + A + SE(oc) * (12 + 4 * A); + assert AWS(innerIf, decls) == WS(innerIf, decls) + AWE(oc, decls) + A + AWSs([], decls); + assert ([innerIf] + rest)[1..] == rest; + assert AWSs([innerIf] + rest, decls) + == HC(innerIf, rest, decls) + AWS(innerIf, decls) + AWSs(rest, decls); +} + +// What the original `||` guard is charged: no if-position or early-return +// driver reads a `||`, so its whole charge is the or-chain one. +lemma OrChainOuterCharge(s: TStmt, rest: seq, decls: seq) + requires s.if_? && s.else_ == [] && s.cond.binop? && s.cond.op == "||" + requires FOrChain(s, rest, decls) + ensures HC(s, rest, decls) + == (DET(s.cond, decls) + SE(s.cond)) * (16 + 4 * AWSs(s.then_, decls)) + ensures AWS(s, decls) == AWE(s.cond, decls) + AWSs(s.then_, decls) +{ + assert s == TStmt.if_(s.cond, s.then_, s.else_); + leadingIsArrayDeclsOnly(s.cond, CondCtx(decls, 0), CondCtx(decls, 0)); + assert WS(s, decls) == 0; +} + +// The site's arithmetic, over bare nats: the guard shrinks by at least two +// units of size and one detector, and 3 * (16 + 4A) of charge outweighs +// everything the residual guard can be billed for. +lemma OrChainArith(seOc: nat, seCond: nat, det: nat, a: nat, + aweOc: nat, aweCond: nat, tail: nat, hc: nat, aws: nat, lhs: nat) + requires seOc + 2 <= seCond && det >= 1 && aweOc <= aweCond + requires lhs <= 4 + 3 * seOc + 3 * a + seOc * (12 + 4 * a) + aweOc + tail + requires hc == (det + seCond) * (16 + 4 * a) + requires aws == aweCond + a + ensures lhs < hc + aws + tail +{ + MulMonotone(seOc + 3, det + seCond, 16 + 4 * a); + assert (seOc + 3) * (16 + 4 * a) == seOc * (12 + 4 * a) + 4 * seOc + 48 + 12 * a; +} + +lemma OrChainSingleResidualShrinks(s: TStmt, rest: seq, oc: TExpr, residualLeaves: seq, decls: seq) + requires s.if_? && |rest| >= 1 && s.else_ == [] && |s.then_| > 0 && isTerminating(s.then_) + requires FOrChain(s, rest, decls) + requires flattenOr(oc) == residualLeaves + requires |residualLeaves| == 1 + requires AWE(oc, decls) <= AWEs(residualLeaves, decls) + requires AWEs(residualLeaves, decls) <= AWE(s.cond, decls) + requires DETs(residualLeaves, decls) + 1 <= DET(s.cond, decls) + requires SE(oc) + 2 <= SE(s.cond) + ensures AWSs([TStmt.if_(oc, s.then_, [])] + rest, decls) + < HC(s, rest, decls) + AWS(s, decls) + AWSs(rest, decls) +{ + var A := AWSs(s.then_, decls); + var M := 16 + 4 * A; + FlattenOrSingleton(oc, residualLeaves); + OrChainInnerBound(oc, s.then_, rest, decls); + OrChainOuterCharge(s, rest, decls); + assert DET(s.cond, decls) >= 1; + OrChainArith(SE(oc), SE(s.cond), DET(s.cond, decls), A, + AWE(oc, decls), AWE(s.cond, decls), AWSs(rest, decls), + HC(s, rest, decls), AWS(s, decls), + AWSs([TStmt.if_(oc, s.then_, [])] + rest, decls)); +} + +// Bound-optional early return: `a?.x !== b?.y` leaves the second chain in +// the rewritten inner guard, so the inner `if` re-arms this same rule. +// (The `ruleEarlyReturnConsume` half of the site is proved, in +// `ConsumeSiteShrinks`.) +// Atom counts never exceed the term's size. +lemma PCleSE(c: TExpr) + ensures PC(c) <= SE(c) + decreases c +{ + if c.binop? && c.op == "&&" { PCleSE(c.left); PCleSE(c.right); } +} + +lemma ACleSE(c: TExpr, decls: seq) + ensures AC(c, decls) <= SE(c) + decreases c +{ + if c.binop? && c.op == "&&" { ACleSE(c.left, decls); ACleSE(c.right, decls); } +} + +lemma MulMonotone(a: nat, b: nat, m: nat) + requires a <= b + ensures a * m <= b * m +{ +} + +// The multi-leaf site's arithmetic, over bare nats. +lemma OrChainMultiArith(inner: nat, outer: nat, m: nat, aweOc: nat, aweCond: nat, + a: nat, tail: nat, lhs: nat, hc: nat, aws: nat) + requires inner < outer && m >= 1 && aweOc <= aweCond + requires lhs <= inner * m + aweOc + a + tail + requires hc >= outer * m + requires aws == aweCond + a + ensures lhs < hc + aws + tail +{ + MulStrict(inner, outer, m); +} + +lemma MulStrict(a: nat, b: nat, m: nat) + requires a < b && m >= 1 + ensures a * m < b * m +{ +} + +lemma MulAtLeast(a: nat, m: nat) + requires a >= 1 + ensures a * m >= m +{ +} + +lemma {:vcs_split_on_every_assert} OptChainCompareSiteShrinks(s: TStmt, rest: seq, out: TStmt, ctx: CondCtx) + requires ruleEarlyReturnOptChainCompare(s, rest, ctx).Some? + && out == ruleEarlyReturnOptChainCompare(s, rest, ctx).value + ensures AWS(out, ctx.decls) < HC(s, rest, ctx.decls) + AWS(s, ctx.decls) + AWSs(rest, ctx.decls) +{ + var decls := ctx.decls; + var c := s.cond; + var ocOnLeft := c.left.optChain?; + var oc := if ocOnLeft then c.left else c.right; + var lit := if ocOnLeft then c.right else c.left; + var innerTy := oc.obj.ty.inner; + var binder := freshName(binderHintFor(oc.obj).value); + var bv := TExpr.var_(binder, innerTy); + var unwrapped := restoreDiscriminantFlag(applyChain(bv, oc.chain), decls); + var ig := TExpr.binop("!==", unwrapped, lit, Ty.bool_); + var innerIf := TStmt.if_(ig, s.then_, []); + var A := AWSs(s.then_, decls); + assert out == TStmt.someMatch(oc.obj, binder, innerTy, [innerIf] + rest, s.then_); + + // the rewritten guard is strictly smaller than the one it replaces, and + // weighs no more: it trades the chain node for its application + applyChainBounds(bv, oc.chain); + restoreDiscriminantFlagBounds(applyChain(bv, oc.chain), decls); + assert c == TExpr.binop(c.op, c.left, c.right, c.ty); + assert oc == TExpr.optChain(oc.obj, oc.chain, oc.ty); + assert SE(bv) == 1 && SE(oc.obj) >= 1; + assert SE(ig) + 1 <= SE(c); + assert AWE(oc, decls) == WE(oc, decls) + AWE(oc.obj, decls) + AWSteps(oc.chain, decls); + assert WE(oc, decls) >= 2; + assert AWE(bv, decls) == WE(bv, decls) == 0; + assert AWE(ig, decls) == WE(ig, decls) + AWE(unwrapped, decls) + AWE(lit, decls); + assert WE(ig, decls) == 0; + assert AWE(unwrapped, decls) <= AWSteps(oc.chain, decls) + 2; + assert AWE(c, decls) == WE(c, decls) + AWE(oc, decls) + AWE(lit, decls); + assert AWE(ig, decls) <= AWE(c, decls); + + // a `!==` guard is not an `&&`, so its atom counts are at most one each + assert PC(ig) <= 1 && AC(ig, decls) <= 1; + leadingIsArrayDeclsOnly(ig, CondCtx(decls, 0), CondCtx(decls, 0)); + assert WS(innerIf, decls) <= 3 + A; + assert !FOrChain(innerIf, rest, decls); + assert HC(innerIf, rest, decls) + <= 3 + A + SE(ig) * (12 + 4 * A); + assert AWS(innerIf, decls) == WS(innerIf, decls) + AWE(ig, decls) + A + AWSs([], decls); + assert ([innerIf] + rest)[1..] == rest; + assert AWSs([innerIf] + rest, decls) + == HC(innerIf, rest, decls) + AWS(innerIf, decls) + AWSs(rest, decls); + assert AWS(out, decls) == AWSs([innerIf] + rest, decls) + A; + + assert s == TStmt.if_(c, s.then_, s.else_) && s.else_ == []; + assert AWS(s, decls) == WS(s, decls) + AWE(c, decls) + A; + var M := 12 + 4 * A; + assert HC(s, rest, decls) >= SE(c) * M; + MulMonotone(SE(ig) + 1, SE(c), M); + assert (SE(ig) + 1) * M == SE(ig) * M + M; +} + +// The or-chain rule's re-walk target: the residual guard plus the block's +// tail. The detector count in the head charge pays for it — the rewrite +// consumes at least one detector from the chain. +lemma {:vcs_split_on_every_assert} OrChainInnerShrinks(s: TStmt, rest: seq, oc: TExpr, residualLeaves: seq, decls: seq) + requires s.if_? && |rest| >= 1 && s.else_ == [] && |s.then_| > 0 && isTerminating(s.then_) + requires FOrChain(s, rest, decls) + requires flattenOr(oc) == residualLeaves + requires |residualLeaves| >= 2 + requires AWE(oc, decls) <= AWEs(residualLeaves, decls) + requires AWEs(residualLeaves, decls) <= AWE(s.cond, decls) + requires DETs(residualLeaves, decls) + 1 <= DET(s.cond, decls) + requires SEs(residualLeaves) <= SE(s.cond) + 1 + ensures AWSs([TStmt.if_(oc, s.then_, [])] + rest, decls) + < HC(s, rest, decls) + AWS(s, decls) + AWSs(rest, decls) +{ + var innerIf := TStmt.if_(oc, s.then_, []); + var A := AWSs(s.then_, decls); + var M := 16 + 4 * A; + var lo := DET(oc, decls) + SE(oc); + var hi := DET(s.cond, decls) + SE(s.cond); + // With two or more residual leaves the guard is itself a `||`, which no + // if-position or early-return driver reads — so the inner head and root + // charges collapse to the detector count, and the rewrite consumed one. + assert oc.binop? && oc.op == "||"; + leadingIsArrayDeclsOnly(oc, CondCtx(decls, 0), CondCtx(decls, 0)); + assert WS(innerIf, decls) == 0; + assert ruleEarlyReturnConsume(innerIf, rest).None?; + assert ruleEarlyReturnOptChainCompare(innerIf, rest, CondCtx(decls, 0)).None?; + assert innerIf.cond == oc && innerIf.then_ == s.then_ && innerIf.else_ == []; + if FOrChain(innerIf, rest, decls) { + assert HC(innerIf, rest, decls) == lo * M; + } else { + assert HC(innerIf, rest, decls) == 0; + } + assert HC(innerIf, rest, decls) <= lo * M; + assert ([innerIf] + rest)[1..] == rest; + assert AWSs([innerIf] + rest, decls) + == HC(innerIf, rest, decls) + AWS(innerIf, decls) + AWSs(rest, decls); + assert AWS(innerIf, decls) + == WS(innerIf, decls) + AWE(oc, decls) + AWSs(s.then_, decls) + AWSs([], decls); + assert s == TStmt.if_(s.cond, s.then_, s.else_); + assert AWS(s, decls) == WS(s, decls) + AWE(s.cond, decls) + AWSs(s.then_, decls); + flattenOrSize(oc); + assert DET(oc, decls) == DETs(residualLeaves, decls); + assert SEs(residualLeaves) == SE(oc) + 1; + assert SE(oc) <= SE(s.cond); + assert WS(s, decls) == 0; + leadingIsArrayDeclsOnly(s.cond, CondCtx(decls, 0), CondCtx(decls, 0)); + assert HC(s, rest, decls) == hi * M; + assert AWSs([innerIf] + rest, decls) <= lo * M + AWE(oc, decls) + A + AWSs(rest, decls); + assert lo < hi; + OrChainMultiArith(lo, hi, M, AWE(oc, decls), AWE(s.cond, decls), A, + AWSs(rest, decls), + AWSs([innerIf] + rest, decls), HC(s, rest, decls), AWS(s, decls)); +} + +// Stated as the bare decreases fact the caller needs: the head/tail +// decomposition stays inside this lemma, because letting `HC`/`AWSs` terms +// into `walkStmts`' context sets off the recursive weight family and the +// method stops converging. +lemma ConsumedSiteShrinks(stmts: seq, out: TStmt, ctx: CondCtx) + requires |stmts| >= 1 + requires ruleEarlyReturnConsume(stmts[0], stmts[1..]) == Option.Some(out) + || ruleEarlyReturnOptChainCompare(stmts[0], stmts[1..], ctx) == Option.Some(out) + ensures AWS(out, ctx.decls) < AWSs(stmts, ctx.decls) +{ + var s := stmts[0]; + var rest := stmts[1..]; + assert AWSs(stmts, ctx.decls) + == HC(s, rest, ctx.decls) + AWS(s, ctx.decls) + AWSs(rest, ctx.decls); + if ruleEarlyReturnConsume(s, rest) == Option.Some(out) { + ConsumeSiteShrinks(s, rest, out, ctx.decls); + } else { + OptChainCompareSiteShrinks(s, rest, out, ctx); + } +} + +// A walked head arms no list rule. Each of the three reads the head's cond +// through an inert shape (presence check, `!==` over an optional chain, or +// a detector leaf), so the walk returned it unchanged — and the three rules +// were already tried on the original and declined. Stated one rule at a +// time: proving all three at once does not converge. +// TEMPORARY (third assumption, and unlike the other two this one is not a +// design gap — the statement is true and was going through inside +// `walkStmts`' monolithic VC before the site lemmas were introduced; it +// just does not converge on its own. 44 of its sub-VCs discharge; the +// `ruleEarlyReturnConsume` and `FOrChain` cases are the holdouts.) +// A walked head arms no list rule. Each of the three reads the head's cond +// through an inert shape (a presence check, a `!==` over an optional chain, +// or a detector leaf), so the walk returned it unchanged — and all three +// were already tried on the original and declined. One lemma per rule: +// proving the three together does not converge. +lemma WalkedHeadNoConsume(s: TStmt, rest: seq, w: TStmt, after: seq, ctx: CondCtx) + requires ruleEarlyReturnConsume(s, rest).None? + requires ruleEarlyReturnOptChainCompare(s, rest, ctx).None? + requires !FOrChain(s, rest, ctx.decls) + requires |after| > 0 ==> |rest| > 0 + requires w.if_? ==> (s.if_? && Tame(w.cond, s.cond, ctx.decls) + && (w.cond.binop? ==> (!(w.cond.left.optChain? && w.cond.left.obj.ty.optional?) + && !(w.cond.right.optChain? && w.cond.right.obj.ty.optional?))) + && (|w.then_| == 0 <==> |s.then_| == 0) + && (|w.else_| == 0 <==> |s.else_| == 0) + && (isTerminating(w.then_) ==> isTerminating(s.then_)) + && (isTerminating(w.else_) ==> isTerminating(s.else_))) + ensures ruleEarlyReturnConsume(w, after).None? +{ + if w.if_? && |after| > 0 && presentFact(w.cond).Some? { + assert Inert(w.cond); + assert w.cond == s.cond; + } +} + +lemma WalkedHeadNoOptChainCompare(s: TStmt, rest: seq, w: TStmt, after: seq, ctx: CondCtx) + requires ruleEarlyReturnConsume(s, rest).None? + requires ruleEarlyReturnOptChainCompare(s, rest, ctx).None? + requires !FOrChain(s, rest, ctx.decls) + requires |after| > 0 ==> |rest| > 0 + requires w.if_? ==> (s.if_? && Tame(w.cond, s.cond, ctx.decls) + && (w.cond.binop? ==> (!(w.cond.left.optChain? && w.cond.left.obj.ty.optional?) + && !(w.cond.right.optChain? && w.cond.right.obj.ty.optional?))) + && (|w.then_| == 0 <==> |s.then_| == 0) + && (|w.else_| == 0 <==> |s.else_| == 0) + && (isTerminating(w.then_) ==> isTerminating(s.then_)) + && (isTerminating(w.else_) ==> isTerminating(s.else_))) + ensures ruleEarlyReturnOptChainCompare(w, after, CondCtx(ctx.decls, 0)).None? +{ +} + +lemma {:vcs_split_on_every_assert} WalkedHeadNoOrChain(s: TStmt, rest: seq, w: TStmt, after: seq, ctx: CondCtx) + requires ruleEarlyReturnConsume(s, rest).None? + requires ruleEarlyReturnOptChainCompare(s, rest, ctx).None? + requires !FOrChain(s, rest, ctx.decls) + requires |after| > 0 ==> |rest| > 0 + requires w.if_? ==> (s.if_? && Tame(w.cond, s.cond, ctx.decls) + && (w.cond.binop? ==> (!(w.cond.left.optChain? && w.cond.left.obj.ty.optional?) + && !(w.cond.right.optChain? && w.cond.right.obj.ty.optional?))) + && (|w.then_| == 0 <==> |s.then_| == 0) + && (|w.else_| == 0 <==> |s.else_| == 0) + && (isTerminating(w.then_) ==> isTerminating(s.then_)) + && (isTerminating(w.else_) ==> isTerminating(s.else_))) + ensures !FOrChain(w, after, ctx.decls) +{ + if FOrChain(w, after, ctx.decls) { + // shape, conjunct by conjunct + assert |rest| > 0; + assert s.if_? && |s.then_| > 0 && isTerminating(s.then_); + assert |s.else_| == 0 && s.else_ == []; + assert s.cond.binop? && s.cond.op == "||"; + flattenOrSize(s.cond.left); + flattenOrSize(s.cond.right); + assert flattenOr(s.cond) == flattenOr(s.cond.left) + flattenOr(s.cond.right); + assert |flattenOr(s.cond)| >= 2; + // and the detector, via the counts + var ls := flattenOr(w.cond); + var k :| 0 <= k < |ls| && noneDetector(ls[k], CondCtx(ctx.decls, 0)).Some?; + DETsPositive(ls, k, ctx.decls); + flattenOrSize(w.cond); + assert DET(w.cond, ctx.decls) >= 1; + assert DET(s.cond, ctx.decls) >= 1; + flattenOrSize(s.cond); + DETsWitness(flattenOr(s.cond), ctx.decls); + assert FOrChain(s, rest, ctx.decls); + } +} + +lemma WalkedHeadDeclines(s: TStmt, rest: seq, w: TStmt, after: seq, ctx: CondCtx) + requires ruleEarlyReturnConsume(s, rest).None? + requires ruleEarlyReturnOptChainCompare(s, rest, ctx).None? + requires !FOrChain(s, rest, ctx.decls) + requires |after| > 0 ==> |rest| > 0 + requires w.if_? ==> (s.if_? && Tame(w.cond, s.cond, ctx.decls) + && (w.cond.binop? ==> (!(w.cond.left.optChain? && w.cond.left.obj.ty.optional?) + && !(w.cond.right.optChain? && w.cond.right.obj.ty.optional?))) + && (|w.then_| == 0 <==> |s.then_| == 0) + && (|w.else_| == 0 <==> |s.else_| == 0) + && (isTerminating(w.then_) ==> isTerminating(s.then_)) + && (isTerminating(w.else_) ==> isTerminating(s.else_))) + ensures ruleEarlyReturnConsume(w, after).None? + ensures ruleEarlyReturnOptChainCompare(w, after, CondCtx(ctx.decls, 0)).None? + ensures !FOrChain(w, after, ctx.decls) +{ + WalkedHeadNoConsume(s, rest, w, after, ctx); + WalkedHeadNoOptChainCompare(s, rest, w, after, ctx); + WalkedHeadNoOrChain(s, rest, w, after, ctx); +} + +lemma WalkedHeadInert(s: TStmt, rest: seq, w: TStmt, after: seq, ctx: CondCtx) + requires ruleEarlyReturnConsume(s, rest).None? + requires ruleEarlyReturnOptChainCompare(s, rest, ctx).None? + requires !FOrChain(s, rest, ctx.decls) + requires |after| > 0 ==> |rest| > 0 + requires w.if_? ==> (s.if_? && Tame(w.cond, s.cond, ctx.decls) + && (w.cond.binop? ==> (!(w.cond.left.optChain? && w.cond.left.obj.ty.optional?) + && !(w.cond.right.optChain? && w.cond.right.obj.ty.optional?))) + && (|w.then_| == 0 <==> |s.then_| == 0) + && (|w.else_| == 0 <==> |s.else_| == 0) + && (isTerminating(w.then_) ==> isTerminating(s.then_)) + && (isTerminating(w.else_) ==> isTerminating(s.else_))) + ensures HC(w, after, ctx.decls) == 0 +{ + WalkedHeadDeclines(s, rest, w, after, ctx); +} + +// The let-cond desugar: the charge of 8 covers the two statements it emits +// (a mutable let plus a presence match holding the guarded assignment), +// each of which is built from already-walked pieces. +lemma LetCondExpansionShrinks(s: TStmt, out: seq, decls: seq) + requires ruleLetCondAndOptional(s).Some? && out == ruleLetCondAndOptional(s).value + requires AWS(s, decls) == WS(s, decls) + ensures AWSs(out, decls) < AWS(s, decls) + ensures forall i :: 0 <= i < |out| ==> !out[i].if_? && (out[i].let? ==> out[i].mutable) +{ + var lp := leadingPresent(s.init.cond).value; + leadingPresentShrinks(s.init.cond); + leadingPresentCheckWeightless(s.init.cond); + assert s == TStmt.let(s.name, s.ty, s.mutable, s.init); + assert AWS(s, decls) == WS(s, decls) + AWE(s.init, decls); + assert AWE(s.init, decls) == 0; + assert s.init == TExpr.conditional(s.init.cond, s.init.then_, s.init.else_, s.init.ty); + assert AWE(s.init.cond, decls) == 0 && AWE(s.init.then_, decls) == 0 && AWE(s.init.else_, decls) == 0; + assert AWE(lp.restCond, decls) == 0; + assert WS(s, decls) >= 8 + PC(s.init.cond) + AC(s.init.cond, decls); + var assignIf := TStmt.if_(lp.restCond, [TStmt.assign(s.name, s.init.then_)], []); + var pm := presentMatchStmts(lp.check, [assignIf], []); + var letOut := TStmt.let(s.name, s.ty, true, s.init.else_); + assert out == [letOut, pm]; + assert out[1..] == [pm]; + assert HC(TStmt.assign(s.name, s.init.then_), [], decls) == 0; + assert AWS(TStmt.assign(s.name, s.init.then_), decls) == AWE(s.init.then_, decls); + assert AWSs([TStmt.assign(s.name, s.init.then_)], decls) == 0; + leadingIsArrayDeclsOnly(assignIf.cond, CondCtx(decls, 0), CondCtx(decls, 0)); + assert WS(assignIf, decls) <= 2 + PC(lp.restCond) + AC(lp.restCond, decls); + assert AWS(assignIf, decls) == WS(assignIf, decls); + assert HC(assignIf, [], decls) == 0; + assert AWSs([assignIf], decls) == WS(assignIf, decls); + var gate := TStmt.if_(TExpr.var_(lp.check.binder, lp.check.innerTy), [assignIf], []); + leadingIsArrayDeclsOnly(gate.cond, CondCtx(decls, 0), CondCtx(decls, 0)); + assert AWE(gate.cond, decls) == WE(gate.cond, decls); + if lp.check.truthiness && FalsyCapable(lp.check.innerTy) { + assert AWE(gate.cond, decls) == 0; + assert WS(gate, decls) == 0; + assert HC(gate, [], decls) == 0; + assert AWSs([gate], decls) == AWSs([assignIf], decls); + assert pm == TStmt.someMatch(lp.check.scrutinee, lp.check.binder, lp.check.innerTy, [gate], []); + } + assert AWS(pm, decls) == WS(assignIf, decls); + assert WS(letOut, decls) <= 1; + assert AWS(letOut, decls) == WS(letOut, decls); + assert HC(letOut, out[1..], decls) == 0 && HC(pm, [], decls) == 0; + assert AWSs([pm], decls) == AWS(pm, decls); + assert AWSs(out, decls) == HC(letOut, out[1..], decls) + AWS(letOut, decls) + AWSs(out[1..], decls); +} + +lemma AWSsConcatNoIf(a: seq, b: seq, decls: seq) + requires forall i :: 0 <= i < |a| ==> !a[i].if_? + ensures AWSs(a + b, decls) == AWSs(a, decls) + AWSs(b, decls) + ensures |b| > 0 ==> (a + b)[|a + b| - 1] == b[|b| - 1] + ensures |b| == 0 ==> a + b == a + decreases a +{ + if |a| == 0 { + assert a + b == b; + } else { + assert (a + b)[1..] == a[1..] + b; + assert forall i :: 0 <= i < |a| - 1 ==> a[1..][i] == a[i+1]; + AWSsConcatNoIf(a[1..], b, decls); + assert HC(a[0], a[1..] + b, decls) == 0; + assert HC(a[0], a[1..], decls) == 0; + } +} + +lemma AWSCsConcat(a: seq, b: seq, decls: seq) + ensures AWSCs(a + b, decls) == AWSCs(a, decls) + AWSCs(b, decls) + decreases a +{ + if |a| == 0 { + assert a + b == b; + } else { + AWSCsConcat(a[1..], b, decls); + assert (a + b)[1..] == a[1..] + b; + } +} + +lemma SSCsConcat(a: seq, b: seq) + ensures SSCs(a + b) == SSCs(a) + SSCs(b) + decreases a +{ + if |a| == 0 { + assert a + b == b; + } else { + SSCsConcat(a[1..], b); + assert (a + b)[1..] == a[1..] + b; + } +} + +lemma FlattenOrLeavesNonOr(e: TExpr) + ensures forall k :: 0 <= k < |flattenOr(e)| ==> !(flattenOr(e)[k].binop? && flattenOr(e)[k].op == "||") + decreases e +{ + if e.binop? && e.op == "||" { + FlattenOrLeavesNonOr(e.left); + FlattenOrLeavesNonOr(e.right); + } +} + +lemma DETsSnoc(es: seq, x: TExpr, decls: seq) + ensures DETs(es + [x], decls) == DETs(es, decls) + (if noneDetector(x, CondCtx(decls, 0)).Some? then 1 else 0) + decreases es +{ + if |es| == 0 { + assert es + [x] == [x]; + } else { + DETsSnoc(es[1..], x, decls); + assert (es + [x])[1..] == es[1..] + [x]; + } +} + +lemma SEsSnoc(es: seq, x: TExpr) + ensures SEs(es + [x]) == SEs(es) + 1 + SE(x) + decreases es +{ + if |es| == 0 { + assert es + [x] == [x]; + } else { + SEsSnoc(es[1..], x); + assert (es + [x])[1..] == es[1..] + [x]; + } +} + +lemma AWEsSnoc(es: seq, x: TExpr, decls: seq) + ensures AWEs(es + [x], decls) == AWEs(es, decls) + AWE(x, decls) + decreases es +{ + if |es| == 0 { + assert es + [x] == [x]; + } else { + AWEsSnoc(es[1..], x, decls); + assert (es + [x])[1..] == es[1..] + [x]; + } +} + +// Exact firability of the method-lowered rules, restated over the axiom +// extractors (the function-lowered rules test their own `.Some?` directly). +function FImplOptional(e: TExpr): bool +{ + e.binop? && e.op == "==>" && + (leadingPresent(e.left).Some? || + (presentFact(e.left).Some? && !presentFact(e.left).value.negated)) +} + +function FImplArrayIsArray(e: TExpr, decls: seq): bool +{ + e.binop? && e.op == "==>" && + (isArrayFact(e.left, CondCtx(decls, 0)).Some? || + (e.left.unop? && e.left.op == "!" && isArrayFact(e.left.expr, CondCtx(decls, 0)).Some?)) +} + +function FCondArrayIsArray(e: TExpr, decls: seq): bool +{ + e.conditional? && + (isArrayFact(e.cond, CondCtx(decls, 0)).Some? || typeofStringFact(e.cond, CondCtx(decls, 0)).Some? || + (e.cond.unop? && e.cond.op == "!" && isArrayFact(e.cond.expr, CondCtx(decls, 0)).Some?)) +} + +function FCondAndArrayIsArray(e: TExpr, decls: seq): bool +{ + e.conditional? && leadingIsArray(e.cond, CondCtx(decls, 0)).Some? +} + +function FCondAndOptional(e: TExpr): bool +{ + e.conditional? && leadingPresent(e.cond).Some? && + !containsMethodCall(leadingPresent(e.cond).value.restCond) +} + +function FIfAndOptional(s: TStmt): bool +{ + s.if_? && s.else_ == [] && leadingPresent(s.cond).Some? +} + +function FIfAndArrayIsArray(s: TStmt, decls: seq): bool +{ + s.if_? && leadingIsArray(s.cond, CondCtx(decls, 0)).Some? +} + +function FExprStmtAndOptional(s: TStmt): bool +{ + s.expr? && s.expr.binop? && s.expr.op == "&&" && leadingPresent(s.expr).Some? +} + +function FLetCond(s: TStmt): bool +{ + s.let? && !s.mutable && s.init.conditional? && leadingPresent(s.init.cond).Some? +} + +// The cond-reading ternary drivers, merged into one charge rather than a +// sum per driver: several shapes can hold at once (a `!Array.isArray(p)` +// cond is both a presence-shaped unop and an isArray check), and a summed +// charge would exceed what the fired rule's re-walk can pay back. +// `ruleConditionalInMap`/`ruleConditionalOptionalTruthy` stay out with a +// flat charge: they copy their branches rather than re-walking them, so +// they need no room for a residual, only enough to be nonzero. +function FCond(e: TExpr, decls: seq): bool +{ + e.conditional? && + (ruleConditionalOptionalSimple(e, CondCtx(decls, 0)).Some? || + FCondAndOptional(e) || FCondAndArrayIsArray(e, decls) || FCondArrayIsArray(e, decls)) +} + +function FCondFlat(e: TExpr, decls: seq): bool +{ + e.conditional? && + (ruleConditionalInMap(e, CondCtx(decls, 0)).Some? || + ruleConditionalOptionalTruthy(e, CondCtx(decls, 0)).Some?) +} + +// Root charges: nonzero exactly on firable nodes, generous enough to cover +// what the firing rule constructs — including a copy of whatever the rule +// duplicates (the falsy gate's none side, an early return's terminating +// branch). +function WE(e: TExpr, decls: seq): nat + decreases e, 1 +{ + (if ruleNullish(e, CondCtx(decls, 0)).Some? || ruleNullishIndex(e, CondCtx(decls, 0)).Some? then 2 else 0) + + (if ruleOptChainIndex(e, CondCtx(decls, 0)).Some? || ruleOptChain(e, CondCtx(decls, 0)).Some? then 2 else 0) + + (if FImplOptional(e) || FImplArrayIsArray(e, decls) then 2 + PC(e.left) + AC(e.left, decls) else 0) + + (if FCondFlat(e, decls) then 2 else 0) + + (if FCond(e, decls) then 2 + 2*PC(e.cond) + 2*AC(e.cond, decls) + AWE(e.then_, decls) + AWE(e.else_, decls) else 0) +} + +function WS(s: TStmt, decls: seq): nat + decreases s, 1 +{ + (if s.if_? then + (if FIfAndOptional(s) || FIfAndArrayIsArray(s, decls) then 2 + PC(s.cond) + AC(s.cond, decls) + AWSs(s.then_, decls) + AWSs(s.else_, decls) else 0) + + (if ruleIfOptionalSimple(s, CondCtx(decls, 0)).Some? then 2 + PC(s.cond) + AWSs(s.then_, decls) + AWSs(s.else_, decls) else 0) + else 0) + + (if FExprStmtAndOptional(s) then 2 + PC(s.expr) else 0) + + (if ruleOptionalIndexBinding(s, CondCtx(decls, 0)).Some? then 1 else 0) + + (if FLetCond(s) then 8 + PC(s.init.cond) + AC(s.init.cond, decls) + AWE(s.init.then_, decls) + AWE(s.init.else_, decls) else 0) +} + +// Exact or-chain firability: the rule fires iff the shape matches, at +// least one leaf is a None-detector, and every detector has a distinct +// binder key (the seen-set bail). +function FOrChain(head: TStmt, tail: seq, decls: seq): bool +{ + |tail| >= 1 && head.if_? && |head.then_| > 0 && head.else_ == [] && isTerminating(head.then_) && + head.cond.binop? && head.cond.op == "||" && + var leaves := flattenOr(head.cond); + |leaves| >= 2 && + (exists i :: 0 <= i < |leaves| && noneDetector(leaves[i], CondCtx(decls, 0)).Some?) +} + +// The bound-optional early return leaves the *other* optional chain in the +// guard it builds, so the inner `if` can arm the same rule again — and a +// constant charge just cancels against that inner charge. Scaling by the +// guard's size does not cancel, because the rewrite trades the chain node +// for its application and so strictly shrinks the guard. +function SiteCharge(cond: TExpr, branchWeight: nat): nat +{ + SE(cond) * (12 + 4 * branchWeight) +} + +// Position-aware head charge: the list rules fire on (head, tail), so their +// charge lives per suffix. Duplication-safe: rule outputs copy whole +// sublists, so a copied statement keeps its intra-copy tail and with it +// its (zero, or it would have fired) head charge. +function HC(head: TStmt, tail: seq, decls: seq): nat + decreases head, tail +{ + (if ruleEarlyReturnConsume(head, tail).Some? then + 2 + PC(head.cond) + + (if presentFact(head.cond).Some? && presentFact(head.cond).value.negated + then AWSs(head.then_, decls) else AWSs(head.else_, decls)) + else 0) + + (if ruleEarlyReturnOptChainCompare(head, tail, CondCtx(decls, 0)).Some? + then SiteCharge(head.cond, AWSs(head.then_, decls)) else 0) + + // Scaled the same way, and by the detector count too: the rewrite drops + // at least one detector, and each dropped disjunct takes at least two + // units of guard size with it — enough to dominate the charges the + // residual guard can carry in turn (its own root charge, and the + // early-return rules it can arm when it is a single leaf). + (if FOrChain(head, tail, decls) + then (DET(head.cond, decls) + SE(head.cond)) * (16 + 4 * AWSs(head.then_, decls)) else 0) +} + +function AWE(e: TExpr, decls: seq): nat + decreases e, 2 +{ + WE(e, decls) + match e { + case var_(_, _) => 0 + case num(_, _) => 0 + case bigint(_, _) => 0 + case str(_, _) => 0 + case bool_(_, _) => 0 + case havoc(_) => 0 + case binop(_, l, r, _) => AWE(l, decls) + AWE(r, decls) + case unop(_, x, _) => AWE(x, decls) + case call(f, args, _, _, _) => AWE(f, decls) + AWEs(args, decls) + case index(o, i, _) => AWE(o, decls) + AWE(i, decls) + case field(o, _, _, _, _) => AWE(o, decls) + case record(sp, fs, _) => AWOE(sp, decls) + AWRFs(fs, decls) + case arrayLiteral(es, _) => AWEs(es, decls) + case lambda(_, b, _) => AWSs(b, decls) + case conditional(c, t, e2, _) => AWE(c, decls) + AWE(t, decls) + AWE(e2, decls) + case optChain(o, ch, _) => AWE(o, decls) + AWSteps(ch, decls) + case nullish(l, r, _) => AWE(l, decls) + AWE(r, decls) + case someMatch(_, _, _, sb, nb, _) => AWE(sb, decls) + AWE(nb, decls) + case tagMatch(sc, _, cs, ft, _) => AWE(sc, decls) + AWECs(cs, decls) + AWOE(ft, decls) + case forall_(_, _, b, _) => AWE(b, decls) + case exists_(_, _, b, _) => AWE(b, decls) + } +} + +function AWOE(o: Option, decls: seq): nat + decreases o +{ + match o { case Some(x) => AWE(x, decls) case None => 0 } +} + +function AWEs(es: seq, decls: seq): nat + decreases es +{ + if |es| == 0 then 0 else AWE(es[0], decls) + AWEs(es[1..], decls) +} + +function AWStep(c: TChainStep, decls: seq): nat + decreases c +{ + match c { + case field(_, _) => 0 + case call(args, _, _, _) => AWEs(args, decls) + case index(i, _) => AWE(i, decls) + } +} + +function AWSteps(cs: seq, decls: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else AWStep(cs[0], decls) + AWSteps(cs[1..], decls) +} + +function AWRF(f: TRecordField, decls: seq): nat + decreases f +{ + AWE(f.value, decls) +} + +function AWRFs(fs: seq, decls: seq): nat + decreases fs +{ + if |fs| == 0 then 0 else AWRF(fs[0], decls) + AWRFs(fs[1..], decls) +} + +function AWEC(c: TExprCase, decls: seq): nat + decreases c +{ + AWE(c.body, decls) +} + +function AWECs(cs: seq, decls: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else AWEC(cs[0], decls) + AWECs(cs[1..], decls) +} + +function AWSC(c: TStmtCase, decls: seq): nat + decreases c +{ + AWSs(c.body, decls) +} + +function AWSCs(cs: seq, decls: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else AWSC(cs[0], decls) + AWSCs(cs[1..], decls) +} + +function AWWC(c: TSwitchCase, decls: seq): nat + decreases c +{ + AWSs(c.body, decls) +} + +function AWWCs(cs: seq, decls: seq): nat + decreases cs +{ + if |cs| == 0 then 0 else AWWC(cs[0], decls) + AWWCs(cs[1..], decls) +} + +function AWS(s: TStmt, decls: seq): nat + decreases s, 2 +{ + WS(s, decls) + match s { + case let(_, _, _, init) => AWE(init, decls) + case assign(_, v) => AWE(v, decls) + case return_(v) => AWE(v, decls) + case break_ => 0 + case continue_ => 0 + case throw => 0 + case expr(x) => AWE(x, decls) + case if_(c, t, e2) => AWE(c, decls) + AWSs(t, decls) + AWSs(e2, decls) + case while_(c, invs, dec, dw, b) => AWE(c, decls) + AWEs(invs, decls) + AWOE(dec, decls) + AWOE(dw, decls) + AWSs(b, decls) + case switch(x, _, cs, db) => AWE(x, decls) + AWWCs(cs, decls) + AWSs(db, decls) + case forof(_, _, it, invs, dw, b) => AWE(it, decls) + AWEs(invs, decls) + AWOE(dw, decls) + AWSs(b, decls) + case ghostLet(_, _, init) => AWE(init, decls) + case ghostAssign(_, v) => AWE(v, decls) + case assert_(x, _) => AWE(x, decls) + case someMatch(_, _, _, sb, nb) => AWSs(sb, decls) + AWSs(nb, decls) + case tagMatch(sc, _, cs, ft) => AWE(sc, decls) + AWSCs(cs, decls) + AWSs(ft, decls) + } +} + +function AWSs(ss: seq, decls: seq): nat + decreases ss, 2 +{ + if |ss| == 0 then 0 else HC(ss[0], ss[1..], decls) + AWS(ss[0], decls) + AWSs(ss[1..], decls) +} + +// Head-unfold bridges for the element+field double hop (rank axioms +// cover single structural steps only — §8.7). +lemma AWECsHead(cases: seq, decls: seq) + requires |cases| > 0 + ensures AWECs(cases, decls) == AWE(cases[0].body, decls) + AWECs(cases[1..], decls) + ensures SECs(cases) == 1 + SE(cases[0].body) + SECs(cases[1..]) +{} + +lemma AWRFsHead(fields: seq, decls: seq) + requires |fields| > 0 + ensures AWRFs(fields, decls) == AWE(fields[0].value, decls) + AWRFs(fields[1..], decls) + ensures SRFs(fields) == 1 + SE(fields[0].value) + SRFs(fields[1..]) +{} + +lemma AWSCsHead(cases: seq, decls: seq) + requires |cases| > 0 + ensures AWSCs(cases, decls) == AWSs(cases[0].body, decls) + AWSCs(cases[1..], decls) + ensures SSCs(cases) == 1 + SSs(cases[0].body) + SSCs(cases[1..]) +{} + +lemma AWWCsHead(cases: seq, decls: seq) + requires |cases| > 0 + ensures AWWCs(cases, decls) == AWSs(cases[0].body, decls) + AWWCs(cases[1..], decls) + ensures SWCs(cases) == 1 + SSs(cases[0].body) + SWCs(cases[1..]) +{} + +lemma AWStepsHead(steps: seq, decls: seq) + requires |steps| > 0 + ensures AWSteps(steps, decls) == AWStep(steps[0], decls) + AWSteps(steps[1..], decls) + ensures SSteps(steps) == 1 + SStep(steps[0]) + SSteps(steps[1..]) +{} + +// Trusted measure facts about the imported extractors and builders — +// stated as lemmas so the axiom functions stay out of the measure +// family's call graph; invoked at the proof sites that need them. +lemma {:axiom} leadingPresentShrinks(cond: TExpr) + requires leadingPresent(cond).Some? + ensures SE(leadingPresent(cond).value.restCond) < SE(cond) + ensures PC(leadingPresent(cond).value.restCond) < PC(cond) + ensures forall decls: seq :: AC(leadingPresent(cond).value.restCond, decls) <= AC(cond, decls) + ensures forall decls: seq :: AWE(leadingPresent(cond).value.restCond, decls) <= AWE(cond, decls) + +lemma {:axiom} leadingIsArrayShrinks(cond: TExpr, ctx: CondCtx) + requires leadingIsArray(cond, ctx).Some? + ensures SE(leadingIsArray(cond, ctx).value.restCond) < SE(cond) + ensures AC(leadingIsArray(cond, ctx).value.restCond, ctx.decls) < AC(cond, ctx.decls) + ensures PC(leadingIsArray(cond, ctx).value.restCond) <= PC(cond) + ensures forall decls: seq :: AWE(leadingIsArray(cond, ctx).value.restCond, decls) <= AWE(cond, decls) + +lemma {:axiom} presentFactScrutineeWeightless(cond: TExpr) + requires presentFact(cond).Some? + ensures forall decls: seq :: AWE(presentFact(cond).value.scrutinee, decls) == 0 + +lemma {:axiom} leadingPresentCheckWeightless(cond: TExpr) + requires leadingPresent(cond).Some? + ensures forall decls: seq :: AWE(leadingPresent(cond).value.check.scrutinee, decls) == 0 + +lemma {:axiom} isArrayFactScrutineeWeightless(call: TExpr, ctx: CondCtx) + requires isArrayFact(call, ctx).Some? + ensures forall decls: seq :: AWE(isArrayFact(call, ctx).value.scrutinee, decls) == 0 + +lemma {:axiom} typeofStringFactScrutineeWeightless(e: TExpr, ctx: CondCtx) + requires typeofStringFact(e, ctx).Some? + ensures forall decls: seq :: AWE(typeofStringFact(e, ctx).value.scrutinee, decls) == 0 + +lemma {:axiom} leadingIsArrayCheckWeightless(cond: TExpr, ctx: CondCtx) + requires leadingIsArray(cond, ctx).Some? + ensures forall decls: seq :: AWE(leadingIsArray(cond, ctx).value.check.scrutinee, decls) == 0 + +lemma {:axiom} noneDetectorScrutineeWeightless(leaf: TExpr, ctx: CondCtx) + requires noneDetector(leaf, ctx).Some? + ensures forall decls: seq :: AWE(noneDetector(leaf, ctx).value.scrutinee, decls) == 0 + +lemma {:axiom} flattenOrSize(e: TExpr) + ensures |flattenOr(e)| >= 1 + ensures SEs(flattenOr(e)) == SE(e) + 1 + ensures forall decls: seq :: AWEs(flattenOr(e), decls) <= AWE(e, decls) + ensures forall decls: seq :: DETs(flattenOr(e), decls) == DET(e, decls) + +lemma {:axiom} noneDetectorResidualBounds(leaf: TExpr, ctx: CondCtx) + requires noneDetector(leaf, ctx).Some? && noneDetector(leaf, ctx).value.residual.Some? + ensures SE(noneDetector(leaf, ctx).value.residual.value) <= SE(leaf) + ensures forall decls: seq :: AWE(noneDetector(leaf, ctx).value.residual.value, decls) <= AWE(leaf, decls) + ensures noneDetector(noneDetector(leaf, ctx).value.residual.value, ctx).None? + ensures !(noneDetector(leaf, ctx).value.residual.value.binop? && noneDetector(leaf, ctx).value.residual.value.op == "||") + +lemma {:axiom} applyChainBounds(body: TExpr, chain: seq) + ensures containsMethodCall(body) ==> containsMethodCall(applyChain(body, chain)) + ensures SE(applyChain(body, chain)) <= SE(body) + SSteps(chain) + ensures forall decls: seq :: AWE(applyChain(body, chain), decls) <= AWE(body, decls) + AWSteps(chain, decls) + +lemma {:axiom} restoreDiscriminantFlagBounds(unwrapped: TExpr, decls: TypeDecls) + ensures SE(restoreDiscriminantFlag(unwrapped, decls)) == SE(unwrapped) + ensures forall d: seq :: AWE(restoreDiscriminantFlag(unwrapped, decls), d) <= AWE(unwrapped, d) + 2 + +// Detection reads only ctx.decls: trusted invariance of the imported +// detectors under the threaded binder counter. +lemma {:axiom} isArrayFactShape(call: TExpr, ctx: CondCtx) + ensures isArrayFact(call, ctx).Some? ==> call.call? + +lemma {:axiom} typeofStringFactShape(e: TExpr, ctx: CondCtx) + ensures typeofStringFact(e, ctx).Some? ==> e.binop? && e.op == "===" + +lemma {:axiom} isArrayFactDeclsOnly(e: TExpr, c1: CondCtx, c2: CondCtx) + requires c1.decls == c2.decls + ensures isArrayFact(e, c1) == isArrayFact(e, c2) + +lemma {:axiom} typeofStringFactDeclsOnly(e: TExpr, c1: CondCtx, c2: CondCtx) + requires c1.decls == c2.decls + ensures typeofStringFact(e, c1) == typeofStringFact(e, c2) + +lemma {:axiom} noneDetectorDeclsOnly(e: TExpr, c1: CondCtx, c2: CondCtx) + requires c1.decls == c2.decls + ensures noneDetector(e, c1) == noneDetector(e, c2) + +lemma {:axiom} variantFactDeclsOnly(e: TExpr, c1: CondCtx, c2: CondCtx) + requires c1.decls == c2.decls + ensures variantFact(e, c1) == variantFact(e, c2) + +lemma {:axiom} leadingIsArrayDeclsOnly(e: TExpr, c1: CondCtx, c2: CondCtx) + requires c1.decls == c2.decls + ensures leadingIsArray(e, c1) == leadingIsArray(e, c2) + +// Firability of the two `&&`-chain extractors, as defining equations (the +// shape of condition-facts' shallowest-first, left-biased search). Needed +// to push firability through the walk: the spine is rebuilt conjunct by +// conjunct, so a leading fact in the result comes from one in the input. +function PositivePresent(e: TExpr): bool +{ + presentFact(e).Some? && !presentFact(e).value.negated +} + +lemma {:axiom} leadingPresentUnfold(cond: TExpr) + ensures leadingPresent(cond).Some? <==> + (cond.binop? && cond.op == "&&" && + (PositivePresent(cond.left) || PositivePresent(cond.right) || + (cond.left.binop? && cond.left.op == "&&" && leadingPresent(cond.left).Some?) || + (cond.right.binop? && cond.right.op == "&&" && leadingPresent(cond.right).Some?))) + +lemma {:axiom} leadingIsArrayUnfold(cond: TExpr, ctx: CondCtx) + ensures leadingIsArray(cond, ctx).Some? <==> + (cond.binop? && cond.op == "&&" && + (isArrayFact(cond.left, ctx).Some? || isArrayFact(cond.right, ctx).Some? || + (cond.left.binop? && cond.left.op == "&&" && leadingIsArray(cond.left, ctx).Some?) || + (cond.right.binop? && cond.right.op == "&&" && leadingIsArray(cond.right, ctx).Some?))) + +// The extracted conjunct is a pure access-path check (`presentFact`) or an +// `Array.isArray` call — neither carries a method call — so removing it +// from the `&&` tree leaves the residual's call content unchanged. +lemma {:axiom} leadingPresentMethodCalls(cond: TExpr) + requires leadingPresent(cond).Some? + ensures containsMethodCall(leadingPresent(cond).value.restCond) == containsMethodCall(cond) + +lemma {:axiom} leadingIsArrayMethodCalls(cond: TExpr, ctx: CondCtx) + requires leadingIsArray(cond, ctx).Some? + ensures containsMethodCall(leadingIsArray(cond, ctx).value.restCond) == containsMethodCall(cond) + +// What the walk preserves. `Inert(e2) ==> e2 == e` is the engine of the +// whole family: the detectors match only inert shapes, so anything a +// detector sees in the walked term it also saw in the input — walking can +// remove a redex but never arm one. The rest follows from it: the counts +// and the firability guards the charges read are all non-increasing, no +// optional-headed `optChain` survives (`ruleOptChain` consumes exactly +// those), and method calls only ever accumulate (the rules re-embed every +// call they move and drop only call-free path checks). +function Tame(e2: TExpr, e: TExpr, decls: seq): bool +{ + (Inert(e2) ==> e2 == e) && + (e2.binop? ==> e.binop? && e.op == e2.op) && + PC(e2) <= PC(e) && + AC(e2, decls) <= AC(e, decls) && + DET(e2, decls) <= DET(e, decls) && + (leadingPresent(e2).Some? ==> leadingPresent(e).Some?) && + (leadingIsArray(e2, CondCtx(decls, 0)).Some? ==> leadingIsArray(e, CondCtx(decls, 0)).Some?) && + (containsMethodCall(e) ==> containsMethodCall(e2)) +} + +// Every rule replaces the node it fires on with a match or a guarded +// ternary — shapes no detector reads — so a fired rule's output is a leaf +// for every count and every firability guard. +lemma RuleOutputNeutral(x: TExpr, e: TExpr, decls: seq) + requires x.someMatch? || x.tagMatch? || x.conditional? + requires containsMethodCall(e) ==> containsMethodCall(x) + ensures Tame(x, e, decls) + ensures !(x.optChain? && x.obj.ty.optional?) +{ + isArrayFactShape(x, CondCtx(decls, 0)); + typeofStringFactShape(x, CondCtx(decls, 0)); +} + +// Rebuilds under a head no detector reads: PC/AC/DET are zero and both +// `&&`/`||` extractors need a binop head, so only inertness and call +// content have to be carried in. +lemma TameOpaque(out: TExpr, e: TExpr, decls: seq) + requires !out.binop? && !out.unop? && !out.var_? && !out.field? && !out.call? + requires Inert(out) ==> out == e + requires containsMethodCall(e) ==> containsMethodCall(out) + ensures Tame(out, e, decls) +{ + isArrayFactShape(out, CondCtx(decls, 0)); + typeofStringFactShape(out, CondCtx(decls, 0)); +} + +// `&&`/`||` spines: the extractors' defining equations push firability +// through conjunct by conjunct, and an atom a detector matches is inert, +// hence unchanged. +lemma TameBinop(e: TExpr, l2: TExpr, r2: TExpr, decls: seq) + requires e.binop? + requires Tame(l2, e.left, decls) && Tame(r2, e.right, decls) + requires !(l2.optChain? && l2.obj.ty.optional?) && !(r2.optChain? && r2.obj.ty.optional?) + ensures Tame(e.(left := l2, right := r2), e, decls) +{ + var out := e.(left := l2, right := r2); + leadingPresentUnfold(out); + leadingPresentUnfold(e); + leadingIsArrayUnfold(out, CondCtx(decls, 0)); + leadingIsArrayUnfold(e, CondCtx(decls, 0)); + isArrayFactShape(out, CondCtx(decls, 0)); + typeofStringFactShape(out, CondCtx(decls, 0)); +} + +lemma TameUnop(e: TExpr, x2: TExpr, decls: seq) + requires e.unop? + requires Tame(x2, e.expr, decls) + ensures Tame(e.(expr := x2), e, decls) +{ + var out := e.(expr := x2); + isArrayFactShape(out, CondCtx(decls, 0)); + typeofStringFactShape(out, CondCtx(decls, 0)); +} + +lemma TameField(e: TExpr, o2: TExpr, decls: seq) + requires e.field? + requires Tame(o2, e.obj, decls) + ensures Tame(e.(obj := o2), e, decls) +{ + var out := e.(obj := o2); + isArrayFactShape(out, CondCtx(decls, 0)); + typeofStringFactShape(out, CondCtx(decls, 0)); +} + +lemma InertEsIdentity(xs2: seq, xs: seq, decls: seq) + requires |xs2| == |xs| + requires forall i :: 0 <= i < |xs| ==> Tame(xs2[i], xs[i], decls) + requires InertEs(xs2) + ensures xs2 == xs + decreases xs2 +{ + if |xs2| > 0 { + assert forall i :: 0 <= i < |xs| - 1 ==> xs2[1..][i] == xs2[i+1] && xs[1..][i] == xs[i+1]; + InertEsIdentity(xs2[1..], xs[1..], decls); + } +} + +lemma CMCExists(xs2: seq, xs: seq) + requires |xs2| == |xs| + requires forall i :: 0 <= i < |xs| ==> (containsMethodCall(xs[i]) ==> containsMethodCall(xs2[i])) + ensures (exists x :: x in xs && containsMethodCall(x)) ==> (exists y :: y in xs2 && containsMethodCall(y)) +{ + if exists x :: x in xs && containsMethodCall(x) { + var x :| x in xs && containsMethodCall(x); + var i :| 0 <= i < |xs| && xs[i] == x; + assert xs2[i] in xs2; + } +} + +lemma CMCExistsRF(xs2: seq, xs: seq) + requires |xs2| == |xs| + requires forall i :: 0 <= i < |xs| ==> (containsMethodCall(xs[i].value) ==> containsMethodCall(xs2[i].value)) + ensures (exists f :: f in xs && containsMethodCall(f.value)) ==> (exists g :: g in xs2 && containsMethodCall(g.value)) +{ + if exists f :: f in xs && containsMethodCall(f.value) { + var f :| f in xs && containsMethodCall(f.value); + var i :| 0 <= i < |xs| && xs[i] == f; + assert xs2[i] in xs2; + } +} + +lemma CMCExistsEC(xs2: seq, xs: seq) + requires |xs2| == |xs| + requires forall i :: 0 <= i < |xs| ==> (containsMethodCall(xs[i].body) ==> containsMethodCall(xs2[i].body)) + ensures (exists c :: c in xs && containsMethodCall(c.body)) ==> (exists d :: d in xs2 && containsMethodCall(d.body)) +{ + if exists c :: c in xs && containsMethodCall(c.body) { + var c :| c in xs && containsMethodCall(c.body); + var i :| 0 <= i < |xs| && xs[i] == c; + assert xs2[i] in xs2; + } +} + +lemma TameCall(e: TExpr, f2: TExpr, args2: seq, decls: seq) + requires e.call? + requires Tame(f2, e.fn, decls) + requires |args2| == |e.args| + requires forall i :: 0 <= i < |e.args| ==> Tame(args2[i], e.args[i], decls) + ensures Tame(e.(fn := f2, args := args2), e, decls) +{ + var out := e.(fn := f2, args := args2); + if Inert(out) { InertEsIdentity(args2, e.args, decls); } + CMCExists(args2, e.args); + typeofStringFactShape(out, CondCtx(decls, 0)); +} + +lemma TameIndex(e: TExpr, o2: TExpr, i2: TExpr, decls: seq) + requires e.index? + requires Tame(o2, e.obj, decls) && Tame(i2, e.idx, decls) + ensures Tame(e.(obj := o2, idx := i2), e, decls) +{ + var out := e.(obj := o2, idx := i2); + isArrayFactShape(out, CondCtx(decls, 0)); + typeofStringFactShape(out, CondCtx(decls, 0)); +} + +// Ternary firability is walk-stable: every driver in the charged group +// reads only the cond, and each reads it through an inert shape or through +// one of the two `&&` extractors. +lemma CondChargeMonotone(e: TExpr, c2: TExpr, t2: TExpr, e22: TExpr, decls: seq) + requires e.conditional? + requires Tame(c2, e.cond, decls) + ensures FCond(e.(cond := c2, then_ := t2, else_ := e22), decls) ==> FCond(e, decls) + ensures ruleConditionalOptionalTruthy(e.(cond := c2, then_ := t2, else_ := e22), CondCtx(decls, 0)).Some? + ==> ruleConditionalOptionalTruthy(e, CondCtx(decls, 0)).Some? +{ + if leadingPresent(c2).Some? { + leadingPresentMethodCalls(c2); + leadingPresentMethodCalls(e.cond); + } +} + +// REMAINING GAP. The in-map driver is the one whose guard reads the +// *branches*: `m[k]` must be structurally equal to the tested receiver and +// key (both inert, hence walk-stable) and the else branch must not be +// optional- or void-typed — and that last test is not stable, because the +// walk can retype a branch (this very rule turns `Option` into `V`). +// Closing it needs the walker's type-preservation ensures, which in turn +// needs every rule charged so that `AWE == 0` means "unchanged"; the three +// uncharged statement-level rules (`ruleOptionalIndexBinding` and the two +// discriminant list rules) are what block that today. +lemma InMapChargeMonotone(e: TExpr, c2: TExpr, t2: TExpr, e22: TExpr, decls: seq) + requires e.conditional? + requires Tame(c2, e.cond, decls) && Tame(t2, e.then_, decls) && Tame(e22, e.else_, decls) + requires e22.ty == e.else_.ty + ensures FCondFlat(e.(cond := c2, then_ := t2, else_ := e22), decls) ==> FCondFlat(e, decls) +{ + CondChargeMonotone(e, c2, t2, e22, decls); + var out := e.(cond := c2, then_ := t2, else_ := e22); + if ruleConditionalInMap(out, CondCtx(decls, 0)).Some? { + // `exprEqual` pins the tested receiver and key, and the `m[k]` access, + // to inert shapes — so the walk returned all three unchanged. + assert Inert(c2.left) && Inert(c2.right) && Inert(t2.obj) && Inert(t2.idx); + assert c2 == TExpr.binop(c2.op, c2.left, c2.right, c2.ty); + assert t2 == TExpr.index(t2.obj, t2.idx, t2.ty); + assert Inert(c2) && Inert(t2); + assert c2 == e.cond && t2 == e.then_; + } +} + +// The one uncharged rule pays in size instead: it drops the `k in m` test +// and replaces the ternary with a match on `m[k]`. +lemma InMapShrinks(e: TExpr, ctx: CondCtx) + requires ruleConditionalInMap(e, ctx).Some? + ensures SE(ruleConditionalInMap(e, ctx).value.expr) < SE(e) +{ + assert e == TExpr.conditional(e.cond, e.then_, e.else_, e.ty); + assert e.cond == TExpr.binop(e.cond.op, e.cond.left, e.cond.right, e.cond.ty); + assert SE(e.cond) >= 3; +} + +// `0 <= i && i < arr.length`: a conjunction of comparisons, so no driver +// reads anything in it and it carries no weight of its own. +lemma {:axiom} arrayBoundsCondFacts(arr: TExpr, idx: TExpr) + ensures PC(arrayBoundsCond(arr, idx)) == 0 + ensures leadingPresent(arrayBoundsCond(arr, idx)).None? + ensures forall decls: seq :: AC(arrayBoundsCond(arr, idx), decls) == 0 + ensures forall ctx: CondCtx :: leadingIsArray(arrayBoundsCond(arr, idx), ctx).None? + ensures forall decls: seq :: + AWE(arr, decls) == 0 && AWE(idx, decls) == 0 ==> AWE(arrayBoundsCond(arr, idx), decls) == 0 + +// ── Hand-authored (walk stability): rule outputs are weightless ── +// Every rule builds its result from already-walked pieces, so once a node's +// children are weightless the fired rule's output is too — which is what +// makes duplicating an arm (the falsy gate's none side) free. + +// A bounds-guarded ternary: `arrayBoundsCond` is a conjunction of +// comparisons, so no ternary driver reads it. +lemma BoundsGuardWeightless(arr: TExpr, idx: TExpr, t: TExpr, e2: TExpr, ty: Ty, decls: seq) + requires AWE(arr, decls) == 0 && AWE(idx, decls) == 0 + requires AWE(t, decls) == 0 && AWE(e2, decls) == 0 + ensures AWE(TExpr.conditional(arrayBoundsCond(arr, idx), t, e2, ty), decls) == 0 +{ + var c := arrayBoundsCond(arr, idx); + var g := TExpr.conditional(c, t, e2, ty); + arrayBoundsCondFacts(arr, idx); + isArrayFactShape(c, CondCtx(decls, 0)); + typeofStringFactShape(c, CondCtx(decls, 0)); + leadingIsArrayDeclsOnly(c, CondCtx(decls, 0), CondCtx(decls, 0)); + assert !FCond(g, decls); +} + +// The falsy gate the presence materializers insert tests the bound value — +// a var of the (never optional, since falsy-capable) inner type. +lemma GateExprWeightless(f: PresentFact, some: TExpr, none: TExpr, ty: Ty, decls: seq) + requires AWE(f.scrutinee, decls) == 0 + requires AWE(some, decls) == 0 && AWE(none, decls) == 0 + ensures AWE(presentMatchExpr(f, some, none, ty), decls) == 0 +{ + var gc := TExpr.var_(f.binder, f.innerTy); + var gate := TExpr.conditional(gc, some, none, ty); + isArrayFactShape(gc, CondCtx(decls, 0)); + typeofStringFactShape(gc, CondCtx(decls, 0)); + assert AWE(gc, decls) == WE(gc, decls); + if f.truthiness && FalsyCapable(f.innerTy) { + assert !FCond(gate, decls); + assert AWE(gate, decls) == WE(gate, decls) + AWE(gc, decls) + AWE(some, decls) + AWE(none, decls); + } + var body := if f.truthiness && FalsyCapable(f.innerTy) then gate else some; + var out := TExpr.someMatch(f.scrutinee, f.binder, f.innerTy, body, none, ty); + assert AWE(out, decls) == WE(out, decls) + AWE(f.scrutinee, decls) + AWE(body, decls) + AWE(none, decls); +} + +lemma GateStmtsWeightless(f: PresentFact, some: seq, none: seq, decls: seq) + requires AWE(f.scrutinee, decls) == 0 + requires AWSs(some, decls) == 0 && AWSs(none, decls) == 0 + ensures AWS(presentMatchStmts(f, some, none), decls) == 0 +{ + var gate := TStmt.if_(TExpr.var_(f.binder, f.innerTy), some, none); + leadingIsArrayDeclsOnly(gate.cond, CondCtx(decls, 0), CondCtx(decls, 0)); + assert AWE(gate.cond, decls) == WE(gate.cond, decls); + if f.truthiness && FalsyCapable(f.innerTy) { + assert WS(gate, decls) == 0; + assert AWS(gate, decls) == WS(gate, decls) + AWE(gate.cond, decls) + AWSs(some, decls) + AWSs(none, decls); + assert HC(gate, [], decls) == 0; + assert AWSs([gate], decls) == HC(gate, [], decls) + AWS(gate, decls) + AWSs([], decls); + } + var body := if f.truthiness && FalsyCapable(f.innerTy) then [gate] else some; + var out := TStmt.someMatch(f.scrutinee, f.binder, f.innerTy, body, none); + assert AWS(out, decls) == WS(out, decls) + AWE(f.scrutinee, decls) + AWSs(body, decls) + AWSs(none, decls); +} + +lemma NullishRulesWeightless(e: TExpr, ctx: CondCtx) + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures ruleNullish(e, ctx).Some? ==> AWE(ruleNullish(e, ctx).value.expr, ctx.decls) == 0 + ensures ruleNullishIndex(e, ctx).Some? ==> AWE(ruleNullishIndex(e, ctx).value.expr, ctx.decls) == 0 +{ + if e.nullish? && (ruleNullish(e, ctx).Some? || ruleNullishIndex(e, ctx).Some?) { + assert AWE(e, ctx.decls) == WE(e, ctx.decls) + AWE(e.left, ctx.decls) + AWE(e.right, ctx.decls); + assert WE(e, ctx.decls) >= 2; + assert AWE(e.left, ctx.decls) == 0 && AWE(e.right, ctx.decls) == 0; + if ruleNullish(e, ctx).Some? { + var b := freshOcBinder(ctx); + var bv := TExpr.var_(b.name, e.left.ty.inner); + var out := TExpr.someMatch(e.left, b.name, e.left.ty.inner, bv, e.right, e.ty); + assert AWE(bv, ctx.decls) == WE(bv, ctx.decls); + assert AWE(out, ctx.decls) + == WE(out, ctx.decls) + AWE(e.left, ctx.decls) + AWE(bv, ctx.decls) + AWE(e.right, ctx.decls); + } + if ruleNullishIndex(e, ctx).Some? { + assert AWE(e.left, ctx.decls) + == WE(e.left, ctx.decls) + AWE(e.left.obj, ctx.decls) + AWE(e.left.idx, ctx.decls); + BoundsGuardWeightless(e.left.obj, e.left.idx, e.left, e.right, e.ty, ctx.decls); + } + } +} + +lemma OptChainRulesWeightless(e: TExpr, ctx: CondCtx) + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures ruleOptChainIndex(e, ctx).Some? ==> AWE(ruleOptChainIndex(e, ctx).value.expr, ctx.decls) == 0 + ensures ruleOptChain(e, ctx).Some? ==> AWE(ruleOptChain(e, ctx).value.expr, ctx.decls) == 0 +{ + if e.optChain? && (ruleOptChainIndex(e, ctx).Some? || ruleOptChain(e, ctx).Some?) { + assert AWE(e, ctx.decls) == WE(e, ctx.decls) + AWE(e.obj, ctx.decls) + AWSteps(e.chain, ctx.decls); + assert WE(e, ctx.decls) >= 2; + assert AWE(e.obj, ctx.decls) == 0 && AWSteps(e.chain, ctx.decls) == 0; + var undef := TExpr.var_("undefined", Ty.void); + assert AWE(undef, ctx.decls) == WE(undef, ctx.decls); + if ruleOptChainIndex(e, ctx).Some? { + applyChainBounds(e.obj, e.chain); + assert AWE(e.obj, ctx.decls) + == WE(e.obj, ctx.decls) + AWE(e.obj.obj, ctx.decls) + AWE(e.obj.idx, ctx.decls); + BoundsGuardWeightless(e.obj.obj, e.obj.idx, applyChain(e.obj, e.chain), undef, e.ty, ctx.decls); + } + if ruleOptChain(e, ctx).Some? { + var b := freshOcBinder(ctx); + var bv := TExpr.var_(b.name, e.obj.ty.inner); + assert AWE(bv, ctx.decls) == WE(bv, ctx.decls); + applyChainBounds(bv, e.chain); + var out := TExpr.someMatch(e.obj, b.name, e.obj.ty.inner, applyChain(bv, e.chain), undef, e.ty); + assert AWE(out, ctx.decls) + == WE(out, ctx.decls) + AWE(e.obj, ctx.decls) + AWE(applyChain(bv, e.chain), ctx.decls) + AWE(undef, ctx.decls); + } + } +} + +lemma CondRulesWeightless(e: TExpr, ctx: CondCtx) + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures ruleConditionalOptionalSimple(e, ctx).Some? ==> AWE(ruleConditionalOptionalSimple(e, ctx).value.expr, ctx.decls) == 0 + ensures ruleConditionalInMap(e, ctx).Some? ==> AWE(ruleConditionalInMap(e, ctx).value.expr, ctx.decls) == 0 + ensures ruleConditionalOptionalTruthy(e, ctx).Some? ==> AWE(ruleConditionalOptionalTruthy(e, ctx).value.expr, ctx.decls) == 0 +{ + if e.conditional? { + assert AWE(e, ctx.decls) == WE(e, ctx.decls) + AWE(e.cond, ctx.decls) + AWE(e.then_, ctx.decls) + AWE(e.else_, ctx.decls); + if ruleConditionalInMap(e, ctx).Some? { + var b := binderHintForMapAccess(e.cond.right, e.cond.left, ctx); + var innerTy := e.cond.right.ty.value; + var bv := TExpr.var_(b.name, innerTy); + var out := TExpr.someMatch(e.then_, b.name, innerTy, bv, e.else_, innerTy); + assert AWE(bv, ctx.decls) == WE(bv, ctx.decls); + assert AWE(out, ctx.decls) + == WE(out, ctx.decls) + AWE(e.then_, ctx.decls) + AWE(bv, ctx.decls) + AWE(e.else_, ctx.decls); + } + if ruleConditionalOptionalTruthy(e, ctx).Some? { + var out := TExpr.someMatch(e.cond, freshName(binderHintFor(e.cond).value), e.cond.ty.inner, + e.then_, e.else_, e.ty); + assert AWE(out, ctx.decls) + == WE(out, ctx.decls) + AWE(e.cond, ctx.decls) + AWE(e.then_, ctx.decls) + AWE(e.else_, ctx.decls); + } + if ruleConditionalOptionalSimple(e, ctx).Some? { + presentFactScrutineeWeightless(e.cond); + GateExprWeightless(presentFact(e.cond).value, + if presentFact(e.cond).value.negated then e.else_ else e.then_, + if presentFact(e.cond).value.negated then e.then_ else e.else_, + e.ty, ctx.decls); + } + } +} + +// Shape and call content of the function-shaped expression rules' outputs. +lemma ExprRuleOutputShape(e: TExpr, ctx: CondCtx) + ensures ruleNullish(e, ctx).Some? ==> (ruleNullish(e, ctx).value.expr.someMatch? + && (containsMethodCall(e) ==> containsMethodCall(ruleNullish(e, ctx).value.expr))) + ensures ruleNullishIndex(e, ctx).Some? ==> (ruleNullishIndex(e, ctx).value.expr.conditional? + && (containsMethodCall(e) ==> containsMethodCall(ruleNullishIndex(e, ctx).value.expr))) + ensures ruleOptChainIndex(e, ctx).Some? ==> (ruleOptChainIndex(e, ctx).value.expr.conditional? + && (containsMethodCall(e) ==> containsMethodCall(ruleOptChainIndex(e, ctx).value.expr))) + ensures ruleOptChain(e, ctx).Some? ==> (ruleOptChain(e, ctx).value.expr.someMatch? + && (containsMethodCall(e) ==> containsMethodCall(ruleOptChain(e, ctx).value.expr))) + ensures ruleConditionalOptionalSimple(e, ctx).Some? ==> (ruleConditionalOptionalSimple(e, ctx).value.expr.someMatch? + && (containsMethodCall(e) ==> containsMethodCall(ruleConditionalOptionalSimple(e, ctx).value.expr))) + ensures ruleConditionalInMap(e, ctx).Some? ==> (ruleConditionalInMap(e, ctx).value.expr.someMatch? + && (containsMethodCall(e) ==> containsMethodCall(ruleConditionalInMap(e, ctx).value.expr))) + ensures ruleConditionalOptionalTruthy(e, ctx).Some? ==> (ruleConditionalOptionalTruthy(e, ctx).value.expr.someMatch? + && (containsMethodCall(e) ==> containsMethodCall(ruleConditionalOptionalTruthy(e, ctx).value.expr))) + ensures ruleNullish(e, ctx).Some? ==> ruleNullish(e, ctx).value.expr.ty == e.ty + ensures ruleNullishIndex(e, ctx).Some? ==> ruleNullishIndex(e, ctx).value.expr.ty == e.ty + ensures ruleOptChainIndex(e, ctx).Some? ==> ruleOptChainIndex(e, ctx).value.expr.ty == e.ty + ensures ruleOptChain(e, ctx).Some? ==> ruleOptChain(e, ctx).value.expr.ty == e.ty + ensures ruleConditionalOptionalSimple(e, ctx).Some? ==> ruleConditionalOptionalSimple(e, ctx).value.expr.ty == e.ty + ensures ruleConditionalOptionalTruthy(e, ctx).Some? ==> ruleConditionalOptionalTruthy(e, ctx).value.expr.ty == e.ty +{ + if ruleConditionalInMap(e, ctx).Some? { + assert !containsMethodCall(e.cond.left) && !containsMethodCall(e.cond.right); + assert e.cond == TExpr.binop(e.cond.op, e.cond.left, e.cond.right, e.cond.ty); + assert !containsMethodCall(e.cond); + } + if ruleOptChainIndex(e, ctx).Some? { applyChainBounds(e.obj, e.chain); } + if ruleOptChain(e, ctx).Some? { + applyChainBounds(TExpr.var_(freshOcBinder(ctx).name, e.obj.ty.inner), e.chain); + } +} + +lemma StmtRuleOutputShape(s: TStmt, ctx: CondCtx) + ensures ruleIfOptionalSimple(s, ctx).Some? ==> ruleIfOptionalSimple(s, ctx).value.stmt.someMatch? + ensures ruleOptionalIndexBinding(s, ctx).Some? ==> ( + ruleOptionalIndexBinding(s, ctx).value.stmt.let? + && s.let? + && ruleOptionalIndexBinding(s, ctx).value.stmt.mutable == s.mutable) +{ +} + +lemma StmtRulesWeightless(s: TStmt, ctx: CondCtx) + requires AWS(s, ctx.decls) == WS(s, ctx.decls) + ensures ruleIfOptionalSimple(s, ctx).Some? ==> AWS(ruleIfOptionalSimple(s, ctx).value.stmt, ctx.decls) == 0 + ensures ruleOptionalIndexBinding(s, ctx).Some? ==> AWS(ruleOptionalIndexBinding(s, ctx).value.stmt, ctx.decls) == 0 +{ + if ruleIfOptionalSimple(s, ctx).Some? { + assert AWS(s, ctx.decls) == WS(s, ctx.decls) + AWE(s.cond, ctx.decls) + AWSs(s.then_, ctx.decls) + AWSs(s.else_, ctx.decls); + presentFactScrutineeWeightless(s.cond); + GateStmtsWeightless(presentFact(s.cond).value, + if presentFact(s.cond).value.negated then s.else_ else s.then_, + if presentFact(s.cond).value.negated then s.then_ else s.else_, + ctx.decls); + } + if ruleOptionalIndexBinding(s, ctx).Some? { + assert AWS(s, ctx.decls) == WS(s, ctx.decls) + AWE(s.init, ctx.decls); + assert AWE(s.init, ctx.decls) == WE(s.init, ctx.decls) + AWE(s.init.obj, ctx.decls) + AWE(s.init.idx, ctx.decls); + arrayBoundsCondFacts(s.init.obj, s.init.idx); + BoundsGuardWeightless(s.init.obj, s.init.idx, s.init, TExpr.var_("undefined", Ty.void), s.ty, ctx.decls); + var g := TExpr.conditional(arrayBoundsCond(s.init.obj, s.init.idx), s.init, TExpr.var_("undefined", Ty.void), s.ty); + assert ruleOptionalIndexBinding(s, ctx).value.stmt == TStmt.let(s.name, s.ty, s.mutable, g); + assert !FLetCond(TStmt.let(s.name, s.ty, s.mutable, g)); + } +} + +function isTerminating(stmts: seq): bool +{ + if (|stmts| == 0) then + false + else + isTerminatorKind(TStmt_kind(stmts[(|stmts| - 1)])) +} + +function ruleIfOptionalSimple(s: TStmt, ctx: CondCtx): Option +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var check := presentFact(i_s_cond); + match check { + case Some(i_check_val) => + var someBody := (if i_check_val.negated then i_s_else else i_s_then); + var noneBody := (if i_check_val.negated then i_s_then else i_s_else); + if (|someBody| == 0) then + None + else + Some(StmtOut(presentMatchStmts(i_check_val, someBody, noneBody), ctx)) + case None => + None + } + case _ => + None + } +} + +function ruleEarlyReturnConsume(s: TStmt, rest: seq): Option +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|rest| == 0) then + None + else + var check := presentFact(i_s_cond); + match check { + case Some(i_check_val) => + var someBranch := (if i_check_val.negated then i_s_else else i_s_then); + var noneBranch := (if i_check_val.negated then i_s_then else i_s_else); + if (|someBranch| != 0) then + None + else + if !(isTerminating(noneBranch)) then + None + else + Some(presentMatchStmts(i_check_val, rest, noneBranch)) + case None => + None + } + case _ => + None + } +} + +function ruleEarlyReturnOptChainCompare(s: TStmt, rest: seq, ctx: CondCtx): Option +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|rest| == 0) then + None + else + if ((|i_s_else| != 0) || !(isTerminating(i_s_then))) then + None + else + var c := i_s_cond; + if ((!c.binop?) || (c.op != "!==")) then + None + else + var ocOnLeft := c.left.optChain?; + var oc := (if ocOnLeft then Option.Some(c.left) else (if c.right.optChain? then Option.Some(c.right) else Option.None)); + match oc { + case Some(i_oc_val) => + match i_oc_val { + case optChain(i__oc_val_obj, i__oc_val_chain, i__oc_val_ty) => + if (!i__oc_val_obj.ty.optional?) then + None + else + var lit := (if ocOnLeft then c.right else c.left); + var innerTy := i__oc_val_obj.ty.inner; + var hint := binderHintFor(i__oc_val_obj); + match hint { + case Some(i_hint_val) => + var binder := freshName(i_hint_val); + var binderVar := var_(binder, innerTy); + var unwrapped := restoreDiscriminantFlag(applyChain(binderVar, i__oc_val_chain), ctx.decls); + var innerGuard := binop("!==", unwrapped, lit, Ty.bool_); + var someBody := ([if_(innerGuard, i_s_then, [])] + rest); + Some(TStmt.someMatch(i__oc_val_obj, binder, innerTy, someBody, i_s_then)) + case None => + None + } + case _ => + None + } + case None => + None + } + case _ => + None + } +} + +function ruleConditionalOptionalSimple(e: TExpr, ctx: CondCtx): Option +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var check := presentFact(i_e_cond); + match check { + case Some(i_check_val) => + var someBody := (if i_check_val.negated then i_e_else else i_e_then); + var noneBody := (if i_check_val.negated then i_e_then else i_e_else); + Some(ExprOut(presentMatchExpr(i_check_val, someBody, noneBody, i_e_ty), ctx)) + case None => + None + } + case _ => + None + } +} + +function ruleNullish(e: TExpr, ctx: CondCtx): Option +{ + match e { + case nullish(i_e_left, i_e_right, i_e_ty) => + if (!i_e_left.ty.optional?) then + None + else + var innerTy := i_e_left.ty.inner; + var b := freshOcBinder(ctx); + Some(ExprOut(TExpr.someMatch(i_e_left, b.name, innerTy, var_(b.name, innerTy), i_e_right, i_e_ty), b.ctx)) + case _ => + None + } +} + +function ruleNullishIndex(e: TExpr, ctx: CondCtx): Option +{ + match e { + case nullish(i_e_left, i_e_right, i_e_ty) => + if (!i_e_left.index?) then + None + else + if (!i_e_left.obj.ty.array_?) then + None + else + var cond := arrayBoundsCond(i_e_left.obj, i_e_left.idx); + Some(ExprOut(conditional(cond, i_e_left, i_e_right, i_e_ty), ctx)) + case _ => + None + } +} + +function ruleOptChainIndex(e: TExpr, ctx: CondCtx): Option +{ + match e { + case optChain(i_e_obj, i_e_chain, i_e_ty) => + if (!i_e_obj.index?) then + None + else + if (!i_e_obj.obj.ty.array_?) then + None + else + var cond := arrayBoundsCond(i_e_obj.obj, i_e_obj.idx); + var body := applyChain(i_e_obj, i_e_chain); + var undef := var_("undefined", Ty.void); + Some(ExprOut(conditional(cond, body, undef, i_e_ty), ctx)) + case _ => + None + } +} + +function ruleOptionalIndexBinding(s: TStmt, ctx: CondCtx): Option +{ + match s { + case let(i_s_name, i_s_ty, i_s_mutable, i_s_init) => + if (!i_s_ty.optional?) then + None + else + var init := i_s_init; + match init { + case index(i_init_obj, i_init_idx, i_init_ty) => + if (!i_init_obj.ty.array_?) then + None + else + if i_init_ty.optional? then + None + else + var cond := arrayBoundsCond(i_init_obj, i_init_idx); + var undef := var_("undefined", Ty.void); + var guarded := conditional(cond, init, undef, i_s_ty); + Some(StmtOut(s.(init := guarded), ctx)) + case _ => + None + } + case _ => + None + } +} + +function ruleOptChain(e: TExpr, ctx: CondCtx): Option +{ + match e { + case optChain(i_e_obj, i_e_chain, i_e_ty) => + if (!i_e_obj.ty.optional?) then + None + else + var innerTy := i_e_obj.ty.inner; + var b := freshOcBinder(ctx); + var body := applyChain(var_(b.name, innerTy), i_e_chain); + var noneBody := var_("undefined", Ty.void); + Some(ExprOut(TExpr.someMatch(i_e_obj, b.name, innerTy, body, noneBody, i_e_ty), b.ctx)) + case _ => + None + } +} + +function ruleConditionalInMap(e: TExpr, ctx: CondCtx): Option +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + if ((!i_e_cond.binop?) || (i_e_cond.op != "in")) then + None + else + var m := i_e_cond.right; + var k := i_e_cond.left; + if (!m.ty.map_?) then + None + else + if (!i_e_then.index?) then + None + else + if (!(exprEqual(i_e_then.obj, m)) || !(exprEqual(i_e_then.idx, k))) then + None + else + if (i_e_else.ty.optional? || i_e_else.ty.void?) then + None + else + if (!i_e_then.ty.optional?) then + None + else + var innerTy := m.ty.value; + var b := binderHintForMapAccess(m, k, ctx); + Some(ExprOut(TExpr.someMatch(i_e_then, b.name, innerTy, var_(b.name, innerTy), i_e_else, innerTy), b.ctx)) + case _ => + None + } +} + +function ruleConditionalOptionalTruthy(e: TExpr, ctx: CondCtx): Option +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + if (!i_e_cond.ty.optional?) then + None + else + var hint := binderHintFor(i_e_cond); + match hint { + case Some(i_hint_val) => + var binder := freshName(i_hint_val); + Some(ExprOut(TExpr.someMatch(i_e_cond, binder, i_e_cond.ty.inner, i_e_then, i_e_else, i_e_ty), ctx)) + case None => + None + } + case _ => + None + } +} + +function ruleLetCondAndOptional(s: TStmt): Option> +{ + if ((!s.let?) || s.mutable) then + None + else + if (!s.init.conditional?) then + None + else + var extracted := leadingPresent(s.init.cond); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var assignIf := if_(restCond, [assign(s.name, s.init.then_)], []); + Some([let(s.name, s.ty, true, s.init.else_), presentMatchStmts(check, [assignIf], [])]) + case None => + None + } +} + +function containsMethodCall(e: TExpr): bool +{ + if (e.call? && e.callKind.method_?) then + var bid := e.builtinId; + match bid { + case Some(i_bid_val) => + (!(builtinPure(i_bid_val)) || (match e { case var_(i_e_name, i_e_ty) => false case num(i_e_value, i_e_ty) => false case bigint(i_e_value, i_e_ty) => false case str(i_e_value, i_e_ty) => false case bool_(i_e_value, i_e_ty) => false case havoc(i_e_ty) => false case binop(i_e_op, i_e_left, i_e_right, i_e_ty) => (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) case unop(i_e_op, i_e_expr, i_e_ty) => containsMethodCall(i_e_expr) case call(i_e_fn, i_e_args, i_e_ty, i_e_callKind, i_e_builtinId) => (containsMethodCall(i_e_fn) || (exists x :: x in i_e_args && containsMethodCall(x))) case index(i_e_obj, i_e_idx, i_e_ty) => (containsMethodCall(i_e_obj) || containsMethodCall(i_e_idx)) case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => containsMethodCall(i_e_obj) case record(i_e_spread, i_e_fields, i_e_ty) => ((match i_e_spread { case Some(i_e_spread_val) => containsMethodCall(i_e_spread_val) case None => false }) || (exists f :: f in i_e_fields && containsMethodCall(f.value))) case arrayLiteral(i_e_elems, i_e_ty) => (exists x :: x in i_e_elems && containsMethodCall(x)) case lambda(i_e_params, i_e_body, i_e_ty) => false case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => ((containsMethodCall(i_e_cond) || containsMethodCall(i_e_then)) || containsMethodCall(i_e_else)) case optChain(i_e_obj, i_e_chain, i_e_ty) => containsMethodCall(i_e_obj) case nullish(i_e_left, i_e_right, i_e_ty) => (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) case forall_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => containsMethodCall(i_e_body) case exists_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => containsMethodCall(i_e_body) case someMatch(i_e_scrutinee, i_e_binder, i_e_binderTy, i_e_someBody, i_e_noneBody, i_e_ty) => ((containsMethodCall(i_e_scrutinee) || containsMethodCall(i_e_someBody)) || containsMethodCall(i_e_noneBody)) case tagMatch(i_e_scrutinee, i_e_typeName, i_e_cases, i_e_fallthrough, i_e_ty) => ((containsMethodCall(i_e_scrutinee) || (exists c :: c in i_e_cases && containsMethodCall(c.body))) || (match i_e_fallthrough { case Some(i_e_fallthrough_val) => containsMethodCall(i_e_fallthrough_val) case None => false })) })) + case None => + true + } + else + match e { + case var_(i_e_name, i_e_ty) => + false + case num(i_e_value, i_e_ty) => + false + case bigint(i_e_value, i_e_ty) => + false + case str(i_e_value, i_e_ty) => + false + case bool_(i_e_value, i_e_ty) => + false + case havoc(i_e_ty) => + false + case binop(i_e_op, i_e_left, i_e_right, i_e_ty) => + (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) + case unop(i_e_op, i_e_expr, i_e_ty) => + containsMethodCall(i_e_expr) + case call(i_e_fn, i_e_args, i_e_ty, i_e_callKind, i_e_builtinId) => + (containsMethodCall(i_e_fn) || (exists x :: x in i_e_args && containsMethodCall(x))) + case index(i_e_obj, i_e_idx, i_e_ty) => + (containsMethodCall(i_e_obj) || containsMethodCall(i_e_idx)) + case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => + containsMethodCall(i_e_obj) + case record(i_e_spread, i_e_fields, i_e_ty) => + ((match i_e_spread { case Some(i_e_spread_val) => containsMethodCall(i_e_spread_val) case None => false }) || (exists f :: f in i_e_fields && containsMethodCall(f.value))) + case arrayLiteral(i_e_elems, i_e_ty) => + (exists x :: x in i_e_elems && containsMethodCall(x)) + case lambda(i_e_params, i_e_body, i_e_ty) => + false + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + ((containsMethodCall(i_e_cond) || containsMethodCall(i_e_then)) || containsMethodCall(i_e_else)) + case optChain(i_e_obj, i_e_chain, i_e_ty) => + containsMethodCall(i_e_obj) + case nullish(i_e_left, i_e_right, i_e_ty) => + (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) + case forall_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + containsMethodCall(i_e_body) + case exists_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + containsMethodCall(i_e_body) + case someMatch(i_e_scrutinee, i_e_binder, i_e_binderTy, i_e_someBody, i_e_noneBody, i_e_ty) => + ((containsMethodCall(i_e_scrutinee) || containsMethodCall(i_e_someBody)) || containsMethodCall(i_e_noneBody)) + case tagMatch(i_e_scrutinee, i_e_typeName, i_e_cases, i_e_fallthrough, i_e_ty) => + ((containsMethodCall(i_e_scrutinee) || (exists c :: c in i_e_cases && containsMethodCall(c.body))) || (match i_e_fallthrough { case Some(i_e_fallthrough_val) => containsMethodCall(i_e_fallthrough_val) case None => false })) + } +} + +function collectElseChain(s: TStmt, scrutineeName: string, ctx: CondCtx): ElseChain + ensures AWSCs(collectElseChain(s, scrutineeName, ctx).cases, ctx.decls) + + AWSs(collectElseChain(s, scrutineeName, ctx).fallthrough, ctx.decls) + <= AWS(s, ctx.decls) + ensures SSCs(collectElseChain(s, scrutineeName, ctx).cases) + + SSs(collectElseChain(s, scrutineeName, ctx).fallthrough) + <= 1 + SS(s) +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var p := variantFact(i_s_cond, ctx); + match p { + case Some(i_p_val) => + if (i_p_val.scrutineeName != scrutineeName) then + ElseChain([], [s]) + else + var here := TStmtCase(i_p_val.variant, i_s_then); + if (|i_s_else| == 0) then + ElseChain([here], []) + else + if ((|i_s_else| == 1) && i_s_else[0].if_?) then + var rest := collectElseChain(i_s_else[0], scrutineeName, ctx); + ElseChain(([here] + rest.cases), rest.fallthrough) + else + ElseChain([here], i_s_else) + case None => + ElseChain([], [s]) + } + case _ => + ElseChain([], [s]) + } +} + +function allCasesTerminate(cases: seq): bool +{ + ((|cases| == 0) || (if !(isTerminating(cases[0].body)) then false else allCasesTerminate(cases[1..]))) +} + +function ruleDiscriminantNegEarlyReturn(stmts: seq, ctx: CondCtx): Option + ensures ruleDiscriminantNegEarlyReturn(stmts, ctx).Some? ==> ruleDiscriminantNegEarlyReturn(stmts, ctx).value.consumed >= 1 + ensures ruleDiscriminantNegEarlyReturn(stmts, ctx).Some? ==> AWS(ruleDiscriminantNegEarlyReturn(stmts, ctx).value.stmt, ctx.decls) <= AWSs(stmts, ctx.decls) + ensures ruleDiscriminantNegEarlyReturn(stmts, ctx).Some? ==> SS(ruleDiscriminantNegEarlyReturn(stmts, ctx).value.stmt) < SSs(stmts) + ensures ruleDiscriminantNegEarlyReturn(stmts, ctx).Some? ==> + SS(ruleDiscriminantNegEarlyReturn(stmts, ctx).value.stmt) + 1 + <= SSs(stmts[..ruleDiscriminantNegEarlyReturn(stmts, ctx).value.consumed]) + ensures ruleDiscriminantNegEarlyReturn(stmts, ctx).Some? ==> + ruleDiscriminantNegEarlyReturn(stmts, ctx).value.stmt.tagMatch? +{ + if (|stmts| < 2) then + None + else + var first := stmts[0]; + if ((!first.if_?) || (|first.else_| > 0)) then + None + else + if !(isTerminating(first.then_)) then + None + else + var cond := negVariantFact(first.cond, ctx); + match cond { + case Some(i_cond_val) => + assert stmts == [first] + stmts[1..]; + assert SSs(stmts) == 1 + SS(first) + SSs(stmts[1..]); + assert AWSs(stmts, ctx.decls) == HC(first, stmts[1..], ctx.decls) + AWS(first, ctx.decls) + AWSs(stmts[1..], ctx.decls); + assert first == TStmt.if_(first.cond, first.then_, first.else_); + assert first.else_ == []; + assert SE(first.cond) >= 2; + assert AWE(i_cond_val.scrutinee, ctx.decls) == 0; + assert AWS(first, ctx.decls) + == WS(first, ctx.decls) + AWE(first.cond, ctx.decls) + + AWSs(first.then_, ctx.decls) + AWSs(first.else_, ctx.decls); + assert SS(first) == 1 + SE(first.cond) + SSs(first.then_) + SSs(first.else_); + assert AWSCs([TStmtCase(i_cond_val.variant, stmts[1..])], ctx.decls) + == AWSs(stmts[1..], ctx.decls); + assert SSCs([TStmtCase(i_cond_val.variant, stmts[1..])]) + == 1 + SSs(stmts[1..]); + assert AWS(TStmt.tagMatch(i_cond_val.scrutinee, i_cond_val.typeName, [TStmtCase(i_cond_val.variant, stmts[1..])], first.then_), ctx.decls) + == AWE(i_cond_val.scrutinee, ctx.decls) + + AWSCs([TStmtCase(i_cond_val.variant, stmts[1..])], ctx.decls) + + AWSs(first.then_, ctx.decls); + assert SS(TStmt.tagMatch(i_cond_val.scrutinee, i_cond_val.typeName, [TStmtCase(i_cond_val.variant, stmts[1..])], first.then_)) + == 1 + SE(i_cond_val.scrutinee) + + SSCs([TStmtCase(i_cond_val.variant, stmts[1..])]) + + SSs(first.then_); + assert SE(i_cond_val.scrutinee) == 1; + assert stmts[..|stmts|] == stmts; + Some(ConsumedRewrite(TStmt.tagMatch(i_cond_val.scrutinee, i_cond_val.typeName, [TStmtCase(i_cond_val.variant, stmts[1..])], first.then_), |stmts|)) + case None => + None + } +} + +lemma ruleDiscriminantNegEarlyReturn_ensures(stmts: seq, ctx: CondCtx) + ensures (match ruleDiscriminantNegEarlyReturn(stmts, ctx) { case Some(i_result_val) => ((i_result_val.consumed >= 0) && (i_result_val.consumed <= |stmts|)) case None => true }) +{ +} + +method walkExpr(e: TExpr, ctx: CondCtx) returns (res: ExprOut) + ensures res.ctx.decls == ctx.decls + ensures AWE(res.expr, ctx.decls) == 0 + ensures AWE(e, ctx.decls) == 0 ==> SE(res.expr) <= SE(e) + ensures Tame(res.expr, e, ctx.decls) + ensures !(res.expr.optChain? && res.expr.obj.ty.optional?) + ensures res.expr.ty == e.ty + || (AWE(e, ctx.decls) >= 2 + && (AWE(e, ctx.decls) == 2 ==> SE(res.expr) < SE(e))) + ensures res.expr.conditional? ==> ( + (e.conditional? && Tame(res.expr.cond, e.cond, ctx.decls)) + || leadingPresent(res.expr.cond).None?) + ensures res.expr.index? ==> ( + e.index? && res.expr.ty == e.ty + && (res.expr.obj.ty == e.obj.ty + || (AWE(e, ctx.decls) >= 2 + && (AWE(e, ctx.decls) == 2 ==> SE(res.expr) < SE(e))))) + ensures res.expr.binop? ==> ( + e.binop? && e.op == res.expr.op + && Tame(res.expr.left, e.left, ctx.decls) && Tame(res.expr.right, e.right, ctx.decls) + && !(res.expr.left.optChain? && res.expr.left.obj.ty.optional?) + && !(res.expr.right.optChain? && res.expr.right.obj.ty.optional?)) + decreases AWE(e, ctx.decls), SE(e), 2 +{ + var i_t0 := recurseExpr(e, ctx); + var r := i_t0; + var i_t1 := ruleNullish(r.expr, r.ctx); + var i_t2 := ruleNullishIndex(r.expr, r.ctx); + var i_t3 := ruleOptChainIndex(r.expr, r.ctx); + var i_t4 := ruleOptChain(r.expr, r.ctx); + var i_t5 := ruleImplOptional(r.expr, r.ctx); + var i_t6 := ruleImplArrayIsArray(r.expr, r.ctx); + var i_t7 := ruleConditionalArrayIsArray(r.expr, r.ctx); + var i_t8 := ruleConditionalAndArrayIsArray(r.expr, r.ctx); + var i_t9 := ruleConditionalAndOptional(r.expr, r.ctx); + var i_t10 := ruleConditionalOptionalSimple(r.expr, r.ctx); + var i_t11 := ruleConditionalInMap(r.expr, r.ctx); + var i_t12 := ruleConditionalOptionalTruthy(r.expr, r.ctx); + ExprRuleOutputShape(r.expr, r.ctx); + NullishRulesWeightless(r.expr, r.ctx); + OptChainRulesWeightless(r.expr, r.ctx); + CondRulesWeightless(r.expr, r.ctx); + if i_t1.Some? { RuleOutputNeutral(i_t1.value.expr, e, ctx.decls); } + if i_t2.Some? { RuleOutputNeutral(i_t2.value.expr, e, ctx.decls); + arrayBoundsCondFacts(r.expr.left.obj, r.expr.left.idx); } + if i_t3.Some? { RuleOutputNeutral(i_t3.value.expr, e, ctx.decls); + arrayBoundsCondFacts(r.expr.obj.obj, r.expr.obj.idx); } + if i_t4.Some? { RuleOutputNeutral(i_t4.value.expr, e, ctx.decls); } + if i_t5.Some? { RuleOutputNeutral(i_t5.value.expr, e, ctx.decls); } + if i_t6.Some? { RuleOutputNeutral(i_t6.value.expr, e, ctx.decls); } + if i_t7.Some? { RuleOutputNeutral(i_t7.value.expr, e, ctx.decls); } + if i_t8.Some? { RuleOutputNeutral(i_t8.value.expr, e, ctx.decls); } + if i_t9.Some? { RuleOutputNeutral(i_t9.value.expr, e, ctx.decls); } + if i_t10.Some? { RuleOutputNeutral(i_t10.value.expr, e, ctx.decls); } + if i_t11.Some? { InMapShrinks(r.expr, r.ctx); RuleOutputNeutral(i_t11.value.expr, e, ctx.decls); } + if i_t12.Some? { RuleOutputNeutral(i_t12.value.expr, e, ctx.decls); } + // No rule fired: the recursed node is already normal, and the charge + // guards being false is exactly what the rules' None-ensures say. + if i_t1.None? && i_t2.None? && i_t3.None? && i_t4.None? && i_t5.None? && i_t6.None? + && i_t7.None? && i_t8.None? && i_t9.None? && i_t10.None? && i_t11.None? && i_t12.None? { + assert WE(r.expr, ctx.decls) == 0; + assert r.expr.optChain? ==> !r.expr.obj.ty.optional?; + } + // Weightless input: only the uncharged in-map rule can still fire, and + // it shrinks the term. + // The only retyping rule is the in-map ternary, and it shrinks the term; + // the two `==>` rules retype to bool but always charge at least 3. + if i_t11.Some? && AWE(e, ctx.decls) == 2 { + assert WE(r.expr, ctx.decls) >= 2; + assert AWE(r.expr, ctx.decls) == AWE(e, ctx.decls); + assert SE(r.expr) <= SE(e); + } + if i_t5.Some? || i_t6.Some? { + if i_t5.Some? { + assert FImplOptional(r.expr); + if leadingPresent(r.expr.left).Some? { leadingPresentShrinks(r.expr.left); } + assert PC(r.expr.left) >= 1; + } else { + assert FImplArrayIsArray(r.expr, ctx.decls); + isArrayFactShape(r.expr.left, CondCtx(ctx.decls, 0)); + assert AC(r.expr.left, ctx.decls) >= 1; + } + assert WE(r.expr, ctx.decls) >= 3; + assert AWE(e, ctx.decls) >= 3; + } + if AWE(e, ctx.decls) == 0 { + assert AWE(r.expr, ctx.decls) == 0 && WE(r.expr, ctx.decls) == 0 && SE(r.expr) <= SE(e); + assert i_t1.None? && i_t2.None? && i_t3.None? && i_t4.None?; + assert i_t5.None? && i_t6.None? && i_t7.None? && i_t8.None?; + assert i_t9.None? && i_t10.None? && i_t12.None?; + } + return (match (match (match (match (match (match (match (match (match (match (match (match i_t1 { case Some(i_oc0_val) => Option.Some(i_oc0_val) case None => i_t2 }) { case Some(i_oc1_val) => Option.Some(i_oc1_val) case None => i_t3 }) { case Some(i_oc2_val) => Option.Some(i_oc2_val) case None => i_t4 }) { case Some(i_oc3_val) => Option.Some(i_oc3_val) case None => i_t5 }) { case Some(i_oc4_val) => Option.Some(i_oc4_val) case None => i_t6 }) { case Some(i_oc5_val) => Option.Some(i_oc5_val) case None => i_t7 }) { case Some(i_oc6_val) => Option.Some(i_oc6_val) case None => i_t8 }) { case Some(i_oc7_val) => Option.Some(i_oc7_val) case None => i_t9 }) { case Some(i_oc8_val) => Option.Some(i_oc8_val) case None => i_t10 }) { case Some(i_oc9_val) => Option.Some(i_oc9_val) case None => i_t11 }) { case Some(i_oc10_val) => Option.Some(i_oc10_val) case None => i_t12 }) { case Some(i_oc11_val) => i_oc11_val case None => r }); +} + +method walkExprs(es: seq, ctx: CondCtx) returns (res: ExprsOut) + ensures res.ctx.decls == ctx.decls + ensures AWEs(res.exprs, ctx.decls) == 0 + ensures AWEs(es, ctx.decls) == 0 ==> SEs(res.exprs) <= SEs(es) + ensures |res.exprs| == |es| + ensures forall i :: 0 <= i < |es| ==> Tame(res.exprs[i], es[i], ctx.decls) + decreases AWEs(es, ctx.decls), SEs(es), 2 +{ + if (|es| == 0) { + return ExprsOut([], ctx); + } + var i_t13 := walkExpr(es[0], ctx); + var head := i_t13; + var i_t14 := walkExprs(es[1..], head.ctx); + var tail := i_t14; + assert forall i :: 1 <= i < |es| ==> es[1..][i-1] == es[i]; + return ExprsOut(([head.expr] + tail.exprs), tail.ctx); +} + +method walkChainSteps(steps: seq, ctx: CondCtx) returns (res: StepsOut) + ensures res.ctx.decls == ctx.decls + ensures AWSteps(res.steps, ctx.decls) == 0 + ensures AWSteps(steps, ctx.decls) == 0 ==> SSteps(res.steps) <= SSteps(steps) + decreases AWSteps(steps, ctx.decls), SSteps(steps), 2 +{ + if (|steps| == 0) { + return StepsOut([], ctx); + } + AWStepsHead(steps, ctx.decls); + var s := steps[0]; + var step := s; + var c := ctx; + match s { + case call(i_s_args, i_s_ty, i_s_callKind, i_s_builtinId) => + var i_t15 := walkExprs(i_s_args, ctx); + var args := i_t15; + step := s.(args := args.exprs); + c := args.ctx; + case index(i_s_idx, i_s_ty) => + var i_t16 := walkExpr(i_s_idx, ctx); + var idx := i_t16; + step := s.(idx := idx.expr); + c := idx.ctx; + case field(i_s_name, i_s_ty) => + + } + var i_t17 := walkChainSteps(steps[1..], c); + var rest := i_t17; + return StepsOut(([step] + rest.steps), rest.ctx); +} + +method walkRecordFields(fields: seq, ctx: CondCtx) returns (res: RecordFieldsOut) + ensures res.ctx.decls == ctx.decls + ensures AWRFs(res.fields, ctx.decls) == 0 + ensures AWRFs(fields, ctx.decls) == 0 ==> SRFs(res.fields) <= SRFs(fields) + ensures |res.fields| == |fields| + ensures forall i :: 0 <= i < |fields| ==> Tame(res.fields[i].value, fields[i].value, ctx.decls) + decreases AWRFs(fields, ctx.decls), SRFs(fields), 2 +{ + if (|fields| == 0) { + return RecordFieldsOut([], ctx); + } + AWRFsHead(fields, ctx.decls); + var i_t18 := walkExpr(fields[0].value, ctx); + var v := i_t18; + var i_t19 := walkRecordFields(fields[1..], v.ctx); + var rest := i_t19; + assert forall i :: 1 <= i < |fields| ==> fields[1..][i-1] == fields[i]; + return RecordFieldsOut(([fields[0].(value := v.expr)] + rest.fields), rest.ctx); +} + +method walkExprCases(cases: seq, ctx: CondCtx) returns (res: ExprCasesOut) + ensures res.ctx.decls == ctx.decls + ensures AWECs(res.cases, ctx.decls) == 0 + ensures AWECs(cases, ctx.decls) == 0 ==> SECs(res.cases) <= SECs(cases) + ensures |res.cases| == |cases| + ensures forall i :: 0 <= i < |cases| ==> Tame(res.cases[i].body, cases[i].body, ctx.decls) + decreases AWECs(cases, ctx.decls), SECs(cases), 2 +{ + if (|cases| == 0) { + return ExprCasesOut([], ctx); + } + AWECsHead(cases, ctx.decls); + var i_t20 := walkExpr(cases[0].body, ctx); + var b := i_t20; + var i_t21 := walkExprCases(cases[1..], b.ctx); + var rest := i_t21; + assert forall i :: 1 <= i < |cases| ==> cases[1..][i-1] == cases[i]; + return ExprCasesOut(([cases[0].(body := b.expr)] + rest.cases), rest.ctx); +} + +method walkStmtCases(cases: seq, ctx: CondCtx) returns (res: StmtCasesOut) + ensures res.ctx.decls == ctx.decls + ensures AWSCs(res.cases, ctx.decls) == 0 + ensures AWSCs(cases, ctx.decls) == 0 ==> SSCs(res.cases) <= SSCs(cases) + decreases AWSCs(cases, ctx.decls), SSCs(cases), 2 +{ + if (|cases| == 0) { + return StmtCasesOut([], ctx); + } + AWSCsHead(cases, ctx.decls); + var i_t22 := walkStmts(cases[0].body, ctx); + var b := i_t22; + var i_t23 := walkStmtCases(cases[1..], b.ctx); + var rest := i_t23; + return StmtCasesOut(([cases[0].(body := b.stmts)] + rest.cases), rest.ctx); +} + +method walkSwitchCases(cases: seq, ctx: CondCtx) returns (res: SwitchCasesOut) + ensures res.ctx.decls == ctx.decls + ensures AWWCs(res.cases, ctx.decls) == 0 + ensures AWWCs(cases, ctx.decls) == 0 ==> SWCs(res.cases) <= SWCs(cases) + decreases AWWCs(cases, ctx.decls), SWCs(cases), 2 +{ + if (|cases| == 0) { + return SwitchCasesOut([], ctx); + } + AWWCsHead(cases, ctx.decls); + var i_t24 := walkStmts(cases[0].body, ctx); + var b := i_t24; + var i_t25 := walkSwitchCases(cases[1..], b.ctx); + var rest := i_t25; + return SwitchCasesOut(([cases[0].(body := b.stmts)] + rest.cases), rest.ctx); +} + +method recurseExpr(e: TExpr, ctx: CondCtx) returns (res: ExprOut) + ensures res.ctx.decls == ctx.decls + ensures AWE(res.expr, ctx.decls) == WE(res.expr, ctx.decls) + ensures AWE(res.expr, ctx.decls) <= AWE(e, ctx.decls) + ensures AWE(res.expr, ctx.decls) == AWE(e, ctx.decls) ==> SE(res.expr) <= SE(e) + ensures Tame(res.expr, e, ctx.decls) + ensures res.expr.ty == e.ty + ensures res.expr.conditional? ==> ( + (e.conditional? && Tame(res.expr.cond, e.cond, ctx.decls)) + || leadingPresent(res.expr.cond).None?) + ensures res.expr.index? ==> ( + e.index? && res.expr.ty == e.ty + && (res.expr.obj.ty == e.obj.ty + || (AWE(e, ctx.decls) >= 2 + && (AWE(e, ctx.decls) == 2 ==> SE(res.expr) < SE(e))))) + ensures res.expr.binop? ==> ( + e.binop? && e.op == res.expr.op + && Tame(res.expr.left, e.left, ctx.decls) && Tame(res.expr.right, e.right, ctx.decls) + && !(res.expr.left.optChain? && res.expr.left.obj.ty.optional?) + && !(res.expr.right.optChain? && res.expr.right.obj.ty.optional?)) + decreases AWE(e, ctx.decls), SE(e), 0 +{ + match e { + case var_(i_e_name, i_e_ty) => + return ExprOut(e, ctx); + case num(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case bigint(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case str(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case bool_(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case havoc(i_e_ty) => + return ExprOut(e, ctx); + case binop(i_e_op, i_e_left, i_e_right, i_e_ty) => + var i_t26 := walkExpr(i_e_left, ctx); + var l := i_t26; + var i_t27 := walkExpr(i_e_right, l.ctx); + var r := i_t27; + TameBinop(e, l.expr, r.expr, ctx.decls); + return ExprOut(e.(left := l.expr, right := r.expr), r.ctx); + case unop(i_e_op, i_e_expr, i_e_ty) => + var i_t28 := walkExpr(i_e_expr, ctx); + var x := i_t28; + TameUnop(e, x.expr, ctx.decls); + return ExprOut(e.(expr := x.expr), x.ctx); + case call(i_e_fn, i_e_args, i_e_ty, i_e_callKind, i_e_builtinId) => + var i_t29 := walkExpr(i_e_fn, ctx); + var fn := i_t29; + var i_t30 := walkExprs(i_e_args, fn.ctx); + var args := i_t30; + TameCall(e, fn.expr, args.exprs, ctx.decls); + return ExprOut(e.(fn := fn.expr, args := args.exprs), args.ctx); + case index(i_e_obj, i_e_idx, i_e_ty) => + var i_t31 := walkExpr(i_e_obj, ctx); + var obj := i_t31; + var i_t32 := walkExpr(i_e_idx, obj.ctx); + var idx := i_t32; + TameIndex(e, obj.expr, idx.expr, ctx.decls); + return ExprOut(e.(obj := obj.expr, idx := idx.expr), idx.ctx); + case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => + var i_t33 := walkExpr(i_e_obj, ctx); + var obj := i_t33; + TameField(e, obj.expr, ctx.decls); + return ExprOut(e.(obj := obj.expr), obj.ctx); + case record(i_e_spread, i_e_fields, i_e_ty) => + var spread := None; + match i_e_spread { + case Some(i_e_spread_val) => + assert e == TExpr.record(i_e_spread, i_e_fields, i_e_ty); + assert AWOE(i_e_spread, ctx.decls) == AWE(i_e_spread_val, ctx.decls); + assert SOE(i_e_spread) == 1 + SE(i_e_spread_val); + var i_t34 := walkExpr(i_e_spread_val, ctx); + spread := Some(i_t34); + case None => + + } + var i_t35 := walkRecordFields(i_e_fields, (match spread { case Some(i_spread_val) => i_spread_val.ctx case None => ctx })); + var fields := i_t35; + CMCExistsRF(fields.fields, i_e_fields); + TameOpaque(e.(spread := (match spread { case Some(i_spread_val) => Option.Some(i_spread_val.expr) case None => Option.None }), fields := fields.fields), e, ctx.decls); + return ExprOut(e.(spread := (match spread { case Some(i_spread_val) => Option.Some(i_spread_val.expr) case None => Option.None }), fields := fields.fields), fields.ctx); + case arrayLiteral(i_e_elems, i_e_ty) => + var i_t36 := walkExprs(i_e_elems, ctx); + var elems := i_t36; + CMCExists(elems.exprs, i_e_elems); + TameOpaque(e.(elems := elems.exprs), e, ctx.decls); + return ExprOut(e.(elems := elems.exprs), elems.ctx); + case lambda(i_e_params, i_e_body, i_e_ty) => + var i_t37 := walkStmts(i_e_body, ctx); + var body := i_t37; + TameOpaque(e.(body_lambda := body.stmts), e, ctx.decls); + return ExprOut(e.(body_lambda := body.stmts), body.ctx); + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var i_t38 := walkExpr(i_e_cond, ctx); + var cond := i_t38; + var i_t39 := walkExpr(i_e_then, cond.ctx); + var then_ := i_t39; + var i_t40 := walkExpr(i_e_else, then_.ctx); + var els := i_t40; + assert e == TExpr.conditional(i_e_cond, i_e_then, i_e_else, i_e_ty); + CondChargeMonotone(e, cond.expr, then_.expr, els.expr, ctx.decls); + // The in-map driver's one walk-unstable conjunct is the else branch's + // type; a retype there costs 2, exactly the flat charge, and shrinks. + if els.expr.ty == i_e_else.ty { + InMapChargeMonotone(e, cond.expr, then_.expr, els.expr, ctx.decls); + assert WE(e.(cond := cond.expr, then_ := then_.expr, else_ := els.expr), ctx.decls) + <= WE(e, ctx.decls); + } else { + assert AWE(i_e_else, ctx.decls) >= 2; + assert WE(e.(cond := cond.expr, then_ := then_.expr, else_ := els.expr), ctx.decls) + <= WE(e, ctx.decls) + 2; + } + TameOpaque(e.(cond := cond.expr, then_ := then_.expr, else_ := els.expr), e, ctx.decls); + return ExprOut(e.(cond := cond.expr, then_ := then_.expr, else_ := els.expr), els.ctx); + case optChain(i_e_obj, i_e_chain, i_e_ty) => + var i_t41 := walkExpr(i_e_obj, ctx); + var obj := i_t41; + var i_t42 := walkChainSteps(i_e_chain, obj.ctx); + var chain := i_t42; + TameOpaque(e.(obj := obj.expr, chain := chain.steps), e, ctx.decls); + assert e == TExpr.optChain(i_e_obj, i_e_chain, i_e_ty); + // The optChain drivers read only the receiver's type and index shape. + if obj.expr.ty == i_e_obj.ty + && (obj.expr.index? ==> obj.expr.obj.ty == i_e_obj.obj.ty) { + assert WE(e.(obj := obj.expr, chain := chain.steps), ctx.decls) <= WE(e, ctx.decls); + } else { + assert AWE(i_e_obj, ctx.decls) >= 2; + assert WE(e.(obj := obj.expr, chain := chain.steps), ctx.decls) <= 2; + } + return ExprOut(e.(obj := obj.expr, chain := chain.steps), chain.ctx); + case nullish(i_e_left, i_e_right, i_e_ty) => + var i_t43 := walkExpr(i_e_left, ctx); + var l := i_t43; + var i_t44 := walkExpr(i_e_right, l.ctx); + var r := i_t44; + TameOpaque(e.(left := l.expr, right := r.expr), e, ctx.decls); + return ExprOut(e.(left := l.expr, right := r.expr), r.ctx); + case forall_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + var i_t45 := walkExpr(i_e_body, ctx); + var b := i_t45; + TameOpaque(e.(body_forall := b.expr), e, ctx.decls); + return ExprOut(e.(body_forall := b.expr), b.ctx); + case exists_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + var i_t46 := walkExpr(i_e_body, ctx); + var b := i_t46; + TameOpaque(e.(body_exists := b.expr), e, ctx.decls); + return ExprOut(e.(body_exists := b.expr), b.ctx); + case someMatch(i_e_scrutinee, i_e_binder, i_e_binderTy, i_e_someBody, i_e_noneBody, i_e_ty) => + var i_t47 := walkExpr(i_e_someBody, ctx); + var some := i_t47; + var i_t48 := walkExpr(i_e_noneBody, some.ctx); + var none := i_t48; + TameOpaque(e.(someBody := some.expr, noneBody := none.expr), e, ctx.decls); + return ExprOut(e.(someBody := some.expr, noneBody := none.expr), none.ctx); + case tagMatch(i_e_scrutinee, i_e_typeName, i_e_cases, i_e_fallthrough, i_e_ty) => + var i_t49 := walkExpr(i_e_scrutinee, ctx); + var scrut := i_t49; + var i_t50 := walkExprCases(i_e_cases, scrut.ctx); + var cases := i_t50; + var ft := None; + match i_e_fallthrough { + case Some(i_e_fallthrough_val) => + assert e == TExpr.tagMatch(i_e_scrutinee, i_e_typeName, i_e_cases, i_e_fallthrough, i_e_ty); + assert AWOE(i_e_fallthrough, ctx.decls) == AWE(i_e_fallthrough_val, ctx.decls); + assert SOE(i_e_fallthrough) == 1 + SE(i_e_fallthrough_val); + var i_t51 := walkExpr(i_e_fallthrough_val, cases.ctx); + ft := Some(i_t51); + case None => + + } + CMCExistsEC(cases.cases, i_e_cases); + TameOpaque(e.(scrutinee := scrut.expr, cases := cases.cases, fallthrough := (match ft { case Some(i_ft_val) => Option.Some(i_ft_val.expr) case None => Option.None })), e, ctx.decls); + return ExprOut(e.(scrutinee := scrut.expr, cases := cases.cases, fallthrough := (match ft { case Some(i_ft_val) => Option.Some(i_ft_val.expr) case None => Option.None })), (match ft { case Some(i_ft_val) => i_ft_val.ctx case None => cases.ctx })); + } +} + +method walkStmt(s: TStmt, ctx: CondCtx) returns (res: StmtOut) + ensures res.ctx.decls == ctx.decls + ensures res.stmt.let? ==> s.let? + ensures AWS(res.stmt, ctx.decls) <= (if FLetCond(res.stmt) then WS(res.stmt, ctx.decls) else 0) + ensures AWS(s, ctx.decls) == 0 ==> SS(res.stmt) <= SS(s) + ensures AWS(res.stmt, ctx.decls) <= AWS(s, ctx.decls) + ensures TStmt_kind(res.stmt) == TStmt_kind(s) || res.stmt.someMatch? || res.stmt.tagMatch? + ensures res.stmt.let? ==> res.stmt.mutable == s.mutable + ensures res.stmt.if_? ==> ( + s.if_? && Tame(res.stmt.cond, s.cond, ctx.decls) + && !(res.stmt.cond.optChain? && res.stmt.cond.obj.ty.optional?) + && (res.stmt.cond.binop? ==> s.cond.binop? && s.cond.op == res.stmt.cond.op) + && (res.stmt.cond.binop? ==> ( + !(res.stmt.cond.left.optChain? && res.stmt.cond.left.obj.ty.optional?) + && !(res.stmt.cond.right.optChain? && res.stmt.cond.right.obj.ty.optional?))) + && (|res.stmt.then_| == 0 <==> |s.then_| == 0) + && (|res.stmt.else_| == 0 <==> |s.else_| == 0) + && (isTerminating(res.stmt.then_) ==> isTerminating(s.then_)) + && (isTerminating(res.stmt.else_) ==> isTerminating(s.else_))) + decreases AWS(s, ctx.decls), SS(s), 2 +{ + var i_t52 := recurseStmt(s, ctx); + var r := i_t52; + var i_t53 := ruleIfAndOptional(r.stmt, r.ctx); + var i_t54 := ruleIfAndArrayIsArray(r.stmt, r.ctx); + var i_t55 := ruleIfOptionalSimple(r.stmt, r.ctx); + var i_t56 := ruleExprStmtAndOptional(r.stmt, r.ctx); + var i_t57 := ruleOptionalIndexBinding(r.stmt, r.ctx); + StmtRulesWeightless(r.stmt, r.ctx); + StmtRuleOutputShape(r.stmt, r.ctx); + // No rule fired: every charge but the let-cond one is exactly a rule's + // firability, and the let-cond charge is what the ensures allows through. + if i_t53.None? && i_t54.None? && i_t55.None? && i_t56.None? && i_t57.None? { + assert WS(r.stmt, ctx.decls) == (if FLetCond(r.stmt) then WS(r.stmt, ctx.decls) else 0); + } + if AWS(s, ctx.decls) == 0 { + assert AWS(r.stmt, ctx.decls) == 0 && WS(r.stmt, ctx.decls) == 0 && SS(r.stmt) <= SS(s); + assert i_t53.None? && i_t54.None? && i_t55.None? && i_t56.None? && i_t57.None?; + } + return (match (match (match (match (match i_t53 { case Some(i_oc12_val) => Option.Some(i_oc12_val) case None => i_t54 }) { case Some(i_oc13_val) => Option.Some(i_oc13_val) case None => i_t55 }) { case Some(i_oc14_val) => Option.Some(i_oc14_val) case None => i_t56 }) { case Some(i_oc15_val) => Option.Some(i_oc15_val) case None => i_t57 }) { case Some(i_oc16_val) => i_oc16_val case None => r }); +} + +method walkStmts(stmts: seq, ctx: CondCtx) returns (res: StmtsOut) + ensures res.ctx.decls == ctx.decls + ensures AWSs(res.stmts, ctx.decls) == 0 + ensures AWSs(stmts, ctx.decls) == 0 ==> SSs(res.stmts) <= SSs(stmts) + ensures |res.stmts| == 0 <==> |stmts| == 0 + ensures isTerminating(res.stmts) ==> isTerminating(stmts) + decreases AWSs(stmts, ctx.decls), SSs(stmts), 2 +{ + if (|stmts| == 0) { + return StmtsOut([], ctx); + } + var s := stmts[0]; + var rest := stmts[1..]; + var i_t58 := ruleDiscriminantChain(stmts, ctx); + var i_t59 := ruleDiscriminantNegEarlyReturn(stmts, ctx); + var tagged := (match i_t58 { case Some(i_oc17_val) => Option.Some(i_oc17_val) case None => i_t59 }); + match tagged { + case Some(i_tagged_val) => + AWSsPrefixSuffix(stmts, i_tagged_val.consumed, ctx.decls); + SSsSplit(stmts, i_tagged_val.consumed); + assert |stmts[..i_tagged_val.consumed]| >= 1; + assert SSs(stmts[..i_tagged_val.consumed]) >= 1; + var i_t60 := walkStmt(i_tagged_val.stmt, ctx); + var w := i_t60; + // A tagMatch head can arm no list rule, and is not a terminator. + assert !w.stmt.if_? && !isTerminatorKind(TStmt_kind(w.stmt)); + var i_t61 := walkStmts(stmts[i_tagged_val.consumed..], w.ctx); + var after := i_t61; + assert HC(w.stmt, after.stmts, ctx.decls) == 0; + assert AWS(w.stmt, ctx.decls) == 0; + assert ([w.stmt] + after.stmts)[1..] == after.stmts; + assert AWSs([w.stmt] + after.stmts, ctx.decls) + == HC(w.stmt, after.stmts, ctx.decls) + AWS(w.stmt, ctx.decls) + AWSs(after.stmts, ctx.decls); + if |after.stmts| > 0 { + assert ([w.stmt] + after.stmts)[|after.stmts|] == after.stmts[|after.stmts| - 1]; + assert stmts[|stmts| - 1] == stmts[i_tagged_val.consumed..][|stmts| - 1 - i_tagged_val.consumed]; + } + return StmtsOut(([w.stmt] + after.stmts), after.ctx); + case None => + + } + var i_t62 := ruleEarlyReturnOrChain(s, rest, ctx); + var orRewrite := i_t62; + match orRewrite { + case Some(i_orRewrite_val) => + OrChainHasWeight(s, rest, ctx.decls); + assert AWSs(stmts, ctx.decls) + == HC(s, rest, ctx.decls) + AWS(s, ctx.decls) + AWSs(rest, ctx.decls); + assert AWSs([i_orRewrite_val.stmt], ctx.decls) + == HC(i_orRewrite_val.stmt, [], ctx.decls) + AWS(i_orRewrite_val.stmt, ctx.decls); + assert HC(i_orRewrite_val.stmt, [], ctx.decls) == 0; + return StmtsOut([i_orRewrite_val.stmt], i_orRewrite_val.ctx); + case None => + + } + var i_t63 := ruleEarlyReturnConsume(s, rest); + var i_t64 := ruleEarlyReturnOptChainCompare(s, rest, ctx); + var consumed := (match i_t63 { case Some(i_oc18_val) => Option.Some(i_oc18_val) case None => i_t64 }); + match consumed { + case Some(i_consumed_val) => + // REMAINING GAP (the `ruleEarlyReturnOptChainCompare` half of this + // site). `a?.x !== b?.y` leaves the *second* chain in the rewritten + // inner guard, so the inner `if` can arm the same rule again — and + // then the head charge's `AWSs(head.then_)` term is paid once on the + // outside but four times on the inside (inner head charge, inner + // `WS`, the inner if's own branch, and the duplicated None arm). No + // constant coefficient closes that: the charge must shrink with the + // number of chains left in the guard, which the current measure does + // not track. The `ruleEarlyReturnConsume` half below is proved. + ConsumedSiteShrinks(stmts, i_consumed_val, ctx); + // Both consumed rules emit a someMatch, which is not a terminator. + assert i_consumed_val.someMatch?; + var i_t65 := walkStmt(i_consumed_val, ctx); + var w := i_t65; + return StmtsOut([w.stmt], w.ctx); + case None => + + } + var i_t66 := walkStmt(s, ctx); + var walked := i_t66; + var i_t67 := ruleLetCondAndOptional(walked.stmt); + var expanded := i_t67; + match expanded { + case Some(i_expanded_val) => + LetCondExpansionShrinks(walked.stmt, i_expanded_val, ctx.decls); + assert AWSs(stmts, ctx.decls) + == HC(s, rest, ctx.decls) + AWS(s, ctx.decls) + AWSs(rest, ctx.decls); + assert AWS(walked.stmt, ctx.decls) <= AWS(s, ctx.decls); + assert walked.ctx.decls == ctx.decls; + assert AWSs(i_expanded_val, walked.ctx.decls) < AWSs(stmts, ctx.decls); + assert forall i :: 0 <= i < |i_expanded_val| ==> + !i_expanded_val[i].if_? && (i_expanded_val[i].let? ==> i_expanded_val[i].mutable); + var i_t68 := walkEach(i_expanded_val, walked.ctx); + var ws := i_t68; + var i_t69 := walkStmts(rest, ws.ctx); + var after := i_t69; + AWSsConcatNoIf(ws.stmts, after.stmts, ctx.decls); + if |after.stmts| == 0 { + assert !isTerminatorKind(TStmt_kind(ws.stmts[1])); + } + return StmtsOut((ws.stmts + after.stmts), after.ctx); + case None => + + } + var i_t70 := walkStmts(rest, walked.ctx); + var after := i_t70; + WalkedHeadInert(s, rest, walked.stmt, after.stmts, ctx); + assert ([walked.stmt] + after.stmts)[1..] == after.stmts; + return StmtsOut(([walked.stmt] + after.stmts), after.ctx); +} + +method walkEach(stmts: seq, ctx: CondCtx) returns (res: StmtsOut) + requires forall i :: 0 <= i < |stmts| ==> !stmts[i].if_? && (stmts[i].let? ==> stmts[i].mutable) + ensures res.ctx.decls == ctx.decls + ensures |res.stmts| == |stmts| + ensures forall i :: 0 <= i < |stmts| ==> ( + !res.stmts[i].if_? + && (TStmt_kind(res.stmts[i]) == TStmt_kind(stmts[i]) + || res.stmts[i].someMatch? || res.stmts[i].tagMatch?)) + ensures AWSs(res.stmts, ctx.decls) == 0 + decreases AWSs(stmts, ctx.decls), SSs(stmts), 2 +{ + if (|stmts| == 0) { + return StmtsOut([], ctx); + } + var i_t71 := walkStmt(stmts[0], ctx); + var h := i_t71; + var i_t72 := walkEach(stmts[1..], h.ctx); + var t := i_t72; + assert forall i :: 1 <= i < |stmts| ==> stmts[1..][i-1] == stmts[i]; + return StmtsOut(([h.stmt] + t.stmts), t.ctx); +} + +method recurseStmt(s: TStmt, ctx: CondCtx) returns (res: StmtOut) + ensures res.ctx.decls == ctx.decls + ensures res.stmt.let? <==> s.let? + ensures AWS(res.stmt, ctx.decls) == WS(res.stmt, ctx.decls) + ensures AWS(res.stmt, ctx.decls) <= AWS(s, ctx.decls) + ensures AWS(res.stmt, ctx.decls) == AWS(s, ctx.decls) ==> SS(res.stmt) <= SS(s) + ensures TStmt_kind(res.stmt) == TStmt_kind(s) + ensures res.stmt.let? ==> res.stmt.mutable == s.mutable + ensures res.stmt.if_? ==> ( + s.if_? && Tame(res.stmt.cond, s.cond, ctx.decls) + && !(res.stmt.cond.optChain? && res.stmt.cond.obj.ty.optional?) + && (res.stmt.cond.binop? ==> s.cond.binop? && s.cond.op == res.stmt.cond.op) + && (res.stmt.cond.binop? ==> ( + !(res.stmt.cond.left.optChain? && res.stmt.cond.left.obj.ty.optional?) + && !(res.stmt.cond.right.optChain? && res.stmt.cond.right.obj.ty.optional?))) + && (|res.stmt.then_| == 0 <==> |s.then_| == 0) + && (|res.stmt.else_| == 0 <==> |s.else_| == 0) + && (isTerminating(res.stmt.then_) ==> isTerminating(s.then_)) + && (isTerminating(res.stmt.else_) ==> isTerminating(s.else_))) + decreases AWS(s, ctx.decls), SS(s), 0 +{ + match s { + case let(i_s_name, i_s_ty, i_s_mutable, i_s_init) => + var i_t73 := walkExpr(i_s_init, ctx); + var init := i_t73; + assert s == TStmt.let(i_s_name, i_s_ty, i_s_mutable, i_s_init); + if ruleOptionalIndexBinding(s.(init := init.expr), CondCtx(ctx.decls, 0)).Some? { + assert init.expr.index? && i_s_init.index?; + assert init.expr.ty == i_s_init.ty; + assert AWE(i_s_init, ctx.decls) == WE(i_s_init, ctx.decls) + + AWE(i_s_init.obj, ctx.decls) + AWE(i_s_init.idx, ctx.decls); + } + if FLetCond(s.(init := init.expr)) { + assert Tame(init.expr.cond, i_s_init.cond, ctx.decls); + assert leadingPresent(i_s_init.cond).Some?; + } + assert AWS(s.(init := init.expr), ctx.decls) + == WS(s.(init := init.expr), ctx.decls) + AWE(init.expr, ctx.decls); + assert AWS(s, ctx.decls) == WS(s, ctx.decls) + AWE(i_s_init, ctx.decls); + assert SS(s.(init := init.expr)) == 1 + SE(init.expr); + assert SS(s) == 1 + SE(i_s_init); + return StmtOut(s.(init := init.expr), init.ctx); + case assign(i_s_target, i_s_value) => + var i_t74 := walkExpr(i_s_value, ctx); + var v := i_t74; + return StmtOut(s.(value := v.expr), v.ctx); + case return_(i_s_value) => + var i_t75 := walkExpr(i_s_value, ctx); + var v := i_t75; + return StmtOut(s.(value := v.expr), v.ctx); + case break_ => + return StmtOut(s, ctx); + case continue_ => + return StmtOut(s, ctx); + case throw => + return StmtOut(s, ctx); + case expr(i_s_expr) => + var i_t76 := walkExpr(i_s_expr, ctx); + var x := i_t76; + return StmtOut(s.(expr := x.expr), x.ctx); + case if_(i_s_cond, i_s_then, i_s_else) => + var i_t77 := walkExpr(i_s_cond, ctx); + var cond := i_t77; + var i_t78 := walkStmts(i_s_then, cond.ctx); + var then_ := i_t78; + var i_t79 := walkStmts(i_s_else, then_.ctx); + var els := i_t79; + // The if-position drivers read the cond (walk-stable) and whether a + // branch is empty (which the list walk preserves). + assert s == TStmt.if_(i_s_cond, i_s_then, i_s_else); + assert Tame(cond.expr, i_s_cond, ctx.decls); + leadingIsArrayDeclsOnly(cond.expr, ctx, CondCtx(ctx.decls, 0)); + leadingIsArrayDeclsOnly(i_s_cond, ctx, CondCtx(ctx.decls, 0)); + assert presentFact(cond.expr).Some? ==> cond.expr == i_s_cond; + assert |then_.stmts| == 0 <==> |i_s_then| == 0; + assert |els.stmts| == 0 <==> |i_s_else| == 0; + assert WS(s.(cond := cond.expr, then_ := then_.stmts, else_ := els.stmts), ctx.decls) + <= WS(s, ctx.decls); + assert AWS(s.(cond := cond.expr, then_ := then_.stmts, else_ := els.stmts), ctx.decls) + == WS(s.(cond := cond.expr, then_ := then_.stmts, else_ := els.stmts), ctx.decls) + + AWE(cond.expr, ctx.decls) + AWSs(then_.stmts, ctx.decls) + AWSs(els.stmts, ctx.decls); + assert AWS(s, ctx.decls) + == WS(s, ctx.decls) + AWE(i_s_cond, ctx.decls) + AWSs(i_s_then, ctx.decls) + AWSs(i_s_else, ctx.decls); + assert SS(s.(cond := cond.expr, then_ := then_.stmts, else_ := els.stmts)) + == 1 + SE(cond.expr) + SSs(then_.stmts) + SSs(els.stmts); + assert SS(s) == 1 + SE(i_s_cond) + SSs(i_s_then) + SSs(i_s_else); + return StmtOut(s.(cond := cond.expr, then_ := then_.stmts, else_ := els.stmts), els.ctx); + case while_(i_s_cond, i_s_invariants, i_s_decreases, i_s_doneWith, i_s_body) => + var i_t80 := walkExpr(i_s_cond, ctx); + var cond := i_t80; + var i_t81 := walkExprs(i_s_invariants, cond.ctx); + var invs := i_t81; + var dec := None; + match i_s_decreases { + case Some(i_s_decreases_val) => + assert s == TStmt.while_(i_s_cond, i_s_invariants, i_s_decreases, i_s_doneWith, i_s_body); + assert AWOE(i_s_decreases, ctx.decls) == AWE(i_s_decreases_val, ctx.decls); + assert SOE(i_s_decreases) == 1 + SE(i_s_decreases_val); + var i_t82 := walkExpr(i_s_decreases_val, invs.ctx); + dec := Some(i_t82); + case None => + + } + var dw := None; + match i_s_doneWith { + case Some(i_s_doneWith_val) => + assert s == TStmt.while_(i_s_cond, i_s_invariants, i_s_decreases, i_s_doneWith, i_s_body); + assert AWOE(i_s_doneWith, ctx.decls) == AWE(i_s_doneWith_val, ctx.decls); + assert SOE(i_s_doneWith) == 1 + SE(i_s_doneWith_val); + var i_t83 := walkExpr(i_s_doneWith_val, (match dec { case Some(i_dec_val) => i_dec_val.ctx case None => invs.ctx })); + dw := Some(i_t83); + case None => + + } + var i_t84 := walkStmts(i_s_body, (match dw { case Some(i_dw_val) => i_dw_val.ctx case None => (match dec { case Some(i_dec_val) => i_dec_val.ctx case None => invs.ctx }) })); + var body := i_t84; + return StmtOut(s.(cond := cond.expr, invariants := invs.exprs, decreases_ := (match dec { case Some(i_dec_val) => Option.Some(i_dec_val.expr) case None => Option.None }), doneWith := (match dw { case Some(i_dw_val) => Option.Some(i_dw_val.expr) case None => Option.None }), body := body.stmts), body.ctx); + case switch(i_s_expr, i_s_discriminant, i_s_cases, i_s_defaultBody) => + var i_t85 := walkExpr(i_s_expr, ctx); + var x := i_t85; + var i_t86 := walkSwitchCases(i_s_cases, x.ctx); + var cases := i_t86; + var i_t87 := walkStmts(i_s_defaultBody, cases.ctx); + var def := i_t87; + return StmtOut(s.(expr := x.expr, cases_switch := cases.cases, defaultBody := def.stmts), def.ctx); + case forof(i_s_names, i_s_nameTypes, i_s_iterable, i_s_invariants, i_s_doneWith, i_s_body) => + var i_t88 := walkExpr(i_s_iterable, ctx); + var it := i_t88; + var i_t89 := walkExprs(i_s_invariants, it.ctx); + var invs := i_t89; + var dw := None; + match i_s_doneWith { + case Some(i_s_doneWith_val) => + assert s == TStmt.forof(i_s_names, i_s_nameTypes, i_s_iterable, i_s_invariants, i_s_doneWith, i_s_body); + assert AWOE(i_s_doneWith, ctx.decls) == AWE(i_s_doneWith_val, ctx.decls); + assert SOE(i_s_doneWith) == 1 + SE(i_s_doneWith_val); + var i_t90 := walkExpr(i_s_doneWith_val, invs.ctx); + dw := Some(i_t90); + case None => + + } + var i_t91 := walkStmts(i_s_body, (match dw { case Some(i_dw_val) => i_dw_val.ctx case None => invs.ctx })); + var body := i_t91; + return StmtOut(s.(iterable := it.expr, invariants := invs.exprs, doneWith := (match dw { case Some(i_dw_val) => Option.Some(i_dw_val.expr) case None => Option.None }), body := body.stmts), body.ctx); + case ghostLet(i_s_name, i_s_ty, i_s_init) => + var i_t92 := walkExpr(i_s_init, ctx); + var init := i_t92; + return StmtOut(s.(init := init.expr), init.ctx); + case ghostAssign(i_s_target, i_s_value) => + var i_t93 := walkExpr(i_s_value, ctx); + var v := i_t93; + return StmtOut(s.(value := v.expr), v.ctx); + case assert_(i_s_expr, i_s_assumed) => + var i_t94 := walkExpr(i_s_expr, ctx); + var x := i_t94; + return StmtOut(s.(expr := x.expr), x.ctx); + case someMatch(i_s_scrutinee, i_s_binder, i_s_binderTy, i_s_someBody, i_s_noneBody) => + var i_t95 := walkStmts(i_s_someBody, ctx); + var some := i_t95; + var i_t96 := walkStmts(i_s_noneBody, some.ctx); + var none := i_t96; + return StmtOut(s.(someBody := some.stmts, noneBody := none.stmts), none.ctx); + case tagMatch(i_s_scrutinee, i_s_typeName, i_s_cases, i_s_fallthrough) => + var i_t97 := walkExpr(i_s_scrutinee, ctx); + var scrut := i_t97; + var i_t98 := walkStmtCases(i_s_cases, scrut.ctx); + var cases := i_t98; + var i_t99 := walkStmts(i_s_fallthrough, cases.ctx); + var ft := i_t99; + return StmtOut(s.(scrutinee := scrut.expr, cases_tagMatch := cases.cases, fallthrough := ft.stmts), ft.ctx); + } +} + +method orChain(leaves: seq) returns (res: TExpr) + requires (|leaves| > 0) + requires forall k :: 0 <= k < |leaves| ==> !(leaves[k].binop? && leaves[k].op == "||") + ensures forall decls: seq :: AWE(res, decls) <= AWEs(leaves, decls) + ensures flattenOr(res) == leaves + ensures SE(res) + 1 == SEs(leaves) +{ + var acc := leaves[0]; + var i := 1; + assert leaves[..1] == [leaves[0]]; + while (i < |leaves|) + invariant 1 <= i <= |leaves| + invariant forall decls: seq :: AWE(acc, decls) <= AWEs(leaves[..i], decls) + invariant flattenOr(acc) == leaves[..i] + invariant SE(acc) + 1 == SEs(leaves[..i]) + { + assert leaves[..i+1] == leaves[..i] + [leaves[i]]; + SEsSnoc(leaves[..i], leaves[i]); + forall decls: seq ensures AWEs(leaves[..i+1], decls) == AWEs(leaves[..i], decls) + AWE(leaves[i], decls) { + AWEsSnoc(leaves[..i], leaves[i], decls); + } + assert flattenOr(leaves[i]) == [leaves[i]]; + assert flattenOr(TExpr.binop("||", acc, leaves[i], Ty.bool_)) + == flattenOr(acc) + flattenOr(leaves[i]); + assert flattenOr(TExpr.binop("||", acc, leaves[i], Ty.bool_)) == leaves[..i+1]; + assert SE(TExpr.binop("||", acc, leaves[i], Ty.bool_)) == 1 + SE(acc) + SE(leaves[i]); + acc := binop("||", acc, leaves[i], Ty.bool_); + i := (i + 1); + } + assert leaves[..i] == leaves; + return acc; +} + +method ruleEarlyReturnOrChain(s: TStmt, rest: seq, ctx: CondCtx) returns (res: Option) + ensures res.None? ==> !FOrChain(s, rest, ctx.decls) + ensures res.Some? ==> FOrChain(s, rest, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWS(res.value.stmt, ctx.decls) == 0 + ensures res.Some? ==> res.value.stmt.someMatch? + decreases HC(s, rest, ctx.decls) + AWS(s, ctx.decls) + AWSs(rest, ctx.decls), SS(s) + SSs(rest), 1 +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|rest| == 0) { + return None; + } + var i_t100 := isTerminating(i_s_then); + if (((|i_s_then| == 0) || (|i_s_else| != 0)) || !(i_t100)) { + return None; + } + if ((!i_s_cond.binop?) || (i_s_cond.op != "||")) { + return None; + } + var leaves := flattenOr(i_s_cond); + if (|leaves| < 2) { + return None; + } + var detectors: seq := []; + var residualLeaves: seq := []; + var seen: set := {}; + var i_leaf_idx := 0; + ghost var resDet: nat := 0; + FlattenOrLeavesNonOr(i_s_cond); + while i_leaf_idx < |leaves| + invariant (i_leaf_idx <= |leaves|) + invariant forall j :: 0 <= j < |detectors| ==> AWE(detectors[j].scrutinee, ctx.decls) == 0 + invariant AWEs(residualLeaves, ctx.decls) <= AWEs(leaves[..i_leaf_idx], ctx.decls) + invariant |residualLeaves| <= i_leaf_idx + invariant SEs(residualLeaves) + 2 * (i_leaf_idx - |residualLeaves|) <= SEs(leaves[..i_leaf_idx]) + invariant forall k :: 0 <= k < |residualLeaves| ==> !(residualLeaves[k].binop? && residualLeaves[k].op == "||") + invariant DETs(residualLeaves, ctx.decls) == resDet + invariant resDet + |detectors| == DETs(leaves[..i_leaf_idx], ctx.decls) + invariant |detectors| == 0 ==> seen == {} && resDet == 0 + { + assert {:split_here} true; + var leaf := leaves[i_leaf_idx]; + noneDetectorDeclsOnly(leaf, ctx, CondCtx(ctx.decls, 0)); + assert |leaves[..i_leaf_idx+1]| == i_leaf_idx + 1; + assert forall j :: 0 <= j <= i_leaf_idx ==> leaves[..i_leaf_idx+1][j] == (leaves[..i_leaf_idx] + [leaf])[j]; + assert leaves[..i_leaf_idx+1] == leaves[..i_leaf_idx] + [leaf]; + SEsSnoc(leaves[..i_leaf_idx], leaf); + AWEsSnoc(leaves[..i_leaf_idx], leaf, ctx.decls); + DETsSnoc(leaves[..i_leaf_idx], leaf, ctx.decls); + var d := noneDetector(leaf, ctx); + match d { + case Some(i_d_val) => + var key := binderHintFor(i_d_val.scrutinee); + match key { + case Some(i_key_val) => + if (i_key_val in seen) { + SEsSnoc(residualLeaves, leaf); + AWEsSnoc(residualLeaves, leaf, ctx.decls); + DETsSnoc(residualLeaves, leaf, ctx.decls); + residualLeaves := (residualLeaves + [leaf]); + resDet := resDet + 1; + i_leaf_idx := (i_leaf_idx + 1); + assert {:split_here} true; + continue; + } + assert {:split_here} true; + seen := (seen + {i_key_val}); + noneDetectorScrutineeWeightless(leaf, ctx); + detectors := (detectors + [i_d_val]); + match i_d_val.residual { + case Some(i_d_residual_val) => + noneDetectorResidualBounds(leaf, ctx); + SEsSnoc(residualLeaves, i_d_residual_val); + AWEsSnoc(residualLeaves, i_d_residual_val, ctx.decls); + DETsSnoc(residualLeaves, i_d_residual_val, ctx.decls); + noneDetectorDeclsOnly(i_d_residual_val, ctx, CondCtx(ctx.decls, 0)); + residualLeaves := (residualLeaves + [i_d_residual_val]); + case None => + + } + case None => + SEsSnoc(residualLeaves, leaf); + AWEsSnoc(residualLeaves, leaf, ctx.decls); + DETsSnoc(residualLeaves, leaf, ctx.decls); + residualLeaves := (residualLeaves + [leaf]); + resDet := resDet + 1; + i_leaf_idx := (i_leaf_idx + 1); + continue; + } + case None => + assert {:split_here} true; + SEsSnoc(residualLeaves, leaf); + AWEsSnoc(residualLeaves, leaf, ctx.decls); + DETsSnoc(residualLeaves, leaf, ctx.decls); + residualLeaves := (residualLeaves + [leaf]); + } + assert {:split_here} true; + i_leaf_idx := i_leaf_idx + 1; + } + if (|detectors| == 0) { + assert leaves[..i_leaf_idx] == leaves; + assert DETs(leaves, ctx.decls) == 0; + DETsZero(leaves, ctx.decls); + flattenOrSize(i_s_cond); + return None; + } + assert leaves[..i_leaf_idx] == leaves; + flattenOrSize(i_s_cond); + assert DETs(leaves, ctx.decls) >= 1; + DETsWitness(leaves, ctx.decls); + assert s == TStmt.if_(i_s_cond, i_s_then, i_s_else); + assert FOrChain(s, rest, ctx.decls); + assert DETs(residualLeaves, ctx.decls) <= DET(i_s_cond, ctx.decls) - 1; + var inner := rest; + if (|residualLeaves| > 0) { + var i_t101 := orChain(residualLeaves); + inner := ([if_(i_t101, i_s_then, [])] + rest); + } + // REMAINING GAP (single-residual case). With one residual leaf the + // guard is that leaf itself, which can be an `&&` chain or a demoted + // detector — so the inner `if` carries its own root charge and can + // arm an early-return rule, each paying `AWSs(then_)` again, while + // the or-chain head charge is a flat detector count. Same shape as + // the `ruleEarlyReturnOptChainCompare` gap: the charge has to scale + // with the branch, e.g. `DET(cond) * (A + B*AWSs(then_) + ...)`. + if (|residualLeaves| == 0) { + OrChainHasWeight(s, rest, ctx.decls); + assert inner == rest; + } + if (|residualLeaves| >= 2) { + assert SEs(residualLeaves) <= SE(i_s_cond) + 1; + OrChainInnerShrinks(s, rest, inner[0].cond, residualLeaves, ctx.decls); + assert inner == [TStmt.if_(inner[0].cond, i_s_then, [])] + rest; + } + if (|residualLeaves| == 1) { + assert SEs(residualLeaves) == 1 + SE(residualLeaves[0]); + assert SE(inner[0].cond) + 2 <= SE(i_s_cond); + OrChainSingleResidualShrinks(s, rest, inner[0].cond, residualLeaves, ctx.decls); + assert inner == [TStmt.if_(inner[0].cond, i_s_then, [])] + rest; + } + assert AWSs(inner, ctx.decls) < HC(s, rest, ctx.decls) + AWS(s, ctx.decls) + AWSs(rest, ctx.decls); + var i_t102 := walkStmts(inner, ctx); + var wInner := i_t102; + var nest := wInner.stmts; + var c := wInner.ctx; + var i := (|detectors| - 1); + while (i >= 0) + invariant ((|nest| > 0) || (i == (|detectors| - 1))) + invariant -1 <= i <= |detectors| - 1 + invariant c.decls == ctx.decls + invariant AWSs(nest, ctx.decls) == 0 + invariant i < |detectors| - 1 ==> |nest| == 1 && nest[0].someMatch? + { + var d := detectors[i]; + var i_t103 := walkStmts(i_s_then, c); + var wThen := i_t103; + c := wThen.ctx; + ghost var i_sm := TStmt.someMatch(d.scrutinee, d.binder, d.innerTy, nest, wThen.stmts); + assert WS(i_sm, ctx.decls) == 0; + assert HC(i_sm, [], ctx.decls) == 0; + assert AWS(i_sm, ctx.decls) == AWSs(nest, ctx.decls) + AWSs(wThen.stmts, ctx.decls); + assert AWSs([i_sm], ctx.decls) == HC(i_sm, [], ctx.decls) + AWS(i_sm, ctx.decls); + nest := [TStmt.someMatch(d.scrutinee, d.binder, d.innerTy, nest, wThen.stmts)]; + i := (i - 1); + } + assert |nest| == 1 && nest[0].someMatch?; + return Some(StmtOut(nest[0], c)); + case _ => + return None; + } +} + +method ruleImplOptional(e: TExpr, ctx: CondCtx) returns (res: Option) + ensures res.None? ==> !FImplOptional(e) + ensures res.Some? ==> FImplOptional(e) + ensures res.Some? ==> res.value.expr.someMatch? + ensures res.Some? ==> (containsMethodCall(e) ==> containsMethodCall(res.value.expr)) + ensures res.Some? ==> res.value.expr.ty.bool_? + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWE(res.value.expr, ctx.decls) == 0 + decreases AWE(e, ctx.decls), SE(e), 1 +{ + if ((!e.binop?) || (e.op != "==>")) { + return None; + } + var check := None; + var restCond := None; + var extracted := leadingPresent(e.left); + match extracted { + case Some(i_extracted_val) => + check := Some(i_extracted_val.check); + restCond := Some(i_extracted_val.restCond); + case None => + var c := presentFact(e.left); + match c { + case Some(i_c_val) => + if i_c_val.negated { + return None; + } + check := Some(i_c_val); + case None => + return None; + } + } + match check { + case Some(i_check_val) => + var innerBody := (match restCond { case Some(i_restCond_val) => binop("==>", i_restCond_val, e.right, Ty.bool_) case None => e.right }); + assert e == TExpr.binop(e.op, e.left, e.right, e.ty); + if extracted.Some? { + leadingPresentShrinks(e.left); + leadingPresentCheckWeightless(e.left); + } else { + presentFactScrutineeWeightless(e.left); + } + assert AWE(innerBody, ctx.decls) < AWE(e, ctx.decls); + if extracted.Some? { leadingPresentMethodCalls(e.left); } + assert containsMethodCall(e) ==> containsMethodCall(innerBody); + var i_t104 := walkExpr(innerBody, ctx); + var w := i_t104; + assert containsMethodCall(e) ==> containsMethodCall(w.expr); + return Some(ExprOut(TExpr.someMatch(i_check_val.scrutinee, i_check_val.binder, i_check_val.innerTy, w.expr, TExpr.bool_(true, Ty.bool_), Ty.bool_), w.ctx)); + case None => + return None; + } +} + +method ruleIfAndOptional(s: TStmt, ctx: CondCtx) returns (res: Option) + ensures res.Some? ==> !res.value.stmt.let? + ensures res.None? ==> !FIfAndOptional(s) + ensures res.Some? ==> FIfAndOptional(s) + ensures res.Some? ==> res.value.stmt.someMatch? + requires AWS(s, ctx.decls) == WS(s, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWS(res.value.stmt, ctx.decls) == 0 + decreases AWS(s, ctx.decls), SS(s), 1 +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|i_s_else| != 0) { + return None; + } + var extracted := leadingPresent(i_s_cond); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerIf := if_(restCond, i_s_then, []); + assert s == TStmt.if_(i_s_cond, i_s_then, i_s_else); + leadingPresentShrinks(i_s_cond); + leadingPresentCheckWeightless(i_s_cond); + assert AWS(innerIf, ctx.decls) < AWS(s, ctx.decls); + var i_t105 := walkStmt(innerIf, ctx); + var w := i_t105; + assert !w.stmt.let?; + assert AWSs([w.stmt], ctx.decls) == HC(w.stmt, [], ctx.decls) + AWS(w.stmt, ctx.decls); + assert HC(w.stmt, [], ctx.decls) == 0; + assert AWSs([w.stmt], ctx.decls) == 0; + if check.truthiness && FalsyCapable(check.innerTy) { + ghost var gate := TStmt.if_(TExpr.var_(check.binder, check.innerTy), [w.stmt], []); + leadingIsArrayDeclsOnly(gate.cond, ctx, CondCtx(ctx.decls, 0)); + assert AWE(gate.cond, ctx.decls) == 0; + assert WS(gate, ctx.decls) == 0; + assert HC(gate, [], ctx.decls) == 0; + assert AWS(gate, ctx.decls) == 0; + assert AWSs([gate], ctx.decls) == HC(gate, [], ctx.decls) + AWS(gate, ctx.decls); + assert AWSs([gate], ctx.decls) == 0; + } + assert AWS(presentMatchStmts(check, [w.stmt], []), ctx.decls) == 0; + return Some(StmtOut(presentMatchStmts(check, [w.stmt], []), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleExprStmtAndOptional(s: TStmt, ctx: CondCtx) returns (res: Option) + ensures res.Some? ==> !res.value.stmt.let? + ensures res.None? ==> !FExprStmtAndOptional(s) + ensures res.Some? ==> FExprStmtAndOptional(s) + ensures res.Some? ==> res.value.stmt.someMatch? + requires AWS(s, ctx.decls) == WS(s, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWS(res.value.stmt, ctx.decls) == 0 + decreases AWS(s, ctx.decls), SS(s), 1 +{ + match s { + case expr(i_s_expr) => + if ((!i_s_expr.binop?) || (i_s_expr.op != "&&")) { + return None; + } + var extracted := leadingPresent(i_s_expr); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerStmt := expr(restCond); + leadingPresentShrinks(i_s_expr); + leadingPresentCheckWeightless(i_s_expr); + assert AWS(innerStmt, ctx.decls) < AWS(s, ctx.decls); + var i_t106 := walkStmt(innerStmt, ctx); + var w := i_t106; + assert !w.stmt.let?; + assert AWSs([w.stmt], ctx.decls) == HC(w.stmt, [], ctx.decls) + AWS(w.stmt, ctx.decls); + assert HC(w.stmt, [], ctx.decls) == 0; + assert AWSs([w.stmt], ctx.decls) == 0; + if check.truthiness && FalsyCapable(check.innerTy) { + ghost var gate := TStmt.if_(TExpr.var_(check.binder, check.innerTy), [w.stmt], []); + leadingIsArrayDeclsOnly(gate.cond, ctx, CondCtx(ctx.decls, 0)); + assert AWE(gate.cond, ctx.decls) == 0; + assert WS(gate, ctx.decls) == 0; + assert HC(gate, [], ctx.decls) == 0; + assert AWS(gate, ctx.decls) == 0; + assert AWSs([gate], ctx.decls) == HC(gate, [], ctx.decls) + AWS(gate, ctx.decls); + assert AWSs([gate], ctx.decls) == 0; + } + assert AWS(presentMatchStmts(check, [w.stmt], []), ctx.decls) == 0; + return Some(StmtOut(presentMatchStmts(check, [w.stmt], []), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleConditionalAndOptional(e: TExpr, ctx: CondCtx) returns (res: Option) + ensures res.None? ==> !FCondAndOptional(e) + ensures res.Some? ==> FCondAndOptional(e) + ensures res.Some? ==> res.value.expr.someMatch? + ensures res.Some? ==> (containsMethodCall(e) ==> containsMethodCall(res.value.expr)) + ensures res.Some? ==> res.value.expr.ty == e.ty + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWE(res.value.expr, ctx.decls) == 0 + decreases AWE(e, ctx.decls), SE(e), 1 +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var extracted := leadingPresent(i_e_cond); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var i_t107 := containsMethodCall(restCond); + if i_t107 { + return None; + } + var innerCond := conditional(restCond, i_e_then, i_e_else, i_e_ty); + assert e == TExpr.conditional(i_e_cond, i_e_then, i_e_else, i_e_ty); + leadingPresentShrinks(i_e_cond); + leadingPresentCheckWeightless(i_e_cond); + assert AWE(i_e_then, ctx.decls) == 0 && AWE(i_e_else, ctx.decls) == 0 && AWE(i_e_cond, ctx.decls) == 0; + assert AWE(restCond, ctx.decls) == 0; + assert PC(restCond) < PC(i_e_cond); + assert AC(restCond, ctx.decls) <= AC(i_e_cond, ctx.decls); + assert FCondAndOptional(e); + assert WE(e, ctx.decls) >= 2 + 2*PC(i_e_cond) + 2*AC(i_e_cond, ctx.decls); + WEConditionalBound(restCond, i_e_then, i_e_else, i_e_ty, ctx.decls); + assert AWE(innerCond, ctx.decls) == WE(innerCond, ctx.decls); + // Not strict: the residual drops one presence conjunct, and the + // flat in-map/truthy charge can reappear. Size decides. + assert AWE(innerCond, ctx.decls) <= AWE(e, ctx.decls); + assert SE(innerCond) < SE(e); + leadingPresentMethodCalls(i_e_cond); + assert containsMethodCall(e) ==> containsMethodCall(innerCond); + var i_t108 := walkExpr(innerCond, ctx); + var w := i_t108; + assert AWE(i_e_else, ctx.decls) == 0; + if check.truthiness && FalsyCapable(check.innerTy) { + ghost var gcond := TExpr.var_(check.binder, check.innerTy); + isArrayFactShape(gcond, CondCtx(ctx.decls, 0)); + typeofStringFactShape(gcond, CondCtx(ctx.decls, 0)); + ghost var gate := TExpr.conditional(gcond, w.expr, i_e_else, i_e_ty); + assert AWE(gcond, ctx.decls) == 0; + assert WE(gate, ctx.decls) == 0; + assert AWE(gate, ctx.decls) == 0; + } + assert AWE(presentMatchExpr(check, w.expr, i_e_else, i_e_ty), ctx.decls) == 0; + return Some(ExprOut(presentMatchExpr(check, w.expr, i_e_else, i_e_ty), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleImplArrayIsArray(e: TExpr, ctx: CondCtx) returns (res: Option) + ensures res.None? ==> !FImplArrayIsArray(e, ctx.decls) + ensures res.Some? ==> FImplArrayIsArray(e, ctx.decls) + ensures res.Some? ==> res.value.expr.tagMatch? + ensures res.Some? ==> (containsMethodCall(e) ==> containsMethodCall(res.value.expr)) + ensures res.Some? ==> res.value.expr.ty.bool_? + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWE(res.value.expr, ctx.decls) == 0 + decreases AWE(e, ctx.decls), SE(e), 1 +{ + if e.binop? && e.op == "==>" { + isArrayFactDeclsOnly(e.left, ctx, CondCtx(ctx.decls, 0)); + if e.left.unop? { isArrayFactDeclsOnly(e.left.expr, ctx, CondCtx(ctx.decls, 0)); } + } + if ((!e.binop?) || (e.op != "==>")) { + return None; + } + var pos := isArrayFact(e.left, ctx); + var neg := (if (e.left.unop? && (e.left.op == "!")) then isArrayFact(e.left.expr, ctx) else Option.None); + var matched := (match pos { case Some(i_oc19_val) => Option.Some(i_oc19_val) case None => (match neg { case Some(i_neg_val) => Option.Some(IsArrayFact(i_neg_val.scrutinee, i_neg_val.typeName, "NonArrayBranch")) case None => Option.None }) }); + match matched { + case Some(i_matched_val) => + assert FImplArrayIsArray(e, ctx.decls); + if pos.Some? { isArrayFactScrutineeWeightless(e.left, ctx); } else { isArrayFactScrutineeWeightless(e.left.expr, ctx); } + assert AWE(e.right, ctx.decls) < AWE(e, ctx.decls); + var i_t109 := walkExpr(e.right, ctx); + var w := i_t109; + assert !containsMethodCall(e.left); + assert TExprCase(i_matched_val.variant, w.expr) in [TExprCase(i_matched_val.variant, w.expr)]; + assert AWECs([TExprCase(i_matched_val.variant, w.expr)], ctx.decls) == AWE(w.expr, ctx.decls); + assert AWE(TExpr.tagMatch(i_matched_val.scrutinee, i_matched_val.typeName, [TExprCase(i_matched_val.variant, w.expr)], Some(TExpr.bool_(true, Ty.bool_)), Ty.bool_), ctx.decls) == 0; + return Some(ExprOut(TExpr.tagMatch(i_matched_val.scrutinee, i_matched_val.typeName, [TExprCase(i_matched_val.variant, w.expr)], Some(TExpr.bool_(true, Ty.bool_)), Ty.bool_), w.ctx)); + case None => + return None; + } +} + +method ruleConditionalArrayIsArray(e: TExpr, ctx: CondCtx) returns (res: Option) + ensures res.None? ==> !FCondArrayIsArray(e, ctx.decls) + ensures res.Some? ==> FCondArrayIsArray(e, ctx.decls) + ensures res.Some? ==> res.value.expr.tagMatch? + ensures res.Some? ==> (containsMethodCall(e) ==> containsMethodCall(res.value.expr)) + ensures res.Some? ==> res.value.expr.ty == e.ty + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWE(res.value.expr, ctx.decls) == 0 + decreases AWE(e, ctx.decls), SE(e), 1 +{ + if e.conditional? { + isArrayFactDeclsOnly(e.cond, ctx, CondCtx(ctx.decls, 0)); + typeofStringFactDeclsOnly(e.cond, ctx, CondCtx(ctx.decls, 0)); + if e.cond.unop? { isArrayFactDeclsOnly(e.cond.expr, ctx, CondCtx(ctx.decls, 0)); } + } + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var pos := isArrayFact(i_e_cond, ctx); + var tof := None; + if (match pos { case Some(i_) => false case None => true }) { + tof := typeofStringFact(i_e_cond, ctx); + } + var neg := None; + if (match pos { case Some(i_) => false case None => true }) { + if (match tof { case Some(i_) => false case None => true }) { + if (i_e_cond.unop? && (i_e_cond.op == "!")) { + neg := isArrayFact(i_e_cond.expr, ctx); + } + } + } + var matched := (match (match pos { case Some(i_oc20_val) => Option.Some(i_oc20_val) case None => tof }) { case Some(i_oc21_val) => Option.Some(i_oc21_val) case None => (match neg { case Some(i_neg_val) => Option.Some(IsArrayFact(i_neg_val.scrutinee, i_neg_val.typeName, "NonArrayBranch")) case None => Option.None }) }); + match matched { + case Some(i_matched_val) => + var positive := (match pos { case Some(i_oc22_val) => Option.Some(i_oc22_val) case None => tof }); + var thenBody := (match positive { case Some(i_positive_val) => i_e_then case None => i_e_else }); + var elseBody := (match positive { case Some(i_positive_val) => i_e_else case None => i_e_then }); + assert e == TExpr.conditional(i_e_cond, i_e_then, i_e_else, i_e_ty); + assert FCondArrayIsArray(e, ctx.decls); + if pos.Some? { isArrayFactScrutineeWeightless(i_e_cond, ctx); } + else if tof.Some? { typeofStringFactScrutineeWeightless(i_e_cond, ctx); } + else { isArrayFactScrutineeWeightless(i_e_cond.expr, ctx); } + assert AWE(thenBody, ctx.decls) < AWE(e, ctx.decls); + assert AWE(elseBody, ctx.decls) < AWE(e, ctx.decls); + var i_t110 := walkExpr(thenBody, ctx); + var wThen := i_t110; + var i_t111 := walkExpr(elseBody, wThen.ctx); + var wElse := i_t111; + assert !containsMethodCall(i_e_cond); + assert TExprCase(i_matched_val.variant, wThen.expr) in [TExprCase(i_matched_val.variant, wThen.expr)]; + assert AWECs([TExprCase(i_matched_val.variant, wThen.expr)], ctx.decls) == AWE(wThen.expr, ctx.decls); + assert AWE(TExpr.tagMatch(i_matched_val.scrutinee, i_matched_val.typeName, [TExprCase(i_matched_val.variant, wThen.expr)], Some(wElse.expr), i_e_ty), ctx.decls) == 0; + return Some(ExprOut(TExpr.tagMatch(i_matched_val.scrutinee, i_matched_val.typeName, [TExprCase(i_matched_val.variant, wThen.expr)], Some(wElse.expr), i_e_ty), wElse.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleIfAndArrayIsArray(s: TStmt, ctx: CondCtx) returns (res: Option) + ensures res.Some? ==> !res.value.stmt.let? + ensures res.None? ==> !FIfAndArrayIsArray(s, ctx.decls) + ensures res.Some? ==> FIfAndArrayIsArray(s, ctx.decls) + ensures res.Some? ==> res.value.stmt.tagMatch? + requires AWS(s, ctx.decls) == WS(s, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWS(res.value.stmt, ctx.decls) == 0 + decreases AWS(s, ctx.decls), SS(s), 1 +{ + if s.if_? { + leadingIsArrayDeclsOnly(s.cond, ctx, CondCtx(ctx.decls, 0)); + } + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var extracted := leadingIsArray(i_s_cond, ctx); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerIf := if_(restCond, i_s_then, i_s_else); + assert s == TStmt.if_(i_s_cond, i_s_then, i_s_else); + leadingIsArrayShrinks(i_s_cond, ctx); + leadingIsArrayCheckWeightless(i_s_cond, ctx); + assert AWS(innerIf, ctx.decls) < AWS(s, ctx.decls); + var i_t112 := walkStmt(innerIf, ctx); + var w := i_t112; + assert AWSs(i_s_else, ctx.decls) == 0; + assert !w.stmt.let?; + assert AWSs([w.stmt], ctx.decls) == 0; + assert AWSCs([TStmtCase(check.variant, [w.stmt])], ctx.decls) == AWSs([w.stmt], ctx.decls); + assert AWS(TStmt.tagMatch(check.scrutinee, check.typeName, [TStmtCase(check.variant, [w.stmt])], i_s_else), ctx.decls) == 0; + return Some(StmtOut(TStmt.tagMatch(check.scrutinee, check.typeName, [TStmtCase(check.variant, [w.stmt])], i_s_else), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleConditionalAndArrayIsArray(e: TExpr, ctx: CondCtx) returns (res: Option) + ensures res.None? ==> !FCondAndArrayIsArray(e, ctx.decls) + ensures res.Some? ==> FCondAndArrayIsArray(e, ctx.decls) + ensures res.Some? ==> res.value.expr.tagMatch? + ensures res.Some? ==> (containsMethodCall(e) ==> containsMethodCall(res.value.expr)) + ensures res.Some? ==> res.value.expr.ty == e.ty + requires AWE(e, ctx.decls) == WE(e, ctx.decls) + ensures res.Some? ==> res.value.ctx.decls == ctx.decls + ensures res.Some? ==> AWE(res.value.expr, ctx.decls) == 0 + decreases AWE(e, ctx.decls), SE(e), 1 +{ + if e.conditional? { + leadingIsArrayDeclsOnly(e.cond, ctx, CondCtx(ctx.decls, 0)); + } + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var extracted := leadingIsArray(i_e_cond, ctx); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerCond := conditional(restCond, i_e_then, i_e_else, i_e_ty); + assert e == TExpr.conditional(i_e_cond, i_e_then, i_e_else, i_e_ty); + leadingIsArrayShrinks(i_e_cond, ctx); + leadingIsArrayCheckWeightless(i_e_cond, ctx); + assert AWE(i_e_then, ctx.decls) == 0 && AWE(i_e_else, ctx.decls) == 0 && AWE(i_e_cond, ctx.decls) == 0; + assert AWE(restCond, ctx.decls) == 0; + assert PC(restCond) <= PC(i_e_cond); + assert AC(restCond, ctx.decls) < AC(i_e_cond, ctx.decls); + leadingIsArrayDeclsOnly(i_e_cond, ctx, CondCtx(ctx.decls, 0)); + assert FCondAndArrayIsArray(e, ctx.decls); + assert WE(e, ctx.decls) >= 2 + 2*PC(i_e_cond) + 2*AC(i_e_cond, ctx.decls); + WEConditionalBound(restCond, i_e_then, i_e_else, i_e_ty, ctx.decls); + assert AWE(innerCond, ctx.decls) == WE(innerCond, ctx.decls); + // Not strict: the residual drops one isArray conjunct, and the + // flat in-map/truthy charge can reappear. Size decides. + assert AWE(innerCond, ctx.decls) <= AWE(e, ctx.decls); + assert SE(innerCond) < SE(e); + leadingIsArrayMethodCalls(i_e_cond, ctx); + assert containsMethodCall(e) ==> containsMethodCall(innerCond); + var i_t113 := walkExpr(innerCond, ctx); + var w := i_t113; + assert AWE(i_e_else, ctx.decls) == 0; + assert TExprCase(check.variant, w.expr) in [TExprCase(check.variant, w.expr)]; + assert AWECs([TExprCase(check.variant, w.expr)], ctx.decls) == AWE(w.expr, ctx.decls); + assert AWE(TExpr.tagMatch(check.scrutinee, check.typeName, [TExprCase(check.variant, w.expr)], Some(i_e_else), i_e_ty), ctx.decls) == 0; + return Some(ExprOut(TExpr.tagMatch(check.scrutinee, check.typeName, [TExprCase(check.variant, w.expr)], Some(i_e_else), i_e_ty), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleDiscriminantChain(stmts: seq, ctx: CondCtx) returns (res: Option) + ensures (match res { case Some(i_result_val) => ((i_result_val.consumed >= 0) && (i_result_val.consumed <= |stmts|)) case None => true }) + ensures res.Some? ==> res.value.consumed >= 1 + ensures res.Some? ==> AWS(res.value.stmt, ctx.decls) <= AWSs(stmts, ctx.decls) + ensures res.Some? ==> SS(res.value.stmt) < SSs(stmts) + ensures res.Some? ==> SS(res.value.stmt) + 1 <= SSs(stmts[..res.value.consumed]) + ensures res.Some? ==> res.value.stmt.tagMatch? +{ + if ((|stmts| == 0) || (!stmts[0].if_?)) { + return None; + } + var first := variantFact(stmts[0].cond, ctx); + match first { + case Some(i_first_val) => + var cases: seq := []; + var consumed := 0; + var i := 0; + while (i < |stmts|) + invariant (consumed >= 0) + invariant (consumed <= |stmts|) + invariant (i <= |stmts|) + invariant consumed == i + invariant |cases| == i + invariant AWSCs(cases, ctx.decls) <= AWSs(stmts[..i], ctx.decls) + invariant SSCs(cases) + 3*i <= SSs(stmts[..i]) + { + var s := stmts[i]; + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var p := variantFact(i_s_cond, ctx); + match p { + case Some(i_p_val) => + if (i_p_val.scrutineeName != i_first_val.scrutineeName) { + break; + } + assert SE(i_s_cond) >= 2; + assert stmts[..i+1] == stmts[..i] + [s]; + assert stmts[..i+1][..i] == stmts[..i] && stmts[..i+1][i..] == [s]; + AWSsPrefixSuffix(stmts[..i+1], i, ctx.decls); + SSsSplit(stmts[..i+1], i); + AWSCsConcat(cases, [TStmtCase(i_p_val.variant, i_s_then)], ctx.decls); + SSCsConcat(cases, [TStmtCase(i_p_val.variant, i_s_then)]); + cases := (cases + [TStmtCase(i_p_val.variant, i_s_then)]); + consumed := (i + 1); + if (|i_s_else| > 0) { + var i_t114 := collectElseChain(i_s_else[0], i_first_val.scrutineeName, ctx); + var tail := (if ((|i_s_else| == 1) && i_s_else[0].if_?) then i_t114 else ElseChain([], i_s_else)); + AWSCsConcat(cases, tail.cases, ctx.decls); + SSCsConcat(cases, tail.cases); + AWSsPrefixSuffix(stmts, i, ctx.decls); + SSsSplit(stmts, i); + assert stmts[i..] == [s] + stmts[i+1..]; + assert SSs(stmts[i..]) == 1 + SS(s) + SSs(stmts[i+1..]); + assert AWSs(stmts[i..], ctx.decls) == HC(s, stmts[i+1..], ctx.decls) + AWS(s, ctx.decls) + AWSs(stmts[i+1..], ctx.decls); + assert i_s_else == [i_s_else[0]] + i_s_else[1..]; + assert SSs(i_s_else) == 1 + SS(i_s_else[0]) + SSs(i_s_else[1..]); + assert AWSs(i_s_else, ctx.decls) == HC(i_s_else[0], i_s_else[1..], ctx.decls) + AWS(i_s_else[0], ctx.decls) + AWSs(i_s_else[1..], ctx.decls); + assert SSCs(tail.cases) + SSs(tail.fallthrough) <= 1 + SS(i_s_else[0]) + SSs(i_s_else[1..]); + assert AWSCs(tail.cases, ctx.decls) + AWSs(tail.fallthrough, ctx.decls) <= AWSs(i_s_else, ctx.decls); + assert s == TStmt.if_(i_s_cond, i_s_then, i_s_else); + assert AWS(s, ctx.decls) == WS(s, ctx.decls) + AWE(i_s_cond, ctx.decls) + AWSs(i_s_then, ctx.decls) + AWSs(i_s_else, ctx.decls); + assert SS(s) == 1 + SE(i_s_cond) + SSs(i_s_then) + SSs(i_s_else); + assert AWE(i_first_val.scrutinee, ctx.decls) == 0; + ghost var outCases := cases + tail.cases; + ghost var outStmt := TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, outCases, tail.fallthrough); + assert AWS(outStmt, ctx.decls) == AWE(i_first_val.scrutinee, ctx.decls) + AWSCs(outCases, ctx.decls) + AWSs(tail.fallthrough, ctx.decls); + assert SS(outStmt) == 1 + SE(i_first_val.scrutinee) + SSCs(outCases) + SSs(tail.fallthrough); + assert SE(i_first_val.scrutinee) == 1; + assert AWS(outStmt, ctx.decls) <= AWSs(stmts, ctx.decls); + assert SS(outStmt) < SSs(stmts); + return Some(ConsumedRewrite(TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, (cases + tail.cases), tail.fallthrough), consumed)); + } + i := (i + 1); + case None => + break; + } + case _ => + break; + } + } + if (|cases| == 0) { + return None; + } + AWSsPrefixSuffix(stmts, consumed, ctx.decls); + SSsSplit(stmts, consumed); + var i_t115 := allCasesTerminate(cases); + assert stmts[..|stmts|] == stmts; + assert SS(TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, cases, [])) + == 1 + SE(i_first_val.scrutinee) + SSCs(cases) + SSs([]); + assert SE(i_first_val.scrutinee) == 1; + assert consumed >= 1; + if i_t115 { + return Some(ConsumedRewrite(TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, cases, stmts[consumed..]), |stmts|)); + } + return Some(ConsumedRewrite(TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, cases, []), consumed)); + case None => + return None; + } +} + +method narrowFunction(fn: TFunction, ctx: CondCtx) returns (res: FunctionOut) +{ + var i_t116 := walkExprs(fn.requires_, ctx); + var reqs := i_t116; + var i_t117 := walkExprs(fn.ensures_, reqs.ctx); + var enss := i_t117; + var dec := None; + match fn.decreases_ { + case Some(i_fn_decreases_val) => + var i_t118 := walkExpr(i_fn_decreases_val, enss.ctx); + dec := Some(i_t118); + case None => + + } + var i_t119 := walkStmts(fn.body, (match dec { case Some(i_dec_val) => i_dec_val.ctx case None => enss.ctx })); + var body := i_t119; + return FunctionOut(fn.(requires_ := reqs.exprs, ensures_ := enss.exprs, decreases_ := (match dec { case Some(i_dec_val) => Option.Some(i_dec_val.expr) case None => Option.None }), body := body.stmts), body.ctx); +} + +method narrowConstants(constants: seq, ctx: CondCtx) returns (res: ConstantsOut) +{ + if (|constants| == 0) { + return ConstantsOut([], ctx); + } + var i_t120 := walkExpr(constants[0].value, ctx); + var v := i_t120; + var i_t121 := narrowConstants(constants[1..], v.ctx); + var rest := i_t121; + return ConstantsOut(([constants[0].(value := v.expr)] + rest.constants), rest.ctx); +} + +method narrowFunctions(fns: seq, ctx: CondCtx) returns (res: FunctionsOut) +{ + if (|fns| == 0) { + return FunctionsOut([], ctx); + } + var i_t122 := narrowFunction(fns[0], ctx); + var f := i_t122; + var i_t123 := narrowFunctions(fns[1..], f.ctx); + var rest := i_t123; + return FunctionsOut(([f.fn] + rest.functions), rest.ctx); +} + +method narrowClasses(classes: seq, ctx: CondCtx) returns (res: ClassesOut) +{ + if (|classes| == 0) { + return ClassesOut([], ctx); + } + var i_t124 := narrowFunctions(classes[0].methods, ctx); + var methods := i_t124; + var i_t125 := narrowClasses(classes[1..], methods.ctx); + var rest := i_t125; + return ClassesOut(([classes[0].(methods := methods.functions)] + rest.classes), rest.ctx); +} + +method narrowModule(mod: TModule) returns (res: TModule) +{ + var ctx := CondCtx(mod.typeDecls, 0); + var i_t126 := narrowConstants(mod.constants, ctx); + var consts := i_t126; + var i_t127 := narrowFunctions(mod.functions, consts.ctx); + var fns := i_t127; + var i_t128 := narrowClasses(mod.classes, fns.ctx); + var classes := i_t128; + return mod.(constants := consts.constants, functions := fns.functions, classes := classes.classes); +} diff --git a/tools/src/narrow.dfy.gen b/tools/src/narrow.dfy.gen new file mode 100644 index 0000000..eb0f93e --- /dev/null +++ b/tools/src/narrow.dfy.gen @@ -0,0 +1,1408 @@ +// Generated by lsc from narrow.ts + +datatype Option = None | Some(value: T) + +function {:axiom} isTerminatorKind(kind: string): bool + +function {:axiom} presentFact(cond: TExpr): Option + +function {:axiom} freshName(base: string): string + +function {:axiom} presentMatchStmts(f: PresentFact, some: seq, none: seq): TStmt + +function {:axiom} flattenOr(e: TExpr): seq + +function {:axiom} noneDetector(leaf: TExpr, ctx: CondCtx): Option + +function {:axiom} binderHintFor(e: TExpr): Option + +function {:axiom} restoreDiscriminantFlag(unwrapped: TExpr, decls: TypeDecls): TExpr + +function {:axiom} discriminantOf(decls: TypeDecls, ty: Ty): Option + +function {:axiom} applyChain(body: TExpr, chain: seq): TExpr + +function {:axiom} presentMatchExpr(f: PresentFact, some: TExpr, none: TExpr, ty: Ty): TExpr + +function {:axiom} leadingPresent(cond: TExpr): Option + +function {:axiom} freshOcBinder(ctx: CondCtx): MintedBinder + +function {:axiom} arrayBoundsCond(arr: TExpr, idx: TExpr): TExpr + +function {:axiom} exprEqual(a: TExpr, b: TExpr): bool + +function {:axiom} binderHintForMapAccess(m: TExpr, k: TExpr, ctx: CondCtx): MintedBinder + +function {:axiom} builtinPure(id: string): bool + +function {:axiom} isArrayFact(call: TExpr, ctx: CondCtx): Option + +function {:axiom} unionDeclOfTy(decls: TypeDecls, ty: Ty): Option + +function {:axiom} typeofStringFact(e: TExpr, ctx: CondCtx): Option + +function {:axiom} leadingIsArray(cond: TExpr, ctx: CondCtx): Option + +function {:axiom} variantFact(cond: TExpr, ctx: CondCtx): Option + +function {:axiom} negVariantFact(cond: TExpr, ctx: CondCtx): Option + +datatype ExprOut = ExprOut(expr: TExpr, ctx: CondCtx) + +datatype ExprsOut = ExprsOut(exprs: seq, ctx: CondCtx) + +datatype StmtOut = StmtOut(stmt: TStmt, ctx: CondCtx) + +datatype StmtsOut = StmtsOut(stmts: seq, ctx: CondCtx) + +datatype StepsOut = StepsOut(steps: seq, ctx: CondCtx) + +datatype RecordFieldsOut = RecordFieldsOut(fields: seq, ctx: CondCtx) + +datatype ExprCasesOut = ExprCasesOut(cases: seq, ctx: CondCtx) + +datatype StmtCasesOut = StmtCasesOut(cases: seq, ctx: CondCtx) + +datatype SwitchCasesOut = SwitchCasesOut(cases: seq, ctx: CondCtx) + +datatype ElseChain = ElseChain(cases: seq, fallthrough: seq) + +datatype ConsumedRewrite = ConsumedRewrite(stmt: TStmt, consumed: int) + +datatype FunctionOut = FunctionOut(fn: TFunction, ctx: CondCtx) + +datatype ConstantsOut = ConstantsOut(constants: seq, ctx: CondCtx) + +datatype FunctionsOut = FunctionsOut(functions: seq, ctx: CondCtx) + +datatype ClassesOut = ClassesOut(classes: seq, ctx: CondCtx) + +datatype TExpr = var_(name: string, ty: Ty) | num(value_num: int, ty: Ty) | bigint(value_bigint: string, ty: Ty) | str(value_str: string, ty: Ty) | bool_(value_bool: bool, ty: Ty) | binop(op: string, left: TExpr, right: TExpr, ty: Ty) | unop(op: string, expr: TExpr, ty: Ty) | call(fn: TExpr, args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(obj: TExpr, idx: TExpr, ty: Ty) | field(obj: TExpr, field: string, ty: Ty, isDiscriminant: Option, ofVariant: Option) | record(spread: Option, fields: seq, ty: Ty) | arrayLiteral(elems: seq, ty: Ty) | lambda(params: seq, body_lambda: seq, ty: Ty) | conditional(cond: TExpr, then_: TExpr, else_: TExpr, ty: Ty) | optChain(obj: TExpr, chain: seq, ty: Ty) | nullish(left: TExpr, right: TExpr, ty: Ty) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: TExpr, noneBody: TExpr, ty: Ty) | tagMatch(scrutinee: TExpr, typeName: string, cases: seq, fallthrough: Option, ty: Ty) | forall_(var_: string, varTy: Ty, body_forall: TExpr, ty: Ty) | exists_(var_: string, varTy: Ty, body_exists: TExpr, ty: Ty) | havoc(ty: Ty) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +datatype CallKind = unknown | pure | method_ | spec_pure + +datatype TParam = TParam(name: string, ty: Ty) + +datatype TStmt = let(name: string, ty: Ty, mutable: bool, init: TExpr) | assign(target: string, value: TExpr) | return_(value: TExpr) | break_ | continue_ | expr(expr: TExpr) | if_(cond: TExpr, then_: seq, else_: seq) | while_(cond: TExpr, invariants: seq, decreases_: Option, doneWith: Option, body: seq) | switch(expr: TExpr, discriminant: string, cases_switch: seq, defaultBody: seq) | forof(names: seq, nameTypes: seq, iterable: TExpr, invariants: seq, doneWith: Option, body: seq) | throw | ghostLet(name: string, ty: Ty, init: TExpr) | ghostAssign(target: string, value: TExpr) | assert_(expr: TExpr, assumed: Option) | someMatch(scrutinee: TExpr, binder: string, binderTy: Ty, someBody: seq, noneBody: seq) | tagMatch(scrutinee: TExpr, typeName: string, cases_tagMatch: seq, fallthrough: seq) + +datatype TSwitchCase = TSwitchCase(label_: string, body: seq) + +datatype TStmtCase = TStmtCase(variant: string, body: seq) + +datatype TChainStep = field(name: string, ty: Ty) | call(args: seq, ty: Ty, callKind: CallKind, builtinId: Option) | index(idx: TExpr, ty: Ty) + +datatype TExprCase = TExprCase(variant: string, body: TExpr) + +datatype TRecordField = TRecordField(name: string, value: TExpr) + +datatype CondCtx = CondCtx(decls: seq, ocN: int) + +type TypeDecls = seq + +datatype TypeDeclInfo = TypeDeclInfo(name: string, typeParams: Option>, kind: string, values: Option>, discriminant: Option, variants: Option>, fields: Option>, aliasOf: Option, aliasOfTy: Option) + +datatype VariantInfo = VariantInfo(name: string, fields: seq) + +datatype FieldInfo = FieldInfo(name: string, tsType: string, type_: Option) + +datatype TFunction = TFunction(name: string, typeParams: seq, params: seq, returnTy: Ty, requires_: seq, ensures_: seq, decreases_: Option, isPure: bool, forcePure: bool, autohavoc: bool, body: seq) + +datatype TConst = TConst(name: string, ty: Ty, value: TExpr) + +datatype TClass = TClass(name: string, fields: seq, methods: seq) + +datatype TModule = TModule(file: string, typeDecls: seq, externs: seq, constants: seq, functions: seq, classes: seq) + +datatype TExtern = TExtern(qualified: string, flat: string, typeParams: seq, params: seq, returnTy: Ty, requires_: seq, ensures_: seq) + +datatype PresentFact = PresentFact(scrutinee: TExpr, innerTy: Ty, negated: bool, binder: string, truthiness: bool) + +datatype NoneDetector = NoneDetector(scrutinee: TExpr, innerTy: Ty, binder: string, residual: Option) + +datatype LeadingPresent = LeadingPresent(check: PresentFact, restCond: TExpr) + +datatype MintedBinder = MintedBinder(name: string, ctx: CondCtx) + +datatype IsArrayFact = IsArrayFact(scrutinee: TExpr, typeName: string, variant: string) + +datatype LeadingIsArray = LeadingIsArray(check: IsArrayFact, restCond: TExpr) + +datatype VariantFact = VariantFact(scrutinee: TExpr, scrutineeName: string, typeName: string, variant: string) + +function TStmt_kind(t: TStmt): string +{ + match t { + case let(i_0, i_1, i_2, i_3) => + "let" + case assign(i_0, i_1) => + "assign" + case return_(i_0) => + "return" + case break_ => + "break" + case continue_ => + "continue" + case expr(i_0) => + "expr" + case if_(i_0, i_1, i_2) => + "if" + case while_(i_0, i_1, i_2, i_3, i_4) => + "while" + case switch(i_0, i_1, i_2, i_3) => + "switch" + case forof(i_0, i_1, i_2, i_3, i_4, i_5) => + "forof" + case throw => + "throw" + case ghostLet(i_0, i_1, i_2) => + "ghostLet" + case ghostAssign(i_0, i_1) => + "ghostAssign" + case assert_(i_0, i_1) => + "assert" + case someMatch(i_0, i_1, i_2, i_3, i_4) => + "someMatch" + case tagMatch(i_0, i_1, i_2, i_3) => + "tagMatch" + } +} + +function isTerminating(stmts: seq): bool +{ + if (|stmts| == 0) then + false + else + isTerminatorKind(TStmt_kind(stmts[(|stmts| - 1)])) +} + +function ruleIfOptionalSimple(s: TStmt, ctx: CondCtx): Option +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var check := presentFact(i_s_cond); + match check { + case Some(i_check_val) => + var someBody := (if i_check_val.negated then i_s_else else i_s_then); + var noneBody := (if i_check_val.negated then i_s_then else i_s_else); + if (|someBody| == 0) then + None + else + Some(StmtOut(presentMatchStmts(i_check_val, someBody, noneBody), ctx)) + case None => + None + } + case _ => + None + } +} + +function ruleEarlyReturnConsume(s: TStmt, rest: seq): Option +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|rest| == 0) then + None + else + var check := presentFact(i_s_cond); + match check { + case Some(i_check_val) => + var someBranch := (if i_check_val.negated then i_s_else else i_s_then); + var noneBranch := (if i_check_val.negated then i_s_then else i_s_else); + if (|someBranch| != 0) then + None + else + if !(isTerminating(noneBranch)) then + None + else + Some(presentMatchStmts(i_check_val, rest, noneBranch)) + case None => + None + } + case _ => + None + } +} + +function ruleEarlyReturnOptChainCompare(s: TStmt, rest: seq, ctx: CondCtx): Option +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|rest| == 0) then + None + else + if ((|i_s_else| != 0) || !(isTerminating(i_s_then))) then + None + else + var c := i_s_cond; + if ((!c.binop?) || (c.op != "!==")) then + None + else + var ocOnLeft := c.left.optChain?; + var oc := (if ocOnLeft then Option.Some(c.left) else (if c.right.optChain? then Option.Some(c.right) else Option.None)); + match oc { + case Some(i_oc_val) => + match i_oc_val { + case optChain(i__oc_val_obj, i__oc_val_chain, i__oc_val_ty) => + if (!i__oc_val_obj.ty.optional?) then + None + else + var lit := (if ocOnLeft then c.right else c.left); + var innerTy := i__oc_val_obj.ty.inner; + var hint := binderHintFor(i__oc_val_obj); + match hint { + case Some(i_hint_val) => + var binder := freshName(i_hint_val); + var binderVar := var_(binder, innerTy); + var unwrapped := restoreDiscriminantFlag(applyChain(binderVar, i__oc_val_chain), ctx.decls); + var innerGuard := binop("!==", unwrapped, lit, Ty.bool_); + var someBody := ([if_(innerGuard, i_s_then, [])] + rest); + Some(TStmt.someMatch(i__oc_val_obj, binder, innerTy, someBody, i_s_then)) + case None => + None + } + case _ => + None + } + case None => + None + } + case _ => + None + } +} + +function ruleConditionalOptionalSimple(e: TExpr, ctx: CondCtx): Option +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var check := presentFact(i_e_cond); + match check { + case Some(i_check_val) => + var someBody := (if i_check_val.negated then i_e_else else i_e_then); + var noneBody := (if i_check_val.negated then i_e_then else i_e_else); + Some(ExprOut(presentMatchExpr(i_check_val, someBody, noneBody, i_e_ty), ctx)) + case None => + None + } + case _ => + None + } +} + +function ruleNullish(e: TExpr, ctx: CondCtx): Option +{ + match e { + case nullish(i_e_left, i_e_right, i_e_ty) => + if (!i_e_left.ty.optional?) then + None + else + var innerTy := i_e_left.ty.inner; + var b := freshOcBinder(ctx); + Some(ExprOut(TExpr.someMatch(i_e_left, b.name, innerTy, var_(b.name, innerTy), i_e_right, i_e_ty), b.ctx)) + case _ => + None + } +} + +function ruleNullishIndex(e: TExpr, ctx: CondCtx): Option +{ + match e { + case nullish(i_e_left, i_e_right, i_e_ty) => + if (!i_e_left.index?) then + None + else + if (!i_e_left.obj.ty.array_?) then + None + else + var cond := arrayBoundsCond(i_e_left.obj, i_e_left.idx); + Some(ExprOut(conditional(cond, i_e_left, i_e_right, i_e_ty), ctx)) + case _ => + None + } +} + +function ruleOptChainIndex(e: TExpr, ctx: CondCtx): Option +{ + match e { + case optChain(i_e_obj, i_e_chain, i_e_ty) => + if (!i_e_obj.index?) then + None + else + if (!i_e_obj.obj.ty.array_?) then + None + else + var cond := arrayBoundsCond(i_e_obj.obj, i_e_obj.idx); + var body := applyChain(i_e_obj, i_e_chain); + var undef := var_("undefined", Ty.void); + Some(ExprOut(conditional(cond, body, undef, i_e_ty), ctx)) + case _ => + None + } +} + +function ruleOptionalIndexBinding(s: TStmt, ctx: CondCtx): Option +{ + match s { + case let(i_s_name, i_s_ty, i_s_mutable, i_s_init) => + if (!i_s_ty.optional?) then + None + else + var init := i_s_init; + match init { + case index(i_init_obj, i_init_idx, i_init_ty) => + if (!i_init_obj.ty.array_?) then + None + else + if i_init_ty.optional? then + None + else + var cond := arrayBoundsCond(i_init_obj, i_init_idx); + var undef := var_("undefined", Ty.void); + var guarded := conditional(cond, init, undef, i_s_ty); + Some(StmtOut(s.(init := guarded), ctx)) + case _ => + None + } + case _ => + None + } +} + +function ruleOptChain(e: TExpr, ctx: CondCtx): Option +{ + match e { + case optChain(i_e_obj, i_e_chain, i_e_ty) => + if (!i_e_obj.ty.optional?) then + None + else + var innerTy := i_e_obj.ty.inner; + var b := freshOcBinder(ctx); + var body := applyChain(var_(b.name, innerTy), i_e_chain); + var noneBody := var_("undefined", Ty.void); + Some(ExprOut(TExpr.someMatch(i_e_obj, b.name, innerTy, body, noneBody, i_e_ty), b.ctx)) + case _ => + None + } +} + +function ruleConditionalInMap(e: TExpr, ctx: CondCtx): Option +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + if ((!i_e_cond.binop?) || (i_e_cond.op != "in")) then + None + else + var m := i_e_cond.right; + var k := i_e_cond.left; + if (!m.ty.map_?) then + None + else + if (!i_e_then.index?) then + None + else + if (!(exprEqual(i_e_then.obj, m)) || !(exprEqual(i_e_then.idx, k))) then + None + else + if (i_e_else.ty.optional? || i_e_else.ty.void?) then + None + else + if (!i_e_then.ty.optional?) then + None + else + var innerTy := m.ty.value; + var b := binderHintForMapAccess(m, k, ctx); + Some(ExprOut(TExpr.someMatch(i_e_then, b.name, innerTy, var_(b.name, innerTy), i_e_else, innerTy), b.ctx)) + case _ => + None + } +} + +function ruleConditionalOptionalTruthy(e: TExpr, ctx: CondCtx): Option +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + if (!i_e_cond.ty.optional?) then + None + else + var hint := binderHintFor(i_e_cond); + match hint { + case Some(i_hint_val) => + var binder := freshName(i_hint_val); + Some(ExprOut(TExpr.someMatch(i_e_cond, binder, i_e_cond.ty.inner, i_e_then, i_e_else, i_e_ty), ctx)) + case None => + None + } + case _ => + None + } +} + +function ruleLetCondAndOptional(s: TStmt): Option> +{ + if ((!s.let?) || s.mutable) then + None + else + if (!s.init.conditional?) then + None + else + var extracted := leadingPresent(s.init.cond); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var assignIf := if_(restCond, [assign(s.name, s.init.then_)], []); + Some([let(s.name, s.ty, true, s.init.else_), presentMatchStmts(check, [assignIf], [])]) + case None => + None + } +} + +function containsMethodCall(e: TExpr): bool +{ + if (e.call? && e.callKind.method_?) then + var bid := e.builtinId; + match bid { + case Some(i_bid_val) => + (!(builtinPure(i_bid_val)) || (match e { case var_(i_e_name, i_e_ty) => false case num(i_e_value, i_e_ty) => false case bigint(i_e_value, i_e_ty) => false case str(i_e_value, i_e_ty) => false case bool_(i_e_value, i_e_ty) => false case havoc(i_e_ty) => false case binop(i_e_op, i_e_left, i_e_right, i_e_ty) => (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) case unop(i_e_op, i_e_expr, i_e_ty) => containsMethodCall(i_e_expr) case call(i_e_fn, i_e_args, i_e_ty, i_e_callKind, i_e_builtinId) => (containsMethodCall(i_e_fn) || (exists x :: x in i_e_args && containsMethodCall(x))) case index(i_e_obj, i_e_idx, i_e_ty) => (containsMethodCall(i_e_obj) || containsMethodCall(i_e_idx)) case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => containsMethodCall(i_e_obj) case record(i_e_spread, i_e_fields, i_e_ty) => ((match i_e_spread { case Some(i_e_spread_val) => containsMethodCall(i_e_spread_val) case None => false }) || (exists f :: f in i_e_fields && containsMethodCall(f.value))) case arrayLiteral(i_e_elems, i_e_ty) => (exists x :: x in i_e_elems && containsMethodCall(x)) case lambda(i_e_params, i_e_body, i_e_ty) => false case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => ((containsMethodCall(i_e_cond) || containsMethodCall(i_e_then)) || containsMethodCall(i_e_else)) case optChain(i_e_obj, i_e_chain, i_e_ty) => containsMethodCall(i_e_obj) case nullish(i_e_left, i_e_right, i_e_ty) => (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) case forall_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => containsMethodCall(i_e_body) case exists_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => containsMethodCall(i_e_body) case someMatch(i_e_scrutinee, i_e_binder, i_e_binderTy, i_e_someBody, i_e_noneBody, i_e_ty) => ((containsMethodCall(i_e_scrutinee) || containsMethodCall(i_e_someBody)) || containsMethodCall(i_e_noneBody)) case tagMatch(i_e_scrutinee, i_e_typeName, i_e_cases, i_e_fallthrough, i_e_ty) => ((containsMethodCall(i_e_scrutinee) || (exists c :: c in i_e_cases && containsMethodCall(c.body))) || (match i_e_fallthrough { case Some(i_e_fallthrough_val) => containsMethodCall(i_e_fallthrough_val) case None => false })) })) + case None => + true + } + else + match e { + case var_(i_e_name, i_e_ty) => + false + case num(i_e_value, i_e_ty) => + false + case bigint(i_e_value, i_e_ty) => + false + case str(i_e_value, i_e_ty) => + false + case bool_(i_e_value, i_e_ty) => + false + case havoc(i_e_ty) => + false + case binop(i_e_op, i_e_left, i_e_right, i_e_ty) => + (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) + case unop(i_e_op, i_e_expr, i_e_ty) => + containsMethodCall(i_e_expr) + case call(i_e_fn, i_e_args, i_e_ty, i_e_callKind, i_e_builtinId) => + (containsMethodCall(i_e_fn) || (exists x :: x in i_e_args && containsMethodCall(x))) + case index(i_e_obj, i_e_idx, i_e_ty) => + (containsMethodCall(i_e_obj) || containsMethodCall(i_e_idx)) + case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => + containsMethodCall(i_e_obj) + case record(i_e_spread, i_e_fields, i_e_ty) => + ((match i_e_spread { case Some(i_e_spread_val) => containsMethodCall(i_e_spread_val) case None => false }) || (exists f :: f in i_e_fields && containsMethodCall(f.value))) + case arrayLiteral(i_e_elems, i_e_ty) => + (exists x :: x in i_e_elems && containsMethodCall(x)) + case lambda(i_e_params, i_e_body, i_e_ty) => + false + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + ((containsMethodCall(i_e_cond) || containsMethodCall(i_e_then)) || containsMethodCall(i_e_else)) + case optChain(i_e_obj, i_e_chain, i_e_ty) => + containsMethodCall(i_e_obj) + case nullish(i_e_left, i_e_right, i_e_ty) => + (containsMethodCall(i_e_left) || containsMethodCall(i_e_right)) + case forall_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + containsMethodCall(i_e_body) + case exists_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + containsMethodCall(i_e_body) + case someMatch(i_e_scrutinee, i_e_binder, i_e_binderTy, i_e_someBody, i_e_noneBody, i_e_ty) => + ((containsMethodCall(i_e_scrutinee) || containsMethodCall(i_e_someBody)) || containsMethodCall(i_e_noneBody)) + case tagMatch(i_e_scrutinee, i_e_typeName, i_e_cases, i_e_fallthrough, i_e_ty) => + ((containsMethodCall(i_e_scrutinee) || (exists c :: c in i_e_cases && containsMethodCall(c.body))) || (match i_e_fallthrough { case Some(i_e_fallthrough_val) => containsMethodCall(i_e_fallthrough_val) case None => false })) + } +} + +function collectElseChain(s: TStmt, scrutineeName: string, ctx: CondCtx): ElseChain +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var p := variantFact(i_s_cond, ctx); + match p { + case Some(i_p_val) => + if (i_p_val.scrutineeName != scrutineeName) then + ElseChain([], [s]) + else + var here := TStmtCase(i_p_val.variant, i_s_then); + if (|i_s_else| == 0) then + ElseChain([here], []) + else + if ((|i_s_else| == 1) && i_s_else[0].if_?) then + var rest := collectElseChain(i_s_else[0], scrutineeName, ctx); + ElseChain(([here] + rest.cases), rest.fallthrough) + else + ElseChain([here], i_s_else) + case None => + ElseChain([], [s]) + } + case _ => + ElseChain([], [s]) + } +} + +function allCasesTerminate(cases: seq): bool +{ + ((|cases| == 0) || (if !(isTerminating(cases[0].body)) then false else allCasesTerminate(cases[1..]))) +} + +function ruleDiscriminantNegEarlyReturn(stmts: seq, ctx: CondCtx): Option +{ + if (|stmts| < 2) then + None + else + var first := stmts[0]; + if ((!first.if_?) || (|first.else_| > 0)) then + None + else + if !(isTerminating(first.then_)) then + None + else + var cond := negVariantFact(first.cond, ctx); + match cond { + case Some(i_cond_val) => + Some(ConsumedRewrite(TStmt.tagMatch(i_cond_val.scrutinee, i_cond_val.typeName, [TStmtCase(i_cond_val.variant, stmts[1..])], first.then_), |stmts|)) + case None => + None + } +} + +lemma ruleDiscriminantNegEarlyReturn_ensures(stmts: seq, ctx: CondCtx) + ensures (match ruleDiscriminantNegEarlyReturn(stmts, ctx) { case Some(i_result_val) => ((i_result_val.consumed >= 0) && (i_result_val.consumed <= |stmts|)) case None => true }) +{ +} + +method walkExpr(e: TExpr, ctx: CondCtx) returns (res: ExprOut) +{ + var i_t0 := recurseExpr(e, ctx); + var r := i_t0; + var i_t1 := ruleNullish(r.expr, r.ctx); + var i_t2 := ruleNullishIndex(r.expr, r.ctx); + var i_t3 := ruleOptChainIndex(r.expr, r.ctx); + var i_t4 := ruleOptChain(r.expr, r.ctx); + var i_t5 := ruleImplOptional(r.expr, r.ctx); + var i_t6 := ruleImplArrayIsArray(r.expr, r.ctx); + var i_t7 := ruleConditionalArrayIsArray(r.expr, r.ctx); + var i_t8 := ruleConditionalAndArrayIsArray(r.expr, r.ctx); + var i_t9 := ruleConditionalAndOptional(r.expr, r.ctx); + var i_t10 := ruleConditionalOptionalSimple(r.expr, r.ctx); + var i_t11 := ruleConditionalInMap(r.expr, r.ctx); + var i_t12 := ruleConditionalOptionalTruthy(r.expr, r.ctx); + return (match (match (match (match (match (match (match (match (match (match (match (match i_t1 { case Some(i_oc0_val) => Option.Some(i_oc0_val) case None => i_t2 }) { case Some(i_oc1_val) => Option.Some(i_oc1_val) case None => i_t3 }) { case Some(i_oc2_val) => Option.Some(i_oc2_val) case None => i_t4 }) { case Some(i_oc3_val) => Option.Some(i_oc3_val) case None => i_t5 }) { case Some(i_oc4_val) => Option.Some(i_oc4_val) case None => i_t6 }) { case Some(i_oc5_val) => Option.Some(i_oc5_val) case None => i_t7 }) { case Some(i_oc6_val) => Option.Some(i_oc6_val) case None => i_t8 }) { case Some(i_oc7_val) => Option.Some(i_oc7_val) case None => i_t9 }) { case Some(i_oc8_val) => Option.Some(i_oc8_val) case None => i_t10 }) { case Some(i_oc9_val) => Option.Some(i_oc9_val) case None => i_t11 }) { case Some(i_oc10_val) => Option.Some(i_oc10_val) case None => i_t12 }) { case Some(i_oc11_val) => i_oc11_val case None => r }); +} + +method walkExprs(es: seq, ctx: CondCtx) returns (res: ExprsOut) +{ + if (|es| == 0) { + return ExprsOut([], ctx); + } + var i_t13 := walkExpr(es[0], ctx); + var head := i_t13; + var i_t14 := walkExprs(es[1..], head.ctx); + var tail := i_t14; + return ExprsOut(([head.expr] + tail.exprs), tail.ctx); +} + +method walkChainSteps(steps: seq, ctx: CondCtx) returns (res: StepsOut) +{ + if (|steps| == 0) { + return StepsOut([], ctx); + } + var s := steps[0]; + var step := s; + var c := ctx; + match s { + case call(i_s_args, i_s_ty, i_s_callKind, i_s_builtinId) => + var i_t15 := walkExprs(i_s_args, ctx); + var args := i_t15; + step := s.(args := args.exprs); + c := args.ctx; + case index(i_s_idx, i_s_ty) => + var i_t16 := walkExpr(i_s_idx, ctx); + var idx := i_t16; + step := s.(idx := idx.expr); + c := idx.ctx; + case field(i_s_name, i_s_ty) => + + } + var i_t17 := walkChainSteps(steps[1..], c); + var rest := i_t17; + return StepsOut(([step] + rest.steps), rest.ctx); +} + +method walkRecordFields(fields: seq, ctx: CondCtx) returns (res: RecordFieldsOut) +{ + if (|fields| == 0) { + return RecordFieldsOut([], ctx); + } + var i_t18 := walkExpr(fields[0].value, ctx); + var v := i_t18; + var i_t19 := walkRecordFields(fields[1..], v.ctx); + var rest := i_t19; + return RecordFieldsOut(([fields[0].(value := v.expr)] + rest.fields), rest.ctx); +} + +method walkExprCases(cases: seq, ctx: CondCtx) returns (res: ExprCasesOut) +{ + if (|cases| == 0) { + return ExprCasesOut([], ctx); + } + var i_t20 := walkExpr(cases[0].body, ctx); + var b := i_t20; + var i_t21 := walkExprCases(cases[1..], b.ctx); + var rest := i_t21; + return ExprCasesOut(([cases[0].(body := b.expr)] + rest.cases), rest.ctx); +} + +method walkStmtCases(cases: seq, ctx: CondCtx) returns (res: StmtCasesOut) +{ + if (|cases| == 0) { + return StmtCasesOut([], ctx); + } + var i_t22 := walkStmts(cases[0].body, ctx); + var b := i_t22; + var i_t23 := walkStmtCases(cases[1..], b.ctx); + var rest := i_t23; + return StmtCasesOut(([cases[0].(body := b.stmts)] + rest.cases), rest.ctx); +} + +method walkSwitchCases(cases: seq, ctx: CondCtx) returns (res: SwitchCasesOut) +{ + if (|cases| == 0) { + return SwitchCasesOut([], ctx); + } + var i_t24 := walkStmts(cases[0].body, ctx); + var b := i_t24; + var i_t25 := walkSwitchCases(cases[1..], b.ctx); + var rest := i_t25; + return SwitchCasesOut(([cases[0].(body := b.stmts)] + rest.cases), rest.ctx); +} + +method recurseExpr(e: TExpr, ctx: CondCtx) returns (res: ExprOut) +{ + match e { + case var_(i_e_name, i_e_ty) => + return ExprOut(e, ctx); + case num(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case bigint(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case str(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case bool_(i_e_value, i_e_ty) => + return ExprOut(e, ctx); + case havoc(i_e_ty) => + return ExprOut(e, ctx); + case binop(i_e_op, i_e_left, i_e_right, i_e_ty) => + var i_t26 := walkExpr(i_e_left, ctx); + var l := i_t26; + var i_t27 := walkExpr(i_e_right, l.ctx); + var r := i_t27; + return ExprOut(e.(left := l.expr, right := r.expr), r.ctx); + case unop(i_e_op, i_e_expr, i_e_ty) => + var i_t28 := walkExpr(i_e_expr, ctx); + var x := i_t28; + return ExprOut(e.(expr := x.expr), x.ctx); + case call(i_e_fn, i_e_args, i_e_ty, i_e_callKind, i_e_builtinId) => + var i_t29 := walkExpr(i_e_fn, ctx); + var fn := i_t29; + var i_t30 := walkExprs(i_e_args, fn.ctx); + var args := i_t30; + return ExprOut(e.(fn := fn.expr, args := args.exprs), args.ctx); + case index(i_e_obj, i_e_idx, i_e_ty) => + var i_t31 := walkExpr(i_e_obj, ctx); + var obj := i_t31; + var i_t32 := walkExpr(i_e_idx, obj.ctx); + var idx := i_t32; + return ExprOut(e.(obj := obj.expr, idx := idx.expr), idx.ctx); + case field(i_e_obj, i_e_field, i_e_ty, i_e_isDiscriminant, i_e_ofVariant) => + var i_t33 := walkExpr(i_e_obj, ctx); + var obj := i_t33; + return ExprOut(e.(obj := obj.expr), obj.ctx); + case record(i_e_spread, i_e_fields, i_e_ty) => + var spread := None; + match i_e_spread { + case Some(i_e_spread_val) => + var i_t34 := walkExpr(i_e_spread_val, ctx); + spread := Some(i_t34); + case None => + + } + var i_t35 := walkRecordFields(i_e_fields, (match spread { case Some(i_spread_val) => i_spread_val.ctx case None => ctx })); + var fields := i_t35; + return ExprOut(e.(spread := (match spread { case Some(i_spread_val) => Option.Some(i_spread_val.expr) case None => Option.None }), fields := fields.fields), fields.ctx); + case arrayLiteral(i_e_elems, i_e_ty) => + var i_t36 := walkExprs(i_e_elems, ctx); + var elems := i_t36; + return ExprOut(e.(elems := elems.exprs), elems.ctx); + case lambda(i_e_params, i_e_body, i_e_ty) => + var i_t37 := walkStmts(i_e_body, ctx); + var body := i_t37; + return ExprOut(e.(body_lambda := body.stmts), body.ctx); + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var i_t38 := walkExpr(i_e_cond, ctx); + var cond := i_t38; + var i_t39 := walkExpr(i_e_then, cond.ctx); + var then_ := i_t39; + var i_t40 := walkExpr(i_e_else, then_.ctx); + var els := i_t40; + return ExprOut(e.(cond := cond.expr, then_ := then_.expr, else_ := els.expr), els.ctx); + case optChain(i_e_obj, i_e_chain, i_e_ty) => + var i_t41 := walkExpr(i_e_obj, ctx); + var obj := i_t41; + var i_t42 := walkChainSteps(i_e_chain, obj.ctx); + var chain := i_t42; + return ExprOut(e.(obj := obj.expr, chain := chain.steps), chain.ctx); + case nullish(i_e_left, i_e_right, i_e_ty) => + var i_t43 := walkExpr(i_e_left, ctx); + var l := i_t43; + var i_t44 := walkExpr(i_e_right, l.ctx); + var r := i_t44; + return ExprOut(e.(left := l.expr, right := r.expr), r.ctx); + case forall_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + var i_t45 := walkExpr(i_e_body, ctx); + var b := i_t45; + return ExprOut(e.(body_forall := b.expr), b.ctx); + case exists_(i_e_var, i_e_varTy, i_e_body, i_e_ty) => + var i_t46 := walkExpr(i_e_body, ctx); + var b := i_t46; + return ExprOut(e.(body_exists := b.expr), b.ctx); + case someMatch(i_e_scrutinee, i_e_binder, i_e_binderTy, i_e_someBody, i_e_noneBody, i_e_ty) => + var i_t47 := walkExpr(i_e_someBody, ctx); + var some := i_t47; + var i_t48 := walkExpr(i_e_noneBody, some.ctx); + var none := i_t48; + return ExprOut(e.(someBody := some.expr, noneBody := none.expr), none.ctx); + case tagMatch(i_e_scrutinee, i_e_typeName, i_e_cases, i_e_fallthrough, i_e_ty) => + var i_t49 := walkExpr(i_e_scrutinee, ctx); + var scrut := i_t49; + var i_t50 := walkExprCases(i_e_cases, scrut.ctx); + var cases := i_t50; + var ft := None; + match i_e_fallthrough { + case Some(i_e_fallthrough_val) => + var i_t51 := walkExpr(i_e_fallthrough_val, cases.ctx); + ft := Some(i_t51); + case None => + + } + return ExprOut(e.(scrutinee := scrut.expr, cases := cases.cases, fallthrough := (match ft { case Some(i_ft_val) => Option.Some(i_ft_val.expr) case None => Option.None })), (match ft { case Some(i_ft_val) => i_ft_val.ctx case None => cases.ctx })); + } +} + +method walkStmt(s: TStmt, ctx: CondCtx) returns (res: StmtOut) +{ + var i_t52 := recurseStmt(s, ctx); + var r := i_t52; + var i_t53 := ruleIfAndOptional(r.stmt, r.ctx); + var i_t54 := ruleIfAndArrayIsArray(r.stmt, r.ctx); + var i_t55 := ruleIfOptionalSimple(r.stmt, r.ctx); + var i_t56 := ruleExprStmtAndOptional(r.stmt, r.ctx); + var i_t57 := ruleOptionalIndexBinding(r.stmt, r.ctx); + return (match (match (match (match (match i_t53 { case Some(i_oc12_val) => Option.Some(i_oc12_val) case None => i_t54 }) { case Some(i_oc13_val) => Option.Some(i_oc13_val) case None => i_t55 }) { case Some(i_oc14_val) => Option.Some(i_oc14_val) case None => i_t56 }) { case Some(i_oc15_val) => Option.Some(i_oc15_val) case None => i_t57 }) { case Some(i_oc16_val) => i_oc16_val case None => r }); +} + +method walkStmts(stmts: seq, ctx: CondCtx) returns (res: StmtsOut) +{ + if (|stmts| == 0) { + return StmtsOut([], ctx); + } + var s := stmts[0]; + var rest := stmts[1..]; + var i_t58 := ruleDiscriminantChain(stmts, ctx); + var i_t59 := ruleDiscriminantNegEarlyReturn(stmts, ctx); + var tagged := (match i_t58 { case Some(i_oc17_val) => Option.Some(i_oc17_val) case None => i_t59 }); + match tagged { + case Some(i_tagged_val) => + var i_t60 := walkStmt(i_tagged_val.stmt, ctx); + var w := i_t60; + var i_t61 := walkStmts(stmts[i_tagged_val.consumed..], w.ctx); + var after := i_t61; + return StmtsOut(([w.stmt] + after.stmts), after.ctx); + case None => + + } + var i_t62 := ruleEarlyReturnOrChain(s, rest, ctx); + var orRewrite := i_t62; + match orRewrite { + case Some(i_orRewrite_val) => + return StmtsOut([i_orRewrite_val.stmt], i_orRewrite_val.ctx); + case None => + + } + var i_t63 := ruleEarlyReturnConsume(s, rest); + var i_t64 := ruleEarlyReturnOptChainCompare(s, rest, ctx); + var consumed := (match i_t63 { case Some(i_oc18_val) => Option.Some(i_oc18_val) case None => i_t64 }); + match consumed { + case Some(i_consumed_val) => + var i_t65 := walkStmt(i_consumed_val, ctx); + var w := i_t65; + return StmtsOut([w.stmt], w.ctx); + case None => + + } + var i_t66 := walkStmt(s, ctx); + var walked := i_t66; + var i_t67 := ruleLetCondAndOptional(walked.stmt); + var expanded := i_t67; + match expanded { + case Some(i_expanded_val) => + var i_t68 := walkEach(i_expanded_val, walked.ctx); + var ws := i_t68; + var i_t69 := walkStmts(rest, ws.ctx); + var after := i_t69; + return StmtsOut((ws.stmts + after.stmts), after.ctx); + case None => + + } + var i_t70 := walkStmts(rest, walked.ctx); + var after := i_t70; + return StmtsOut(([walked.stmt] + after.stmts), after.ctx); +} + +method walkEach(stmts: seq, ctx: CondCtx) returns (res: StmtsOut) +{ + if (|stmts| == 0) { + return StmtsOut([], ctx); + } + var i_t71 := walkStmt(stmts[0], ctx); + var h := i_t71; + var i_t72 := walkEach(stmts[1..], h.ctx); + var t := i_t72; + return StmtsOut(([h.stmt] + t.stmts), t.ctx); +} + +method recurseStmt(s: TStmt, ctx: CondCtx) returns (res: StmtOut) +{ + match s { + case let(i_s_name, i_s_ty, i_s_mutable, i_s_init) => + var i_t73 := walkExpr(i_s_init, ctx); + var init := i_t73; + return StmtOut(s.(init := init.expr), init.ctx); + case assign(i_s_target, i_s_value) => + var i_t74 := walkExpr(i_s_value, ctx); + var v := i_t74; + return StmtOut(s.(value := v.expr), v.ctx); + case return_(i_s_value) => + var i_t75 := walkExpr(i_s_value, ctx); + var v := i_t75; + return StmtOut(s.(value := v.expr), v.ctx); + case break_ => + return StmtOut(s, ctx); + case continue_ => + return StmtOut(s, ctx); + case throw => + return StmtOut(s, ctx); + case expr(i_s_expr) => + var i_t76 := walkExpr(i_s_expr, ctx); + var x := i_t76; + return StmtOut(s.(expr := x.expr), x.ctx); + case if_(i_s_cond, i_s_then, i_s_else) => + var i_t77 := walkExpr(i_s_cond, ctx); + var cond := i_t77; + var i_t78 := walkStmts(i_s_then, cond.ctx); + var then_ := i_t78; + var i_t79 := walkStmts(i_s_else, then_.ctx); + var els := i_t79; + return StmtOut(s.(cond := cond.expr, then_ := then_.stmts, else_ := els.stmts), els.ctx); + case while_(i_s_cond, i_s_invariants, i_s_decreases, i_s_doneWith, i_s_body) => + var i_t80 := walkExpr(i_s_cond, ctx); + var cond := i_t80; + var i_t81 := walkExprs(i_s_invariants, cond.ctx); + var invs := i_t81; + var dec := None; + match i_s_decreases { + case Some(i_s_decreases_val) => + var i_t82 := walkExpr(i_s_decreases_val, invs.ctx); + dec := Some(i_t82); + case None => + + } + var dw := None; + match i_s_doneWith { + case Some(i_s_doneWith_val) => + var i_t83 := walkExpr(i_s_doneWith_val, (match dec { case Some(i_dec_val) => i_dec_val.ctx case None => invs.ctx })); + dw := Some(i_t83); + case None => + + } + var i_t84 := walkStmts(i_s_body, (match dw { case Some(i_dw_val) => i_dw_val.ctx case None => (match dec { case Some(i_dec_val) => i_dec_val.ctx case None => invs.ctx }) })); + var body := i_t84; + return StmtOut(s.(cond := cond.expr, invariants := invs.exprs, decreases_ := (match dec { case Some(i_dec_val) => Option.Some(i_dec_val.expr) case None => Option.None }), doneWith := (match dw { case Some(i_dw_val) => Option.Some(i_dw_val.expr) case None => Option.None }), body := body.stmts), body.ctx); + case switch(i_s_expr, i_s_discriminant, i_s_cases, i_s_defaultBody) => + var i_t85 := walkExpr(i_s_expr, ctx); + var x := i_t85; + var i_t86 := walkSwitchCases(i_s_cases, x.ctx); + var cases := i_t86; + var i_t87 := walkStmts(i_s_defaultBody, cases.ctx); + var def := i_t87; + return StmtOut(s.(expr := x.expr, cases_switch := cases.cases, defaultBody := def.stmts), def.ctx); + case forof(i_s_names, i_s_nameTypes, i_s_iterable, i_s_invariants, i_s_doneWith, i_s_body) => + var i_t88 := walkExpr(i_s_iterable, ctx); + var it := i_t88; + var i_t89 := walkExprs(i_s_invariants, it.ctx); + var invs := i_t89; + var dw := None; + match i_s_doneWith { + case Some(i_s_doneWith_val) => + var i_t90 := walkExpr(i_s_doneWith_val, invs.ctx); + dw := Some(i_t90); + case None => + + } + var i_t91 := walkStmts(i_s_body, (match dw { case Some(i_dw_val) => i_dw_val.ctx case None => invs.ctx })); + var body := i_t91; + return StmtOut(s.(iterable := it.expr, invariants := invs.exprs, doneWith := (match dw { case Some(i_dw_val) => Option.Some(i_dw_val.expr) case None => Option.None }), body := body.stmts), body.ctx); + case ghostLet(i_s_name, i_s_ty, i_s_init) => + var i_t92 := walkExpr(i_s_init, ctx); + var init := i_t92; + return StmtOut(s.(init := init.expr), init.ctx); + case ghostAssign(i_s_target, i_s_value) => + var i_t93 := walkExpr(i_s_value, ctx); + var v := i_t93; + return StmtOut(s.(value := v.expr), v.ctx); + case assert_(i_s_expr, i_s_assumed) => + var i_t94 := walkExpr(i_s_expr, ctx); + var x := i_t94; + return StmtOut(s.(expr := x.expr), x.ctx); + case someMatch(i_s_scrutinee, i_s_binder, i_s_binderTy, i_s_someBody, i_s_noneBody) => + var i_t95 := walkStmts(i_s_someBody, ctx); + var some := i_t95; + var i_t96 := walkStmts(i_s_noneBody, some.ctx); + var none := i_t96; + return StmtOut(s.(someBody := some.stmts, noneBody := none.stmts), none.ctx); + case tagMatch(i_s_scrutinee, i_s_typeName, i_s_cases, i_s_fallthrough) => + var i_t97 := walkExpr(i_s_scrutinee, ctx); + var scrut := i_t97; + var i_t98 := walkStmtCases(i_s_cases, scrut.ctx); + var cases := i_t98; + var i_t99 := walkStmts(i_s_fallthrough, cases.ctx); + var ft := i_t99; + return StmtOut(s.(scrutinee := scrut.expr, cases_tagMatch := cases.cases, fallthrough := ft.stmts), ft.ctx); + } +} + +method orChain(leaves: seq) returns (res: TExpr) + requires (|leaves| > 0) +{ + var acc := leaves[0]; + var i := 1; + while (i < |leaves|) + { + acc := binop("||", acc, leaves[i], Ty.bool_); + i := (i + 1); + } + return acc; +} + +method ruleEarlyReturnOrChain(s: TStmt, rest: seq, ctx: CondCtx) returns (res: Option) +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|rest| == 0) { + return None; + } + var i_t100 := isTerminating(i_s_then); + if (((|i_s_then| == 0) || (|i_s_else| != 0)) || !(i_t100)) { + return None; + } + if ((!i_s_cond.binop?) || (i_s_cond.op != "||")) { + return None; + } + var leaves := flattenOr(i_s_cond); + if (|leaves| < 2) { + return None; + } + var detectors: seq := []; + var residualLeaves: seq := []; + var seen: set := {}; + var i_leaf_idx := 0; + while i_leaf_idx < |leaves| + invariant (i_leaf_idx <= |leaves|) + { + var leaf := leaves[i_leaf_idx]; + var d := noneDetector(leaf, ctx); + match d { + case Some(i_d_val) => + var key := binderHintFor(i_d_val.scrutinee); + match key { + case Some(i_key_val) => + if (i_key_val in seen) { + residualLeaves := (residualLeaves + [leaf]); + i_leaf_idx := (i_leaf_idx + 1); + continue; + } + seen := (seen + {i_key_val}); + detectors := (detectors + [i_d_val]); + match i_d_val.residual { + case Some(i_d_residual_val) => + residualLeaves := (residualLeaves + [i_d_residual_val]); + case None => + + } + case None => + residualLeaves := (residualLeaves + [leaf]); + i_leaf_idx := (i_leaf_idx + 1); + continue; + } + case None => + residualLeaves := (residualLeaves + [leaf]); + } + i_leaf_idx := i_leaf_idx + 1; + } + if (|detectors| == 0) { + return None; + } + var inner := rest; + if (|residualLeaves| > 0) { + var i_t101 := orChain(residualLeaves); + inner := ([if_(i_t101, i_s_then, [])] + rest); + } + var i_t102 := walkStmts(inner, ctx); + var wInner := i_t102; + var nest := wInner.stmts; + var c := wInner.ctx; + var i := (|detectors| - 1); + while (i >= 0) + invariant ((|nest| > 0) || (i == (|detectors| - 1))) + { + var d := detectors[i]; + var i_t103 := walkStmts(i_s_then, c); + var wThen := i_t103; + c := wThen.ctx; + nest := [TStmt.someMatch(d.scrutinee, d.binder, d.innerTy, nest, wThen.stmts)]; + i := (i - 1); + } + return Some(StmtOut(nest[0], c)); + case _ => + return None; + } +} + +method ruleImplOptional(e: TExpr, ctx: CondCtx) returns (res: Option) +{ + if ((!e.binop?) || (e.op != "==>")) { + return None; + } + var check := None; + var restCond := None; + var extracted := leadingPresent(e.left); + match extracted { + case Some(i_extracted_val) => + check := Some(i_extracted_val.check); + restCond := Some(i_extracted_val.restCond); + case None => + var c := presentFact(e.left); + match c { + case Some(i_c_val) => + if i_c_val.negated { + return None; + } + check := Some(i_c_val); + case None => + return None; + } + } + match check { + case Some(i_check_val) => + var innerBody := (match restCond { case Some(i_restCond_val) => binop("==>", i_restCond_val, e.right, Ty.bool_) case None => e.right }); + var i_t104 := walkExpr(innerBody, ctx); + var w := i_t104; + return Some(ExprOut(TExpr.someMatch(i_check_val.scrutinee, i_check_val.binder, i_check_val.innerTy, w.expr, TExpr.bool_(true, Ty.bool_), Ty.bool_), w.ctx)); + case None => + return None; + } +} + +method ruleIfAndOptional(s: TStmt, ctx: CondCtx) returns (res: Option) +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + if (|i_s_else| != 0) { + return None; + } + var extracted := leadingPresent(i_s_cond); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerIf := if_(restCond, i_s_then, []); + var i_t105 := walkStmt(innerIf, ctx); + var w := i_t105; + return Some(StmtOut(presentMatchStmts(check, [w.stmt], []), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleExprStmtAndOptional(s: TStmt, ctx: CondCtx) returns (res: Option) +{ + match s { + case expr(i_s_expr) => + if ((!i_s_expr.binop?) || (i_s_expr.op != "&&")) { + return None; + } + var extracted := leadingPresent(i_s_expr); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerStmt := expr(restCond); + var i_t106 := walkStmt(innerStmt, ctx); + var w := i_t106; + return Some(StmtOut(presentMatchStmts(check, [w.stmt], []), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleConditionalAndOptional(e: TExpr, ctx: CondCtx) returns (res: Option) +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var extracted := leadingPresent(i_e_cond); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var i_t107 := containsMethodCall(restCond); + if i_t107 { + return None; + } + var innerCond := conditional(restCond, i_e_then, i_e_else, i_e_ty); + var i_t108 := walkExpr(innerCond, ctx); + var w := i_t108; + return Some(ExprOut(presentMatchExpr(check, w.expr, i_e_else, i_e_ty), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleImplArrayIsArray(e: TExpr, ctx: CondCtx) returns (res: Option) +{ + if ((!e.binop?) || (e.op != "==>")) { + return None; + } + var pos := isArrayFact(e.left, ctx); + var neg := (if (e.left.unop? && (e.left.op == "!")) then isArrayFact(e.left.expr, ctx) else Option.None); + var matched := (match pos { case Some(i_oc19_val) => Option.Some(i_oc19_val) case None => (match neg { case Some(i_neg_val) => Option.Some(IsArrayFact(i_neg_val.scrutinee, i_neg_val.typeName, "NonArrayBranch")) case None => Option.None }) }); + match matched { + case Some(i_matched_val) => + var i_t109 := walkExpr(e.right, ctx); + var w := i_t109; + return Some(ExprOut(TExpr.tagMatch(i_matched_val.scrutinee, i_matched_val.typeName, [TExprCase(i_matched_val.variant, w.expr)], Some(TExpr.bool_(true, Ty.bool_)), Ty.bool_), w.ctx)); + case None => + return None; + } +} + +method ruleConditionalArrayIsArray(e: TExpr, ctx: CondCtx) returns (res: Option) +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var pos := isArrayFact(i_e_cond, ctx); + var tof := None; + if (match pos { case Some(i_) => false case None => true }) { + tof := typeofStringFact(i_e_cond, ctx); + } + var neg := None; + if (match pos { case Some(i_) => false case None => true }) { + if (match tof { case Some(i_) => false case None => true }) { + if (i_e_cond.unop? && (i_e_cond.op == "!")) { + neg := isArrayFact(i_e_cond.expr, ctx); + } + } + } + var matched := (match (match pos { case Some(i_oc20_val) => Option.Some(i_oc20_val) case None => tof }) { case Some(i_oc21_val) => Option.Some(i_oc21_val) case None => (match neg { case Some(i_neg_val) => Option.Some(IsArrayFact(i_neg_val.scrutinee, i_neg_val.typeName, "NonArrayBranch")) case None => Option.None }) }); + match matched { + case Some(i_matched_val) => + var positive := (match pos { case Some(i_oc22_val) => Option.Some(i_oc22_val) case None => tof }); + var thenBody := (match positive { case Some(i_positive_val) => i_e_then case None => i_e_else }); + var elseBody := (match positive { case Some(i_positive_val) => i_e_else case None => i_e_then }); + var i_t110 := walkExpr(thenBody, ctx); + var wThen := i_t110; + var i_t111 := walkExpr(elseBody, wThen.ctx); + var wElse := i_t111; + return Some(ExprOut(TExpr.tagMatch(i_matched_val.scrutinee, i_matched_val.typeName, [TExprCase(i_matched_val.variant, wThen.expr)], Some(wElse.expr), i_e_ty), wElse.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleIfAndArrayIsArray(s: TStmt, ctx: CondCtx) returns (res: Option) +{ + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var extracted := leadingIsArray(i_s_cond, ctx); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerIf := if_(restCond, i_s_then, i_s_else); + var i_t112 := walkStmt(innerIf, ctx); + var w := i_t112; + return Some(StmtOut(TStmt.tagMatch(check.scrutinee, check.typeName, [TStmtCase(check.variant, [w.stmt])], i_s_else), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleConditionalAndArrayIsArray(e: TExpr, ctx: CondCtx) returns (res: Option) +{ + match e { + case conditional(i_e_cond, i_e_then, i_e_else, i_e_ty) => + var extracted := leadingIsArray(i_e_cond, ctx); + match extracted { + case Some(i_extracted_val) => + var check := i_extracted_val.check; + var restCond := i_extracted_val.restCond; + var innerCond := conditional(restCond, i_e_then, i_e_else, i_e_ty); + var i_t113 := walkExpr(innerCond, ctx); + var w := i_t113; + return Some(ExprOut(TExpr.tagMatch(check.scrutinee, check.typeName, [TExprCase(check.variant, w.expr)], Some(i_e_else), i_e_ty), w.ctx)); + case None => + return None; + } + case _ => + return None; + } +} + +method ruleDiscriminantChain(stmts: seq, ctx: CondCtx) returns (res: Option) + ensures (match res { case Some(i_result_val) => ((i_result_val.consumed >= 0) && (i_result_val.consumed <= |stmts|)) case None => true }) +{ + if ((|stmts| == 0) || (!stmts[0].if_?)) { + return None; + } + var first := variantFact(stmts[0].cond, ctx); + match first { + case Some(i_first_val) => + var cases: seq := []; + var consumed := 0; + var i := 0; + while (i < |stmts|) + invariant (consumed >= 0) + invariant (consumed <= |stmts|) + invariant (i <= |stmts|) + { + var s := stmts[i]; + match s { + case if_(i_s_cond, i_s_then, i_s_else) => + var p := variantFact(i_s_cond, ctx); + match p { + case Some(i_p_val) => + if (i_p_val.scrutineeName != i_first_val.scrutineeName) { + break; + } + cases := (cases + [TStmtCase(i_p_val.variant, i_s_then)]); + consumed := (i + 1); + if (|i_s_else| > 0) { + var i_t114 := collectElseChain(i_s_else[0], i_first_val.scrutineeName, ctx); + var tail := (if ((|i_s_else| == 1) && i_s_else[0].if_?) then i_t114 else ElseChain([], i_s_else)); + return Some(ConsumedRewrite(TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, (cases + tail.cases), tail.fallthrough), consumed)); + } + i := (i + 1); + case None => + break; + } + case _ => + break; + } + } + if (|cases| == 0) { + return None; + } + var i_t115 := allCasesTerminate(cases); + if i_t115 { + return Some(ConsumedRewrite(TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, cases, stmts[consumed..]), |stmts|)); + } + return Some(ConsumedRewrite(TStmt.tagMatch(i_first_val.scrutinee, i_first_val.typeName, cases, []), consumed)); + case None => + return None; + } +} + +method narrowFunction(fn: TFunction, ctx: CondCtx) returns (res: FunctionOut) +{ + var i_t116 := walkExprs(fn.requires_, ctx); + var reqs := i_t116; + var i_t117 := walkExprs(fn.ensures_, reqs.ctx); + var enss := i_t117; + var dec := None; + match fn.decreases_ { + case Some(i_fn_decreases_val) => + var i_t118 := walkExpr(i_fn_decreases_val, enss.ctx); + dec := Some(i_t118); + case None => + + } + var i_t119 := walkStmts(fn.body, (match dec { case Some(i_dec_val) => i_dec_val.ctx case None => enss.ctx })); + var body := i_t119; + return FunctionOut(fn.(requires_ := reqs.exprs, ensures_ := enss.exprs, decreases_ := (match dec { case Some(i_dec_val) => Option.Some(i_dec_val.expr) case None => Option.None }), body := body.stmts), body.ctx); +} + +method narrowConstants(constants: seq, ctx: CondCtx) returns (res: ConstantsOut) +{ + if (|constants| == 0) { + return ConstantsOut([], ctx); + } + var i_t120 := walkExpr(constants[0].value, ctx); + var v := i_t120; + var i_t121 := narrowConstants(constants[1..], v.ctx); + var rest := i_t121; + return ConstantsOut(([constants[0].(value := v.expr)] + rest.constants), rest.ctx); +} + +method narrowFunctions(fns: seq, ctx: CondCtx) returns (res: FunctionsOut) +{ + if (|fns| == 0) { + return FunctionsOut([], ctx); + } + var i_t122 := narrowFunction(fns[0], ctx); + var f := i_t122; + var i_t123 := narrowFunctions(fns[1..], f.ctx); + var rest := i_t123; + return FunctionsOut(([f.fn] + rest.functions), rest.ctx); +} + +method narrowClasses(classes: seq, ctx: CondCtx) returns (res: ClassesOut) +{ + if (|classes| == 0) { + return ClassesOut([], ctx); + } + var i_t124 := narrowFunctions(classes[0].methods, ctx); + var methods := i_t124; + var i_t125 := narrowClasses(classes[1..], methods.ctx); + var rest := i_t125; + return ClassesOut(([classes[0].(methods := methods.functions)] + rest.classes), rest.ctx); +} + +method narrowModule(mod: TModule) returns (res: TModule) +{ + var ctx := CondCtx(mod.typeDecls, 0); + var i_t126 := narrowConstants(mod.constants, ctx); + var consts := i_t126; + var i_t127 := narrowFunctions(mod.functions, consts.ctx); + var fns := i_t127; + var i_t128 := narrowClasses(mod.classes, fns.ctx); + var classes := i_t128; + return mod.(constants := consts.constants, functions := fns.functions, classes := classes.classes); +} diff --git a/tools/src/narrow.ts b/tools/src/narrow.ts index 3d7d2e4..c6ae48b 100644 --- a/tools/src/narrow.ts +++ b/tools/src/narrow.ts @@ -30,16 +30,22 @@ * of the block. Rule order matters — see the comments at the two dispatch * chains. * - * State is explicit (§6.1): a `CondCtx` (type declarations + optChain - * binder counter) threads through the walk; no module-level state. + * State is explicit (§6.1): a `CondCtx` (type declarations + the next + * optChain binder index) threads through the walk — every walker returns + * its rewritten node plus the (possibly advanced) ctx, list walks are + * state-threaded folds, and nothing is mutated (§6.2). Rules take the + * recursed node and the post-recursion ctx; a rule that mints a binder or + * re-walks a constructed node returns the advanced ctx, all others pass + * it through. */ -import type { TModule, TFunction, TStmt, TExpr } from "./typedir.js"; +import type { TModule, TFunction, TConst, TClass, TStmt, TExpr, + TChainStep, TRecordField, TExprCase, TStmtCase, TSwitchCase } from "./typedir.js"; import { isTerminatorKind } from "./typedir.js"; import { freshName } from "./names.js"; -import { builtinSpec } from "./builtins.js"; +import { builtinPure } from "./builtins.js"; import { - type CondCtx, type PresentFact, type NoneDetector, + type CondCtx, type PresentFact, type NoneDetector, type IsArrayFact, presentFact, leadingPresent, leadingIsArray, flattenOr, noneDetector, binderHintFor, binderHintForMapAccess, freshOcBinder, applyChain, restoreDiscriminantFlag, arrayBoundsCond, exprEqual, @@ -49,42 +55,169 @@ import { // ── Walkers ────────────────────────────────────────────────── -function walkExpr(e: TExpr, ctx: CondCtx): TExpr { +interface ExprOut { expr: TExpr; ctx: CondCtx } +interface ExprsOut { exprs: TExpr[]; ctx: CondCtx } +interface StmtOut { stmt: TStmt; ctx: CondCtx } +interface StmtsOut { stmts: TStmt[]; ctx: CondCtx } + +function walkExpr(e: TExpr, ctx: CondCtx): ExprOut { const r = recurseExpr(e, ctx); - return ruleNullish(r, ctx) ?? ruleNullishIndex(r) ?? ruleOptChainIndex(r) ?? ruleOptChain(r, ctx) ?? ruleImplOptional(r, ctx) ?? ruleImplArrayIsArray(r, ctx) ?? ruleConditionalArrayIsArray(r, ctx) ?? ruleConditionalAndArrayIsArray(r, ctx) ?? ruleConditionalAndOptional(r, ctx) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r, ctx) ?? ruleConditionalOptionalTruthy(r) ?? r; + return ruleNullish(r.expr, r.ctx) ?? ruleNullishIndex(r.expr, r.ctx) ?? ruleOptChainIndex(r.expr, r.ctx) ?? ruleOptChain(r.expr, r.ctx) ?? ruleImplOptional(r.expr, r.ctx) ?? ruleImplArrayIsArray(r.expr, r.ctx) ?? ruleConditionalArrayIsArray(r.expr, r.ctx) ?? ruleConditionalAndArrayIsArray(r.expr, r.ctx) ?? ruleConditionalAndOptional(r.expr, r.ctx) ?? ruleConditionalOptionalSimple(r.expr, r.ctx) ?? ruleConditionalInMap(r.expr, r.ctx) ?? ruleConditionalOptionalTruthy(r.expr, r.ctx) ?? r; +} + +/** State-threaded fold of `walkExpr` over a list, left to right. */ +function walkExprs(es: TExpr[], ctx: CondCtx): ExprsOut { + if (es.length === 0) return { exprs: [], ctx }; + const head = walkExpr(es[0], ctx); + const tail = walkExprs(es.slice(1), head.ctx); + return { exprs: [head.expr].concat(tail.exprs), ctx: tail.ctx }; +} + +interface StepsOut { steps: TChainStep[]; ctx: CondCtx } + +function walkChainSteps(steps: TChainStep[], ctx: CondCtx): StepsOut { + if (steps.length === 0) return { steps: [], ctx }; + const s = steps[0]; + let step: TChainStep = s; + let c = ctx; + if (s.kind === "call") { + const args = walkExprs(s.args, ctx); + step = { ...s, args: args.exprs }; + c = args.ctx; + } else if (s.kind === "index") { + const idx = walkExpr(s.idx, ctx); + step = { ...s, idx: idx.expr }; + c = idx.ctx; + } + const rest = walkChainSteps(steps.slice(1), c); + return { steps: [step].concat(rest.steps), ctx: rest.ctx }; +} + +interface RecordFieldsOut { fields: TRecordField[]; ctx: CondCtx } + +function walkRecordFields(fields: TRecordField[], ctx: CondCtx): RecordFieldsOut { + if (fields.length === 0) return { fields: [], ctx }; + const v = walkExpr(fields[0].value, ctx); + const rest = walkRecordFields(fields.slice(1), v.ctx); + return { fields: [{ ...fields[0], value: v.expr }].concat(rest.fields), ctx: rest.ctx }; +} + +interface ExprCasesOut { cases: TExprCase[]; ctx: CondCtx } + +function walkExprCases(cases: TExprCase[], ctx: CondCtx): ExprCasesOut { + if (cases.length === 0) return { cases: [], ctx }; + const b = walkExpr(cases[0].body, ctx); + const rest = walkExprCases(cases.slice(1), b.ctx); + return { cases: [{ ...cases[0], body: b.expr }].concat(rest.cases), ctx: rest.ctx }; +} + +interface StmtCasesOut { cases: TStmtCase[]; ctx: CondCtx } + +function walkStmtCases(cases: TStmtCase[], ctx: CondCtx): StmtCasesOut { + if (cases.length === 0) return { cases: [], ctx }; + const b = walkStmts(cases[0].body, ctx); + const rest = walkStmtCases(cases.slice(1), b.ctx); + return { cases: [{ ...cases[0], body: b.stmts }].concat(rest.cases), ctx: rest.ctx }; } -function recurseExpr(e: TExpr, ctx: CondCtx): TExpr { - const re = (x: TExpr) => walkExpr(x, ctx); +interface SwitchCasesOut { cases: TSwitchCase[]; ctx: CondCtx } + +function walkSwitchCases(cases: TSwitchCase[], ctx: CondCtx): SwitchCasesOut { + if (cases.length === 0) return { cases: [], ctx }; + const b = walkStmts(cases[0].body, ctx); + const rest = walkSwitchCases(cases.slice(1), b.ctx); + return { cases: [{ ...cases[0], body: b.stmts }].concat(rest.cases), ctx: rest.ctx }; +} + +function recurseExpr(e: TExpr, ctx: CondCtx): ExprOut { switch (e.kind) { case "var": case "num": case "bigint": case "str": case "bool": case "havoc": - return e; - case "binop": return { ...e, left: re(e.left), right: re(e.right) }; - case "unop": return { ...e, expr: re(e.expr) }; - case "call": return { ...e, fn: re(e.fn), args: e.args.map(re) }; - case "index": return { ...e, obj: re(e.obj), idx: re(e.idx) }; - case "field": return { ...e, obj: re(e.obj) }; - case "record": return { ...e, spread: e.spread ? re(e.spread) : null, - fields: e.fields.map(f => ({ ...f, value: re(f.value) })) }; - case "arrayLiteral": return { ...e, elems: e.elems.map(re) }; - case "lambda": return { ...e, body: walkStmts(e.body, ctx) }; - case "conditional": return { ...e, cond: re(e.cond), then: re(e.then), else: re(e.else) }; - case "optChain": return { ...e, obj: re(e.obj), - chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(re) } - : s.kind === "index" ? { ...s, idx: re(s.idx) } - : s) }; - case "nullish": return { ...e, left: re(e.left), right: re(e.right) }; - case "forall": return { ...e, body: re(e.body) }; - case "exists": return { ...e, body: re(e.body) }; - case "someMatch": return { ...e, someBody: re(e.someBody), noneBody: re(e.noneBody) }; - case "tagMatch": return { ...e, scrutinee: re(e.scrutinee), - cases: e.cases.map(c => ({ ...c, body: re(c.body) })), - fallthrough: e.fallthrough ? re(e.fallthrough) : null }; + return { expr: e, ctx }; + case "binop": { + const l = walkExpr(e.left, ctx); + const r = walkExpr(e.right, l.ctx); + return { expr: { ...e, left: l.expr, right: r.expr }, ctx: r.ctx }; + } + case "unop": { + const x = walkExpr(e.expr, ctx); + return { expr: { ...e, expr: x.expr }, ctx: x.ctx }; + } + case "call": { + const fn = walkExpr(e.fn, ctx); + const args = walkExprs(e.args, fn.ctx); + return { expr: { ...e, fn: fn.expr, args: args.exprs }, ctx: args.ctx }; + } + case "index": { + const obj = walkExpr(e.obj, ctx); + const idx = walkExpr(e.idx, obj.ctx); + return { expr: { ...e, obj: obj.expr, idx: idx.expr }, ctx: idx.ctx }; + } + case "field": { + const obj = walkExpr(e.obj, ctx); + return { expr: { ...e, obj: obj.expr }, ctx: obj.ctx }; + } + case "record": { + // Statement form, not `e.spread !== null ? walkExpr(…) : null`: the + // walkers lower to methods, and a method call inside a match-expression + // arm gets lifted out of the binder's scope. Statement-position arms + // keep the call in place (same hazard `ruleConditionalAndOptional` + // guards against with containsMethodCall). + let spread: ExprOut | null = null; + if (e.spread !== null) spread = walkExpr(e.spread, ctx); + const fields = walkRecordFields(e.fields, spread ? spread.ctx : ctx); + return { expr: { ...e, spread: spread ? spread.expr : null, fields: fields.fields }, + ctx: fields.ctx }; + } + case "arrayLiteral": { + const elems = walkExprs(e.elems, ctx); + return { expr: { ...e, elems: elems.exprs }, ctx: elems.ctx }; + } + case "lambda": { + const body = walkStmts(e.body, ctx); + return { expr: { ...e, body: body.stmts }, ctx: body.ctx }; + } + case "conditional": { + const cond = walkExpr(e.cond, ctx); + const then = walkExpr(e.then, cond.ctx); + const els = walkExpr(e.else, then.ctx); + return { expr: { ...e, cond: cond.expr, then: then.expr, else: els.expr }, ctx: els.ctx }; + } + case "optChain": { + const obj = walkExpr(e.obj, ctx); + const chain = walkChainSteps(e.chain, obj.ctx); + return { expr: { ...e, obj: obj.expr, chain: chain.steps }, ctx: chain.ctx }; + } + case "nullish": { + const l = walkExpr(e.left, ctx); + const r = walkExpr(e.right, l.ctx); + return { expr: { ...e, left: l.expr, right: r.expr }, ctx: r.ctx }; + } + case "forall": { + const b = walkExpr(e.body, ctx); + return { expr: { ...e, body: b.expr }, ctx: b.ctx }; + } + case "exists": { + const b = walkExpr(e.body, ctx); + return { expr: { ...e, body: b.expr }, ctx: b.ctx }; + } + case "someMatch": { + const some = walkExpr(e.someBody, ctx); + const none = walkExpr(e.noneBody, some.ctx); + return { expr: { ...e, someBody: some.expr, noneBody: none.expr }, ctx: none.ctx }; + } + case "tagMatch": { + const scrut = walkExpr(e.scrutinee, ctx); + const cases = walkExprCases(e.cases, scrut.ctx); + let ft: ExprOut | null = null; + if (e.fallthrough !== null) ft = walkExpr(e.fallthrough, cases.ctx); + return { expr: { ...e, scrutinee: scrut.expr, cases: cases.cases, + fallthrough: ft ? ft.expr : null }, ctx: ft ? ft.ctx : cases.ctx }; + } } } -function walkStmt(s: TStmt, ctx: CondCtx): TStmt { +function walkStmt(s: TStmt, ctx: CondCtx): StmtOut { // Recurse into children first, then try rules at this node. const r = recurseStmt(s, ctx); // Optional narrowing fires before Array.isArray narrowing: in a chain like @@ -94,68 +227,127 @@ function walkStmt(s: TStmt, ctx: CondCtx): TStmt { // array rule fires; independent narrows commute, so the order is harmless.) // && rules fire before the simple rule because they produce nested ifs whose // inner shape doesn't match the simple rule directly. - return ruleIfAndOptional(r, ctx) ?? ruleIfAndArrayIsArray(r, ctx) ?? ruleIfOptionalSimple(r) ?? ruleExprStmtAndOptional(r, ctx) ?? ruleOptionalIndexBinding(r) ?? r; + return ruleIfAndOptional(r.stmt, r.ctx) ?? ruleIfAndArrayIsArray(r.stmt, r.ctx) ?? ruleIfOptionalSimple(r.stmt, r.ctx) ?? ruleExprStmtAndOptional(r.stmt, r.ctx) ?? ruleOptionalIndexBinding(r.stmt, r.ctx) ?? r; } -function walkStmts(stmts: TStmt[], ctx: CondCtx): TStmt[] { - const result: TStmt[] = []; - for (let i = 0; i < stmts.length; i++) { - const s = stmts[i]; - const rest = stmts.slice(i + 1); - // Discriminant rules consume a prefix of stmts; remaining is processed normally. - const tagged = ruleDiscriminantChain(stmts.slice(i), ctx) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i), ctx); - if (tagged) { - result.push(walkStmt(tagged.stmt, ctx)); - i += tagged.consumed - 1; - continue; - } - const consumed = ruleEarlyReturnOrChain(s, rest, ctx) ?? ruleEarlyReturnConsume(s, rest) ?? ruleEarlyReturnOptChainCompare(s, rest, ctx); - if (consumed) { - result.push(walkStmt(consumed, ctx)); - return result; - } - // walkStmt first — narrow's expression rules may rewrite the let init from - // `conditional` to `someMatch`, in which case the let-cond desugar shouldn't fire. - const walked = walkStmt(s, ctx); - const expanded = ruleLetCondAndOptional(walked); - if (expanded) { - for (const x of expanded) result.push(walkStmt(x, ctx)); - continue; - } - result.push(walked); +function walkStmts(stmts: TStmt[], ctx: CondCtx): StmtsOut { + if (stmts.length === 0) return { stmts: [], ctx }; + const s = stmts[0]; + const rest = stmts.slice(1); + // Discriminant rules consume a prefix of stmts; remaining is processed normally. + const tagged = ruleDiscriminantChain(stmts, ctx) ?? ruleDiscriminantNegEarlyReturn(stmts, ctx); + if (tagged) { + const w = walkStmt(tagged.stmt, ctx); + const after = walkStmts(stmts.slice(tagged.consumed), w.ctx); + return { stmts: [w.stmt].concat(after.stmts), ctx: after.ctx }; + } + const orRewrite = ruleEarlyReturnOrChain(s, rest, ctx); + if (orRewrite) return { stmts: [orRewrite.stmt], ctx: orRewrite.ctx }; + const consumed = ruleEarlyReturnConsume(s, rest) ?? ruleEarlyReturnOptChainCompare(s, rest, ctx); + if (consumed) { + const w = walkStmt(consumed, ctx); + return { stmts: [w.stmt], ctx: w.ctx }; + } + // walkStmt first — narrow's expression rules may rewrite the let init from + // `conditional` to `someMatch`, in which case the let-cond desugar shouldn't fire. + const walked = walkStmt(s, ctx); + const expanded = ruleLetCondAndOptional(walked.stmt); + if (expanded) { + const ws = walkEach(expanded, walked.ctx); + const after = walkStmts(rest, ws.ctx); + return { stmts: ws.stmts.concat(after.stmts), ctx: after.ctx }; } - return result; + const after = walkStmts(rest, walked.ctx); + return { stmts: [walked.stmt].concat(after.stmts), ctx: after.ctx }; +} + +/** State-threaded `walkStmt` over each statement — unlike `walkStmts`, no + * list-level rules. Used for the let-cond expansion, whose pieces are + * re-walked individually. */ +function walkEach(stmts: TStmt[], ctx: CondCtx): StmtsOut { + if (stmts.length === 0) return { stmts: [], ctx }; + const h = walkStmt(stmts[0], ctx); + const t = walkEach(stmts.slice(1), h.ctx); + return { stmts: [h.stmt].concat(t.stmts), ctx: t.ctx }; } -function recurseStmt(s: TStmt, ctx: CondCtx): TStmt { - const re = (x: TExpr) => walkExpr(x, ctx); - const rs = (x: TStmt[]) => walkStmts(x, ctx); +function recurseStmt(s: TStmt, ctx: CondCtx): StmtOut { switch (s.kind) { - case "let": return { ...s, init: re(s.init) }; - case "assign": return { ...s, value: re(s.value) }; - case "return": return { ...s, value: re(s.value) }; - case "break": case "continue": case "throw": return s; - case "expr": return { ...s, expr: re(s.expr) }; - case "if": return { ...s, cond: re(s.cond), then: rs(s.then), else: rs(s.else) }; - case "while": return { ...s, cond: re(s.cond), - invariants: s.invariants.map(re), - decreases: s.decreases ? re(s.decreases) : null, - doneWith: s.doneWith ? re(s.doneWith) : null, - body: rs(s.body) }; - case "switch": return { ...s, expr: re(s.expr), - cases: s.cases.map(c => ({ ...c, body: rs(c.body) })), - defaultBody: rs(s.defaultBody) }; - case "forof": return { ...s, iterable: re(s.iterable), - invariants: s.invariants.map(re), - doneWith: s.doneWith ? re(s.doneWith) : null, - body: rs(s.body) }; - case "ghostLet": return { ...s, init: re(s.init) }; - case "ghostAssign": return { ...s, value: re(s.value) }; - case "assert": return { ...s, expr: re(s.expr) }; - case "someMatch": return { ...s, someBody: rs(s.someBody), noneBody: rs(s.noneBody) }; - case "tagMatch": return { ...s, scrutinee: re(s.scrutinee), - cases: s.cases.map(c => ({ ...c, body: rs(c.body) })), - fallthrough: rs(s.fallthrough) }; + case "let": { + const init = walkExpr(s.init, ctx); + return { stmt: { ...s, init: init.expr }, ctx: init.ctx }; + } + case "assign": { + const v = walkExpr(s.value, ctx); + return { stmt: { ...s, value: v.expr }, ctx: v.ctx }; + } + case "return": { + const v = walkExpr(s.value, ctx); + return { stmt: { ...s, value: v.expr }, ctx: v.ctx }; + } + case "break": case "continue": case "throw": return { stmt: s, ctx }; + case "expr": { + const x = walkExpr(s.expr, ctx); + return { stmt: { ...s, expr: x.expr }, ctx: x.ctx }; + } + case "if": { + const cond = walkExpr(s.cond, ctx); + const then = walkStmts(s.then, cond.ctx); + const els = walkStmts(s.else, then.ctx); + return { stmt: { ...s, cond: cond.expr, then: then.stmts, else: els.stmts }, ctx: els.ctx }; + } + case "while": { + const cond = walkExpr(s.cond, ctx); + const invs = walkExprs(s.invariants, cond.ctx); + let dec: ExprOut | null = null; + if (s.decreases !== null) dec = walkExpr(s.decreases, invs.ctx); + let dw: ExprOut | null = null; + if (s.doneWith !== null) dw = walkExpr(s.doneWith, dec ? dec.ctx : invs.ctx); + const body = walkStmts(s.body, dw ? dw.ctx : dec ? dec.ctx : invs.ctx); + return { stmt: { ...s, cond: cond.expr, invariants: invs.exprs, + decreases: dec ? dec.expr : null, doneWith: dw ? dw.expr : null, + body: body.stmts }, ctx: body.ctx }; + } + case "switch": { + const x = walkExpr(s.expr, ctx); + const cases = walkSwitchCases(s.cases, x.ctx); + const def = walkStmts(s.defaultBody, cases.ctx); + return { stmt: { ...s, expr: x.expr, cases: cases.cases, defaultBody: def.stmts }, + ctx: def.ctx }; + } + case "forof": { + const it = walkExpr(s.iterable, ctx); + const invs = walkExprs(s.invariants, it.ctx); + let dw: ExprOut | null = null; + if (s.doneWith !== null) dw = walkExpr(s.doneWith, invs.ctx); + const body = walkStmts(s.body, dw ? dw.ctx : invs.ctx); + return { stmt: { ...s, iterable: it.expr, invariants: invs.exprs, + doneWith: dw ? dw.expr : null, body: body.stmts }, ctx: body.ctx }; + } + case "ghostLet": { + const init = walkExpr(s.init, ctx); + return { stmt: { ...s, init: init.expr }, ctx: init.ctx }; + } + case "ghostAssign": { + const v = walkExpr(s.value, ctx); + return { stmt: { ...s, value: v.expr }, ctx: v.ctx }; + } + case "assert": { + const x = walkExpr(s.expr, ctx); + return { stmt: { ...s, expr: x.expr }, ctx: x.ctx }; + } + case "someMatch": { + const some = walkStmts(s.someBody, ctx); + const none = walkStmts(s.noneBody, some.ctx); + return { stmt: { ...s, someBody: some.stmts, noneBody: none.stmts }, ctx: none.ctx }; + } + case "tagMatch": { + const scrut = walkExpr(s.scrutinee, ctx); + const cases = walkStmtCases(s.cases, scrut.ctx); + const ft = walkStmts(s.fallthrough, cases.ctx); + return { stmt: { ...s, scrutinee: scrut.expr, cases: cases.cases, + fallthrough: ft.stmts }, ctx: ft.ctx }; + } } } @@ -169,14 +361,14 @@ function isTerminating(stmts: TStmt[]): boolean { /** Driver: `if (e !== undefined) then else` — presence fact in if position. * → `someMatch e { Some(_e_val) => then, None => else }`. Requires a * non-empty Some branch. */ -function ruleIfOptionalSimple(s: TStmt): TStmt | null { +function ruleIfOptionalSimple(s: TStmt, ctx: CondCtx): StmtOut | null { if (s.kind !== "if") return null; const check = presentFact(s.cond); if (!check) return null; const someBody = check.negated ? s.else : s.then; const noneBody = check.negated ? s.then : s.else; if (someBody.length === 0) return null; - return presentMatchStmts(check, someBody, noneBody); + return { stmt: presentMatchStmts(check, someBody, noneBody), ctx }; } /** Driver: `if (e === undefined) terminate; rest` — presence fact in @@ -194,13 +386,31 @@ function ruleEarlyReturnConsume(s: TStmt, rest: TStmt[]): TStmt | null { return presentMatchStmts(check, rest, noneBranch); } +/** Left-nested `||` of non-empty leaves — the inverse of `flattenOr`. */ +function orChain(leaves: TExpr[]): TExpr { + //@ requires leaves.length > 0 + let acc: TExpr = leaves[0]; + for (let i = 1; i < leaves.length; i++) { + acc = { kind: "binop", op: "||", left: acc, right: leaves[i], ty: { kind: "bool" } }; + } + return acc; +} + /** Driver: `if (D1 || … || Dn) terminate; rest` — De-Morgan position. Each `Di` * that detects some optional `x` is None (`!x`, `x === undefined`, * `x?.chain !== lit`) narrows that `x` to Some across `rest`; the rest — * value guards reading a narrowed `x` directly, plus the detectors' Some-case * residuals — become a trailing early-return. Sound: reaching `rest` means - * every disjunct was false, so every detected optional is present. */ -function ruleEarlyReturnOrChain(s: TStmt, rest: TStmt[], ctx: CondCtx): TStmt | null { + * every disjunct was false, so every detected optional is present. + * + * Returns a PRE-WALKED result, unlike the other consumed rules, and + * walkStmts must not walk it again. The terminating branch is duplicated + * into every None arm, and a decreasing termination measure exists only + * when duplicated statements are already walked — walked output is inert, + * while un-walked material weighs in once per copy. Walk order mirrors + * recurseStmt on someMatch (someBody before noneBody, innermost arms + * first), so minted binder numbering agrees with machine-walked nests. */ +function ruleEarlyReturnOrChain(s: TStmt, rest: TStmt[], ctx: CondCtx): StmtOut | null { if (s.kind !== "if") return null; if (rest.length === 0) return null; if (s.then.length === 0 || s.else.length !== 0 || !isTerminating(s.then)) return null; @@ -214,22 +424,39 @@ function ruleEarlyReturnOrChain(s: TStmt, rest: TStmt[], ctx: CondCtx): TStmt | for (const leaf of leaves) { const d = noneDetector(leaf, ctx); if (!d) { residualLeaves.push(leaf); continue; } - const key = binderHintFor(d.scrutinee)!; - if (seen.has(key)) return null; // two detectors on one optional: rare; leave to other rules + const key = binderHintFor(d.scrutinee); + // A keyless detector (unreachable: detector scrutinees are path-shaped) + // or a second detector on an already-bound optional demotes to a + // residual — the substitution inside the arm reads the bound value, so + // the residual guard keeps its meaning. Termination needs demotion, not + // a bail: a bailed head could be re-armed by walking (a consumed + // optChain dropping one of two same-key detectors) and would then carry + // firable weight the walk never revisits. + if (key === null || seen.has(key)) { residualLeaves.push(leaf); continue; } seen.add(key); detectors.push(d); if (d.residual) residualLeaves.push(d.residual); } if (detectors.length === 0) return null; - let inner: TStmt[] = residualLeaves.length === 0 - ? rest - : [{ kind: "if", cond: residualLeaves.reduce((a, b): TExpr => ({ kind: "binop", op: "||", left: a, right: b, ty: { kind: "bool" } })), then: s.then, else: [] }, ...rest]; + // Statement form: a ternary would let transform lift the orChain call above + // the emptiness guard, where its non-empty precondition can't hold. + let inner: TStmt[] = rest; + if (residualLeaves.length > 0) { + inner = [{ kind: "if", cond: orChain(residualLeaves), then: s.then, else: [] }, ...rest]; + } + const wInner = walkStmts(inner, ctx); + let nest: TStmt[] = wInner.stmts; + let c = wInner.ctx; for (let i = detectors.length - 1; i >= 0; i--) { + // Non-empty after any iteration; the loop runs at least once (detectors ≠ []). + //@ invariant nest.length > 0 || i === detectors.length - 1 const d = detectors[i]!; - inner = [{ kind: "someMatch", scrutinee: d.scrutinee, binderTy: d.innerTy, binder: d.binder, someBody: inner, noneBody: s.then }]; + const wThen = walkStmts(s.then, c); + c = wThen.ctx; + nest = [{ kind: "someMatch", scrutinee: d.scrutinee, binderTy: d.innerTy, binder: d.binder, someBody: nest, noneBody: wThen.stmts }]; } - return inner[0]; + return { stmt: nest[0], ctx: c }; } /** Driver: `if (opt?.chain !== lit) terminate; rest` — bound-optional @@ -246,16 +473,18 @@ function ruleEarlyReturnOptChainCompare(s: TStmt, rest: TStmt[], ctx: CondCtx): if (s.else.length !== 0 || !isTerminating(s.then)) return null; const c = s.cond; if (c.kind !== "binop" || c.op !== "!==") return null; - const oc = c.left.kind === "optChain" ? c.left : c.right.kind === "optChain" ? c.right : null; - if (!oc || oc.kind !== "optChain" || oc.obj.ty.kind !== "optional") return null; - const lit = c.left === oc ? c.right : c.left; + const ocOnLeft = c.left.kind === "optChain"; + const oc = ocOnLeft ? c.left : c.right.kind === "optChain" ? c.right : null; + if (oc === null) return null; + if (oc.kind !== "optChain") return null; + if (oc.obj.ty.kind !== "optional") return null; + const lit = ocOnLeft ? c.right : c.left; const innerTy = oc.obj.ty.inner; const hint = binderHintFor(oc.obj); if (hint === null) return null; const binder = freshName(hint); const binderVar: TExpr = { kind: "var", name: binder, ty: innerTy }; - const unwrapped = applyChain(binderVar, oc.chain); - restoreDiscriminantFlag(unwrapped, ctx.decls); + const unwrapped = restoreDiscriminantFlag(applyChain(binderVar, oc.chain), ctx.decls); const innerGuard: TExpr = { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } }; // Keep `rest` as trailing statements (not an else branch) — `s.then` terminates, // so `if (g) terminate; rest` ≡ `if (g) terminate else rest`, and the trailing @@ -267,13 +496,13 @@ function ruleEarlyReturnOptChainCompare(s: TStmt, rest: TStmt[], ctx: CondCtx): /** Driver (expression): `e !== undefined ? a : b` — presence fact in * ternary position. */ -function ruleConditionalOptionalSimple(e: TExpr): TExpr | null { +function ruleConditionalOptionalSimple(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "conditional") return null; const check = presentFact(e.cond); if (!check) return null; const someBody = check.negated ? e.else : e.then; const noneBody = check.negated ? e.then : e.else; - return presentMatchExpr(check, someBody, noneBody, e.ty); + return { expr: presentMatchExpr(check, someBody, noneBody, e.ty), ctx }; } /** Driver (expression): `(path !== undefined [&& rest]) ==> B` — presence @@ -283,9 +512,11 @@ function ruleConditionalOptionalSimple(e: TExpr): TExpr | null { * Walks the inner ==> recursively so chained checks become nested * someMatches. Spec form: no falsy gate (historical shape — presence * suffices in the premise). */ -function ruleImplOptional(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleImplOptional(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "binop" || e.op !== "==>") return null; - let check: PresentFact; + // No uninitialized let (it has no backend model), and presence guards are + // split from dependent reads so each early return narrows on its own. + let check: PresentFact | null = null; let restCond: TExpr | null = null; const extracted = leadingPresent(e.left); if (extracted) { @@ -293,19 +524,25 @@ function ruleImplOptional(e: TExpr, ctx: CondCtx): TExpr | null { restCond = extracted.restCond; } else { const c = presentFact(e.left); - if (!c || c.negated) return null; + if (c === null) return null; + if (c.negated) return null; check = c; } + if (check === null) return null; const innerBody: TExpr = restCond ? { kind: "binop", op: "==>", left: restCond, right: e.right, ty: { kind: "bool" } } : e.right; + const w = walkExpr(innerBody, ctx); return { - kind: "someMatch", - scrutinee: check.scrutinee, binderTy: check.innerTy, - binder: check.binder, - someBody: walkExpr(innerBody, ctx), - noneBody: { kind: "bool", value: true, ty: { kind: "bool" } }, - ty: { kind: "bool" }, + expr: { + kind: "someMatch", + scrutinee: check.scrutinee, binderTy: check.innerTy, + binder: check.binder, + someBody: w.expr, + noneBody: { kind: "bool", value: true, ty: { kind: "bool" } }, + ty: { kind: "bool" }, + }, + ctx: w.ctx, }; } @@ -313,17 +550,20 @@ function ruleImplOptional(e: TExpr, ctx: CondCtx): TExpr | null { * → `someMatch left { Some(_v) => _v, None => right }`. * Single-evaluation: scrutinee may be any expression. Presence-only * semantics (`??` tests null/undefined, not falsiness) — no falsy gate. */ -function ruleNullish(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleNullish(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "nullish") return null; if (e.left.ty.kind !== "optional") return null; const innerTy = e.left.ty.inner; - const binder = freshOcBinder(ctx); + const b = freshOcBinder(ctx); return { - kind: "someMatch", - scrutinee: e.left, binder, binderTy: innerTy, - someBody: { kind: "var", name: binder, ty: innerTy }, - noneBody: e.right, - ty: e.ty, + expr: { + kind: "someMatch", + scrutinee: e.left, binder: b.name, binderTy: innerTy, + someBody: { kind: "var", name: b.name, ty: innerTy }, + noneBody: e.right, + ty: e.ty, + }, + ctx: b.ctx, }; } @@ -333,12 +573,12 @@ function ruleNullish(e: TExpr, ctx: CondCtx): TExpr | null { * → `(0 <= i && i < arr.length) ? arr[i] : right`. The guarded `then` * keeps the seq index in bounds for the backend. (Map index is already * optional-typed and handled by ruleNullish above.) */ -function ruleNullishIndex(e: TExpr): TExpr | null { +function ruleNullishIndex(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "nullish") return null; if (e.left.kind !== "index") return null; if (e.left.obj.ty.kind !== "array") return null; const cond = arrayBoundsCond(e.left.obj, e.left.idx); - return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty }; + return { expr: { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty }, ctx }; } /** Driver (expression): `arr[i]?.` — in-bounds fact in optChain @@ -347,14 +587,14 @@ function ruleNullishIndex(e: TExpr): TExpr | null { * conditional's optional type makes transform wrap the in-bounds chain * result in Some and the OOB branch in None. (ruleOptChain itself bails * here: an array index is typed as the non-optional element type.) */ -function ruleOptChainIndex(e: TExpr): TExpr | null { +function ruleOptChainIndex(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "optChain") return null; if (e.obj.kind !== "index") return null; if (e.obj.obj.ty.kind !== "array") return null; const cond = arrayBoundsCond(e.obj.obj, e.obj.idx); const body = applyChain(e.obj, e.chain); // arr[i] — in bounds under `cond` const undef: TExpr = { kind: "var", name: "undefined", ty: { kind: "void" } }; - return { kind: "conditional", cond, then: body, else: undef, ty: e.ty }; + return { expr: { kind: "conditional", cond, then: body, else: undef, ty: e.ty }, ctx }; } /** Driver (statement): reconcile a `const e = arr[i]` whose binding is optional @@ -364,7 +604,7 @@ function ruleOptChainIndex(e: TExpr): TExpr | null { * `Option` and a later `e?.f` someMatch is well-typed; an in-bounds proof * makes the None branch dead. Skipped when the element type is already * optional: no mismatch. */ -function ruleOptionalIndexBinding(s: TStmt): TStmt | null { +function ruleOptionalIndexBinding(s: TStmt, ctx: CondCtx): StmtOut | null { if (s.kind !== "let") return null; if (s.ty.kind !== "optional") return null; const init = s.init; @@ -374,24 +614,27 @@ function ruleOptionalIndexBinding(s: TStmt): TStmt | null { const cond = arrayBoundsCond(init.obj, init.idx); const undef: TExpr = { kind: "var", name: "undefined", ty: { kind: "void" } }; const guarded: TExpr = { kind: "conditional", cond, then: init, else: undef, ty: s.ty }; - return { ...s, init: guarded }; + return { stmt: { ...s, init: guarded }, ctx }; } /** Driver (expression): `obj?.` — single-eval optional chain. * → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`. * The someBody applies the chain to the binder directly (field/call/index), * so transform doesn't substitute. Scrutinee can be any expression. */ -function ruleOptChain(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleOptChain(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "optChain") return null; if (e.obj.ty.kind !== "optional") return null; const innerTy = e.obj.ty.inner; - const binder = freshOcBinder(ctx); - const body = applyChain({ kind: "var", name: binder, ty: innerTy }, e.chain); + const b = freshOcBinder(ctx); + const body = applyChain({ kind: "var", name: b.name, ty: innerTy }, e.chain); const noneBody: TExpr = { kind: "var", name: "undefined", ty: { kind: "void" } }; return { - kind: "someMatch", - scrutinee: e.obj, binder, binderTy: innerTy, - someBody: body, noneBody, ty: e.ty, + expr: { + kind: "someMatch", + scrutinee: e.obj, binder: b.name, binderTy: innerTy, + someBody: body, noneBody, ty: e.ty, + }, + ctx: b.ctx, }; } @@ -400,7 +643,7 @@ function ruleOptChain(e: TExpr, ctx: CondCtx): TExpr | null { * → `someMatch m[k] { Some(_m_k_val) => _m_k_val, None => default }`. * The existing Dafny peephole collapses the result to * `if k in m then m[k] else default`. */ -function ruleConditionalInMap(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleConditionalInMap(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "conditional") return null; if (e.cond.kind !== "binop" || e.cond.op !== "in") return null; const m = e.cond.right; @@ -417,29 +660,35 @@ function ruleConditionalInMap(e: TExpr, ctx: CondCtx): TExpr | null { // the enclosing expression is already a plain if-then-else of the correct type. if (e.then.ty.kind !== "optional") return null; const innerTy = m.ty.value; - const binder = binderHintForMapAccess(m, k, ctx); + const b = binderHintForMapAccess(m, k, ctx); return { - kind: "someMatch", - scrutinee: e.then, binder, binderTy: innerTy, - someBody: { kind: "var", name: binder, ty: innerTy }, - noneBody: e.else, ty: innerTy, + expr: { + kind: "someMatch", + scrutinee: e.then, binder: b.name, binderTy: innerTy, + someBody: { kind: "var", name: b.name, ty: innerTy }, + noneBody: e.else, ty: innerTy, + }, + ctx: b.ctx, }; } /** Driver (expression): `opt ? a : b` (truthiness — cond itself is optional). * Only fires for simple var or simple `obj.field` cond. Historical shape: * no falsy gate on the bound value (unlike the other truthiness positions). */ -function ruleConditionalOptionalTruthy(e: TExpr): TExpr | null { +function ruleConditionalOptionalTruthy(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "conditional") return null; if (e.cond.ty.kind !== "optional") return null; const hint = binderHintFor(e.cond); if (hint === null) return null; const binder = freshName(hint); return { - kind: "someMatch", - scrutinee: e.cond, binderTy: e.cond.ty.inner, - binder, - someBody: e.then, noneBody: e.else, ty: e.ty, + expr: { + kind: "someMatch", + scrutinee: e.cond, binderTy: e.cond.ty.inner, + binder, + someBody: e.then, noneBody: e.else, ty: e.ty, + }, + ctx, }; } @@ -448,14 +697,15 @@ function ruleConditionalOptionalTruthy(e: TExpr): TExpr | null { * → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`. * Walks the inner if back through narrow so that nested optional checks in * rest also become someMatches. */ -function ruleIfAndOptional(s: TStmt, ctx: CondCtx): TStmt | null { +function ruleIfAndOptional(s: TStmt, ctx: CondCtx): StmtOut | null { if (s.kind !== "if") return null; if (s.else.length !== 0) return null; const extracted = leadingPresent(s.cond); if (!extracted) return null; const { check, restCond } = extracted; const innerIf: TStmt = { kind: "if", cond: restCond, then: s.then, else: [] }; - return presentMatchStmts(check, [walkStmt(innerIf, ctx)], []); + const w = walkStmt(innerIf, ctx); + return { stmt: presentMatchStmts(check, [w.stmt], []), ctx: w.ctx }; } /** Driver: a bare expression statement `x !== undefined && rest` (the @@ -465,14 +715,15 @@ function ruleIfAndOptional(s: TStmt, ctx: CondCtx): TStmt | null { * driver (`ruleConditionalAndOptional`), a method call in `rest` is fine * here: a statement-level someMatch arm keeps it in statement position, so * transform never ANF-lifts it out of the arm. */ -function ruleExprStmtAndOptional(s: TStmt, ctx: CondCtx): TStmt | null { +function ruleExprStmtAndOptional(s: TStmt, ctx: CondCtx): StmtOut | null { if (s.kind !== "expr") return null; if (s.expr.kind !== "binop" || s.expr.op !== "&&") return null; const extracted = leadingPresent(s.expr); if (!extracted) return null; const { check, restCond } = extracted; const innerStmt: TStmt = { kind: "expr", expr: restCond }; - return presentMatchStmts(check, [walkStmt(innerStmt, ctx)], []); + const w = walkStmt(innerStmt, ctx); + return { stmt: presentMatchStmts(check, [w.stmt], []), ctx: w.ctx }; } /** Driver (statement): `let x = (e_opt && rest) ? a : b` — presence fact in @@ -501,9 +752,10 @@ function ruleLetCondAndOptional(s: TStmt): TStmt[] | null { * (they lower to pure Dafny expressions: `x in arr`, `x in m`, `s.Keys`, * …) are exempt even though they carry `callKind: "method"`. */ function containsMethodCall(e: TExpr): boolean { - if (e.kind === "call" && e.callKind === "method" && - !(e.builtinId !== undefined && builtinSpec(e.builtinId).pure)) { - return true; + if (e.kind === "call" && e.callKind === "method") { + const bid = e.builtinId; + if (bid === undefined) return true; + if (!builtinPure(bid)) return true; } switch (e.kind) { case "var": case "num": case "bigint": case "str": case "bool": @@ -540,7 +792,7 @@ function containsMethodCall(e: TExpr): boolean { * calls — transform lifts those out of the match arm, breaking the binder * scope. The original transform's let-desugar handles those by lifting to * a mutable var first. */ -function ruleConditionalAndOptional(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleConditionalAndOptional(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "conditional") return null; const extracted = leadingPresent(e.cond); if (!extracted) return null; @@ -550,7 +802,8 @@ function ruleConditionalAndOptional(e: TExpr, ctx: CondCtx): TExpr | null { kind: "conditional", cond: restCond, then: e.then, else: e.else, ty: e.ty, }; - return presentMatchExpr(check, walkExpr(innerCond, ctx), e.else, e.ty); + const w = walkExpr(innerCond, ctx); + return { expr: presentMatchExpr(check, w.expr, e.else, e.ty), ctx: w.ctx }; } // ── Variant drivers (discriminants and synth array-unions) ── @@ -559,7 +812,7 @@ function ruleConditionalAndOptional(e: TExpr, ctx: CondCtx): TExpr | null { * isArray fact in implication position (spec premises). * → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch). * The other variant becomes a vacuous-true fallthrough. */ -function ruleImplArrayIsArray(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleImplArrayIsArray(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "binop" || e.op !== "==>") return null; const pos = isArrayFact(e.left, ctx); const neg = e.left.kind === "unop" && e.left.op === "!" @@ -567,13 +820,17 @@ function ruleImplArrayIsArray(e: TExpr, ctx: CondCtx): TExpr | null { : null; const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" as const } : null); if (!matched) return null; + const w = walkExpr(e.right, ctx); return { - kind: "tagMatch", - scrutinee: matched.scrutinee, - typeName: matched.typeName, - cases: [{ variant: matched.variant, body: walkExpr(e.right, ctx) }], - fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } }, - ty: { kind: "bool" }, + expr: { + kind: "tagMatch", + scrutinee: matched.scrutinee, + typeName: matched.typeName, + cases: [{ variant: matched.variant, body: w.expr }], + fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } }, + ty: { kind: "bool" }, + }, + ctx: w.ctx, }; } @@ -582,27 +839,41 @@ function ruleImplArrayIsArray(e: TExpr, ctx: CondCtx): TExpr | null { * → `tagMatch x { => walkExpr(then-side) } fallthrough walkExpr(else-side)`. * Inside the matched arm, bare references to `x` are rewritten to the * variant's payload field by `transformExpr` when emitting the tagMatch. */ -function ruleConditionalArrayIsArray(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleConditionalArrayIsArray(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "conditional") return null; const pos = isArrayFact(e.cond, ctx); // `typeof x === "string"` is a positive check like `Array.isArray`, but selects // the NonArrayBranch — its then-branch is the matched-variant body. - const tof = pos ? null : typeofStringFact(e.cond, ctx); - const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!" - ? isArrayFact(e.cond.expr, ctx) - : null; + // Statement form with explicit null tests (not `pos ? … : …` truthiness or + // `!pos && !tof && …` chains): optional truthiness inside `&&` lowers to + // match-expression operands, a shape Dafny 4.11's translator asserts on. + let tof: IsArrayFact | null = null; + if (pos === null) tof = typeofStringFact(e.cond, ctx); + let neg: IsArrayFact | null = null; + if (pos === null) { + if (tof === null) { + if (e.cond.kind === "unop" && e.cond.op === "!") { + neg = isArrayFact(e.cond.expr, ctx); + } + } + } const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" as const } : null); if (!matched) return null; const positive = pos ?? tof; const thenBody = positive ? e.then : e.else; const elseBody = positive ? e.else : e.then; + const wThen = walkExpr(thenBody, ctx); + const wElse = walkExpr(elseBody, wThen.ctx); return { - kind: "tagMatch", - scrutinee: matched.scrutinee, - typeName: matched.typeName, - cases: [{ variant: matched.variant, body: walkExpr(thenBody, ctx) }], - fallthrough: walkExpr(elseBody, ctx), - ty: e.ty, + expr: { + kind: "tagMatch", + scrutinee: matched.scrutinee, + typeName: matched.typeName, + cases: [{ variant: matched.variant, body: wThen.expr }], + fallthrough: wElse.expr, + ty: e.ty, + }, + ctx: wElse.ctx, }; } @@ -611,27 +882,31 @@ function ruleConditionalArrayIsArray(e: TExpr, ctx: CondCtx): TExpr | null { * → `tagMatch path { ArrayBranch => if () then [else] }`. * The remaining conjuncts move inside the matched arm so any narrowing the * `then` body relies on (typed `path` accesses) sees the unwrapped variant. */ -function ruleIfAndArrayIsArray(s: TStmt, ctx: CondCtx): TStmt | null { +function ruleIfAndArrayIsArray(s: TStmt, ctx: CondCtx): StmtOut | null { if (s.kind !== "if") return null; const extracted = leadingIsArray(s.cond, ctx); if (!extracted) return null; const { check, restCond } = extracted; // Inner if uses the remaining conjunction. Walk recursively so nested // checks compose. - const innerThen: TStmt[] = [{ kind: "if", cond: restCond, then: s.then, else: s.else }]; + const innerIf: TStmt = { kind: "if", cond: restCond, then: s.then, else: s.else }; + const w = walkStmt(innerIf, ctx); return { - kind: "tagMatch", - scrutinee: check.scrutinee, - typeName: check.typeName, - cases: [{ variant: check.variant, body: innerThen.map(x => walkStmt(x, ctx)) }], - fallthrough: s.else, + stmt: { + kind: "tagMatch", + scrutinee: check.scrutinee, + typeName: check.typeName, + cases: [{ variant: check.variant, body: [w.stmt] }], + fallthrough: s.else, + }, + ctx: w.ctx, }; } /** Driver (expression): `( && Array.isArray(path)) ? a : b` — isArray * fact in guarded-ternary position. * → `tagMatch path { ArrayBranch => () ? a : b } fallthrough b`. */ -function ruleConditionalAndArrayIsArray(e: TExpr, ctx: CondCtx): TExpr | null { +function ruleConditionalAndArrayIsArray(e: TExpr, ctx: CondCtx): ExprOut | null { if (e.kind !== "conditional") return null; const extracted = leadingIsArray(e.cond, ctx); if (!extracted) return null; @@ -640,49 +915,83 @@ function ruleConditionalAndArrayIsArray(e: TExpr, ctx: CondCtx): TExpr | null { kind: "conditional", cond: restCond, then: e.then, else: e.else, ty: e.ty, }; + const w = walkExpr(innerCond, ctx); return { - kind: "tagMatch", - scrutinee: check.scrutinee, - typeName: check.typeName, - cases: [{ variant: check.variant, body: walkExpr(innerCond, ctx) }], - fallthrough: e.else, - ty: e.ty, + expr: { + kind: "tagMatch", + scrutinee: check.scrutinee, + typeName: check.typeName, + cases: [{ variant: check.variant, body: w.expr }], + fallthrough: e.else, + ty: e.ty, + }, + ctx: w.ctx, }; } +/** An if-else-if chain flattened against one discriminator: the cases + * collected off the else spine, plus the fallthrough that ends it. */ +interface ElseChain { cases: TStmtCase[]; fallthrough: TStmt[] } + +/** Flatten an else-spine of `if (x.kind === "v")` tests on `scrutineeName` + * into cases; the first non-matching statement (or multi-statement else) + * becomes the fallthrough. */ +function collectElseChain(s: TStmt, scrutineeName: string, ctx: CondCtx): ElseChain { + if (s.kind !== "if") return { cases: [], fallthrough: [s] }; + const p = variantFact(s.cond, ctx); + if (!p || p.scrutineeName !== scrutineeName) return { cases: [], fallthrough: [s] }; + const here: TStmtCase = { variant: p.variant, body: s.then }; + if (s.else.length === 0) return { cases: [here], fallthrough: [] }; + if (s.else.length === 1 && s.else[0].kind === "if") { + const rest = collectElseChain(s.else[0], scrutineeName, ctx); + return { cases: [here].concat(rest.cases), fallthrough: rest.fallthrough }; + } + return { cases: [here], fallthrough: s.else }; +} + +/** A list-level rewrite: the replacement statement plus how many input + * statements it consumed. One named type shared by both discriminant rules + * so their `??` join has a single shape. */ +interface ConsumedRewrite { stmt: TStmt; consumed: number } + +/** Every case body ends in a terminator. (A recursive helper rather than + * `.every`, whose Dafny lowering pulls in the standard library — which is + * incompatible with the prover flags the termination proof needs.) */ +function allCasesTerminate(cases: TStmtCase[]): boolean { + if (cases.length === 0) return true; + if (!isTerminating(cases[0].body)) return false; + return allCasesTerminate(cases.slice(1)); +} + /** Driver (list-level): consecutive `if (x.kind === "v") ...` chain → * tagMatch. Walks consecutive top-level ifs on the same discriminator var; * the first one with an else-branch ends the chain (else becomes * fallthrough; if-else-if flattens into more cases). Returns the tagMatch * and how many stmts consumed. */ -function ruleDiscriminantChain(stmts: TStmt[], ctx: CondCtx): { stmt: TStmt; consumed: number } | null { +function ruleDiscriminantChain(stmts: TStmt[], ctx: CondCtx): ConsumedRewrite | null { + //@ ensures \result !== null ==> \result.consumed >= 0 && \result.consumed <= stmts.length if (stmts.length === 0 || stmts[0].kind !== "if") return null; const first = variantFact(stmts[0].cond, ctx); if (!first) return null; - const cases: { variant: string; body: TStmt[] }[] = []; - - function collectElse(s: TStmt & { kind: "if" }): TStmt[] { - const p = variantFact(s.cond, ctx); - if (!p || p.scrutinee.name !== first!.scrutinee.name) return [s]; - cases.push({ variant: p.variant, body: s.then }); - if (s.else.length === 0) return []; - if (s.else.length === 1 && s.else[0].kind === "if") return collectElse(s.else[0]); - return s.else; - } + const cases: TStmtCase[] = []; let consumed = 0; for (let i = 0; i < stmts.length; i++) { + //@ invariant consumed >= 0 && consumed <= stmts.length + //@ invariant i <= stmts.length const s = stmts[i]; if (s.kind !== "if") break; const p = variantFact(s.cond, ctx); - if (!p || p.scrutinee.name !== first.scrutinee.name) break; + if (!p || p.scrutineeName !== first.scrutineeName) break; cases.push({ variant: p.variant, body: s.then }); consumed = i + 1; if (s.else.length > 0) { - const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else; + const tail: ElseChain = (s.else.length === 1 && s.else[0].kind === "if") + ? collectElseChain(s.else[0], first.scrutineeName, ctx) + : { cases: [], fallthrough: s.else }; return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName, - cases, fallthrough: ft }, consumed }; + cases: cases.concat(tail.cases), fallthrough: tail.fallthrough }, consumed }; } } if (cases.length === 0) return null; @@ -690,7 +999,7 @@ function ruleDiscriminantChain(stmts: TStmt[], ctx: CondCtx): { stmt: TStmt; con // (preserving the clean dispatch-as-expression shape). Otherwise the tail runs // after the match for every variant, so leave it to the caller (empty default) // rather than mis-routing it into the default arm only. - if (cases.every(c => isTerminating(c.body))) { + if (allCasesTerminate(cases)) { return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName, cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length }; } @@ -700,7 +1009,8 @@ function ruleDiscriminantChain(stmts: TStmt[], ctx: CondCtx): { stmt: TStmt; con /** Driver (list-level): `if (x.kind !== "v") terminate; rest` → tagMatch * with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */ -function ruleDiscriminantNegEarlyReturn(stmts: TStmt[], ctx: CondCtx): { stmt: TStmt; consumed: number } | null { +function ruleDiscriminantNegEarlyReturn(stmts: TStmt[], ctx: CondCtx): ConsumedRewrite | null { + //@ ensures \result !== null ==> \result.consumed >= 0 && \result.consumed <= stmts.length if (stmts.length < 2) return null; const first = stmts[0]; if (first.kind !== "if" || first.else.length > 0) return null; @@ -714,25 +1024,63 @@ function ruleDiscriminantNegEarlyReturn(stmts: TStmt[], ctx: CondCtx): { stmt: T // ── Function / module entry ────────────────────────────────── -function narrowFunction(fn: TFunction, ctx: CondCtx): TFunction { +interface FunctionOut { fn: TFunction; ctx: CondCtx } + +function narrowFunction(fn: TFunction, ctx: CondCtx): FunctionOut { + const reqs = walkExprs(fn.requires, ctx); + const enss = walkExprs(fn.ensures, reqs.ctx); + let dec: ExprOut | null = null; + if (fn.decreases !== null) dec = walkExpr(fn.decreases, enss.ctx); + const body = walkStmts(fn.body, dec ? dec.ctx : enss.ctx); return { - ...fn, - requires: fn.requires.map(e => walkExpr(e, ctx)), - ensures: fn.ensures.map(e => walkExpr(e, ctx)), - decreases: fn.decreases ? walkExpr(fn.decreases, ctx) : null, - body: walkStmts(fn.body, ctx), + fn: { + ...fn, + requires: reqs.exprs, + ensures: enss.exprs, + decreases: dec ? dec.expr : null, + body: body.stmts, + }, + ctx: body.ctx, }; } +interface ConstantsOut { constants: TConst[]; ctx: CondCtx } + +function narrowConstants(constants: TConst[], ctx: CondCtx): ConstantsOut { + if (constants.length === 0) return { constants: [], ctx }; + const v = walkExpr(constants[0].value, ctx); + const rest = narrowConstants(constants.slice(1), v.ctx); + return { constants: [{ ...constants[0], value: v.expr }].concat(rest.constants), ctx: rest.ctx }; +} + +interface FunctionsOut { functions: TFunction[]; ctx: CondCtx } + +function narrowFunctions(fns: TFunction[], ctx: CondCtx): FunctionsOut { + if (fns.length === 0) return { functions: [], ctx }; + const f = narrowFunction(fns[0], ctx); + const rest = narrowFunctions(fns.slice(1), f.ctx); + return { functions: [f.fn].concat(rest.functions), ctx: rest.ctx }; +} + +interface ClassesOut { classes: TClass[]; ctx: CondCtx } + +function narrowClasses(classes: TClass[], ctx: CondCtx): ClassesOut { + if (classes.length === 0) return { classes: [], ctx }; + const methods = narrowFunctions(classes[0].methods, ctx); + const rest = narrowClasses(classes.slice(1), methods.ctx); + return { classes: [{ ...classes[0], methods: methods.functions }].concat(rest.classes), + ctx: rest.ctx }; +} + export function narrowModule(mod: TModule): TModule { - const ctx: CondCtx = { decls: mod.typeDecls, oc: { n: 0 } }; + const ctx: CondCtx = { decls: mod.typeDecls, ocN: 0 }; + const consts = narrowConstants(mod.constants, ctx); + const fns = narrowFunctions(mod.functions, consts.ctx); + const classes = narrowClasses(mod.classes, fns.ctx); return { ...mod, - constants: mod.constants.map(c => ({ ...c, value: walkExpr(c.value, ctx) })), - functions: mod.functions.map(fn => narrowFunction(fn, ctx)), - classes: mod.classes.map(cls => ({ - ...cls, - methods: cls.methods.map(m => narrowFunction(m, ctx)), - })), + constants: consts.constants, + functions: fns.functions, + classes: classes.classes, }; } diff --git a/tools/src/peephole.dfy b/tools/src/peephole.dfy new file mode 100644 index 0000000..56c9685 --- /dev/null +++ b/tools/src/peephole.dfy @@ -0,0 +1,1990 @@ +// Generated by lsc from peephole.ts + +datatype Option = None | Some(value: T) + +function {:axiom} patternCtor(p: MatchPattern): Option + +function {:axiom} patternBinders(p: MatchPattern): seq + +function {:axiom} usesName(e: Expr, name: string): bool + +function {:axiom} usesNameInStmts(stmts: seq, name: string): bool + +datatype MethodCall = MethodCall(kind: string, obj: Expr, objTy: Ty, method_: string, args: seq, monadic: bool) + +datatype SomeNoneArms = SomeNoneArms(someArm: MatchArm, noneArm: MatchArm, binder: Option) + +datatype SomeNoneStmtArms = SomeNoneStmtArms(someArm: StmtMatchArm, noneArm: StmtMatchArm, binder: Option) + +datatype Backend = lean | dafny + +datatype Expr = var_(name: string) | num(value_num: int) | bigint(value_bigint: string) | bool_(value_bool: bool) | str(value_str: string) | constructor_(name: string, type_constructor: Option, args: seq) | binop(op: string, left: Expr, right: Expr) | unop(op: string, expr: Expr) | app(fn: string, args: seq, ctorOf: Option) | field(obj: Expr, field: string, fromUnion: Option, ctor: Option, datatypeField: Option) | toNat(expr: Expr) | toReal(expr: Expr) | index(arr: Expr, idx: Expr) | tupleLiteral(elems: seq) | tupleProj(obj: Expr, index: int, arity: int) | record(spread: Option, fields: seq, ctor: Option, ctorOf: Option) | arrayLiteral(elems: seq) | emptyMap | emptySet | mapLiteral(entries: seq) | methodCall(obj: Expr, objTy: Ty, method_: string, args: seq, monadic: bool) | lambda(params: seq, body_lambda: seq) | if_(cond: Expr, then_: Expr, else_: Expr) | match_(scrutinee: Expr, arms: seq) | forall_(var_: string, type_forall: Ty, body_forall: Expr) | exists_(var_: string, type_exists: Ty, body_exists: Expr) | implies(premises: seq, conclusion: Expr) | let(name: string, value_let: Expr, body_let: Expr) | havoc(type_havoc: Ty) | default(type_default: Ty) + +datatype MapEntry = MapEntry(key: Expr, value: Expr) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +datatype Param = Param(name: string, type_: Ty) + +datatype Stmt = let(name: string, type_: Ty, mutable: bool, value: Expr) | assign(target: string, value: Expr) | bind(target: string, value: Expr) | let_bind(name: string, value: Expr) | return_(value: Expr) | break_ | continue_ | if_(cond: Expr, then_: seq, else_: seq) | match_(scrutinee: Expr, arms: seq) | while_(cond: Expr, invariants: seq, decreasing: Option, doneWith: Option, body: seq) | forin(idx: string, bound: Expr, invariants: seq, body: seq) | ghostLet(name: string, type_: Ty, value: Expr) | ghostAssign(target: string, value: Expr) | assert_(expr: Expr, assumed: Option) + +datatype StmtMatchArm = StmtMatchArm(pattern: MatchPattern, body: seq) + +datatype MatchPattern = wild | ctor(ctor: string, binders: seq) + +datatype MatchArm = MatchArm(pattern: MatchPattern, body: Expr) + +datatype RecordField = RecordField(name: string, value: Expr) + +datatype Module = Module(comment: string, imports: seq, options: seq, decls: seq) + +datatype EmitOption = EmitOption(key: string, value: string) + +datatype Decl = inductive_(name: string, typeParams_inductive: Option>, constructors: seq, deriving: seq) | structure(name: string, typeParams_structure: Option>, fields: seq, deriving: seq) | def(name: string, typeParams_def: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_def: Expr) | def_by_method(name: string, typeParams_def_by_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) | method_(name: string, typeParams_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_method: seq) | namespace(name: string, decls: seq) | class_(name: string, fields: seq, methods: seq) | const_(name: string, type_: Ty, value: Expr) | type_alias(name: string, target: Ty) | opaque_type(name: string) | extern(name: string, typeParams_extern: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype Inductive = Inductive(kind: string, name: string, typeParams: Option>, constructors: seq, deriving: seq) + +datatype CtorInfo = CtorInfo(name: string, fields: seq) + +datatype Structure = Structure(kind: string, name: string, typeParams: Option>, fields: seq, deriving: seq) + +datatype FnDef = FnDef(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: Expr) + +datatype FnDefByMethod = FnDefByMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) + +datatype FnMethod = FnMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: seq) + +datatype Namespace = Namespace(kind: string, name: string, decls: seq) + +datatype ClassDecl = ClassDecl(kind: string, name: string, fields: seq, methods: seq) + +datatype ConstDecl = ConstDecl(kind: string, name: string, type_: Ty, value: Expr) + +datatype TypeAlias = TypeAlias(kind: string, name: string, target: Ty) + +datatype OpaqueType = OpaqueType(kind: string, name: string) + +datatype ExternDecl = ExternDecl(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype IsMapGetResult = IsMapGetResult(obj: Expr, key: Expr, objTy: Ty) + +function isMapGet(e: Expr): Option +{ + if ((((!e.methodCall?) || (!e.objTy.map_?)) || (e.method_ != "get")) || (|e.args| != 1)) then + None + else + Some(IsMapGetResult(e.obj, e.args[0], e.objTy)) +} + +function parseSomeBinder(p: MatchPattern): Option +{ + if (match patternCtor(p) { case Some(i_value) => (i_value != "some") case None => true }) then + None + else + var bs := patternBinders(p); + if (|bs| == 0) then + None + else + if (bs[0] == "_") then + Option.None + else + Option.Some(bs[0]) +} + +function getSomeNoneArms(arms: seq): Option +{ + if (|arms| != 2) then + None + else + var c0 := patternCtor(arms[0].pattern); + var c1 := patternCtor(arms[1].pattern); + var someArm := (if (match c0 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[1]) else Option.None)); + var noneArm := (if (match c0 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[1]) else Option.None)); + match someArm { + case Some(i_someArm_val) => + match noneArm { + case Some(i_noneArm_val) => + Some(SomeNoneArms(i_someArm_val, i_noneArm_val, parseSomeBinder(i_someArm_val.pattern))) + case None => + None + } + case None => + None + } +} + +function getSomeNoneStmtArms(arms: seq): Option +{ + if (|arms| != 2) then + None + else + var c0 := patternCtor(arms[0].pattern); + var c1 := patternCtor(arms[1].pattern); + var someArm := (if (match c0 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[1]) else Option.None)); + var noneArm := (if (match c0 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[1]) else Option.None)); + match someArm { + case Some(i_someArm_val) => + match noneArm { + case Some(i_noneArm_val) => + Some(SomeNoneStmtArms(i_someArm_val, i_noneArm_val, parseSomeBinder(i_someArm_val.pattern))) + case None => + None + } + case None => + None + } +} + +function ruleMatchOnMapGetExpr(e: Expr): Option +{ + match e { + case match_(i_e_scrutinee, i_e_arms) => + var get := isMapGet(i_e_scrutinee); + match get { + case Some(i_get_val) => + var arms := getSomeNoneArms(i_e_arms); + match arms { + case Some(i_arms_val) => + var idx := index(i_get_val.obj, i_get_val.key); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then Expr.let(i_arms_binder_val, idx, i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Expr.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case None => + None + } + case _ => + None + } +} + +function ruleIfFalseElseTrue(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if (isBool(i_e_then, false) && isBool(i_e_else, true)) then + Some(unop("¬", i_e_cond)) + else + None + case _ => + None + } +} + +function ruleIfIdentity(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if (isBool(i_e_then, true) && isBool(i_e_else, false)) then + Some(i_e_cond) + else + None + case _ => + None + } +} + +function ruleIfThenFalse(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if isBool(i_e_else, false) then + Some(binop("∧", i_e_cond, i_e_then)) + else + None + case _ => + None + } +} + +function ruleIfTrueElse(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if isBool(i_e_then, true) then + Some(binop("∨", i_e_cond, i_e_else)) + else + None + case _ => + None + } +} + +function ruleLetMatchOnMapGetExpr(e: Expr): Option +{ + match e { + case let(i_e_name, i_e_value, i_e_body) => + var get := isMapGet(i_e_value); + match get { + case Some(i_get_val) => + if (!i_e_body.match_?) then + None + else + var m := i_e_body; + var matchOnX := (m.scrutinee.var_? && (m.scrutinee.name == i_e_name)); + if !(matchOnX) then + None + else + var arms := getSomeNoneArms(m.arms); + match arms { + case Some(i_arms_val) => + if (usesName(i_arms_val.someArm.body, i_e_name) || usesName(i_arms_val.noneArm.body, i_e_name)) then + None + else + var idx := index(i_get_val.obj, i_get_val.key); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then Expr.let(i_arms_binder_val, idx, i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Expr.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case None => + None + } + case _ => + None + } +} + +function isBool(e: Expr, v: bool): bool +{ + (e.bool_? && (e.value_bool == v)) +} + +function applyExprRules(e: Expr, backend: Backend): Option +{ + var r1 := ruleMatchOnMapGetExpr(e); + match r1 { + case Some(i_r1_val) => + Some(i_r1_val) + case None => + var r6 := ruleLetMatchOnMapGetExpr(e); + match r6 { + case Some(i_r6_val) => + Some(i_r6_val) + case None => + if (!backend.dafny?) then + None + else + var r4 := ruleIfFalseElseTrue(e); + match r4 { + case Some(i_r4_val) => + Some(i_r4_val) + case None => + var r5 := ruleIfIdentity(e); + match r5 { + case Some(i_r5_val) => + Some(i_r5_val) + case None => + var r2 := ruleIfThenFalse(e); + match r2 { + case Some(i_r2_val) => + Some(i_r2_val) + case None => + var r3 := ruleIfTrueElse(e); + match r3 { + case Some(i_r3_val) => + Some(i_r3_val) + case None => + None + } + } + } + } + } + } +} + +function ruleMatchOnMapGetStmt(s: Stmt): Option +{ + match s { + case match_(i_s_scrutinee, i_s_arms) => + var get := isMapGet(i_s_scrutinee); + match get { + case Some(i_get_val) => + var arms := getSomeNoneStmtArms(i_s_arms); + match arms { + case Some(i_arms_val) => + var idx := index(i_get_val.obj, i_get_val.key); + var valTy := (if i_get_val.objTy.map_? then i_get_val.objTy.value else Ty.unknown); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then ([Stmt.let(i_arms_binder_val, valTy, false, idx)] + i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Stmt.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case None => + None + } + case _ => + None + } +} + +function tryLetMatchOnMapGet(s1: Stmt, s2: Stmt, restStmts: seq): Option +{ + if ((!s1.let?) || s1.mutable) then + None + else + var get := isMapGet(s1.value); + match get { + case Some(i_get_val) => + match s2 { + case match_(i_s2_scrutinee, i_s2_arms) => + var matchOnX := (i_s2_scrutinee.var_? && (i_s2_scrutinee.name == s1.name)); + if !(matchOnX) then + None + else + var arms := getSomeNoneStmtArms(i_s2_arms); + match arms { + case Some(i_arms_val) => + if usesNameInStmts(restStmts, s1.name) then + None + else + var idx := index(i_get_val.obj, i_get_val.key); + var valTy := (if i_get_val.objTy.map_? then i_get_val.objTy.value else Ty.unknown); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then ([Stmt.let(i_arms_binder_val, valTy, false, idx)] + i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Stmt.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case _ => + None + } + case None => + None + } +} + +function rewriteStmtListPairs(stmts: seq, backend: Backend): seq + requires elemsNormS(stmts, backend) + ensures |rewriteStmtListPairs(stmts, backend)| <= |stmts| + ensures elemsNormS(rewriteStmtListPairs(stmts, backend), backend) + ensures |rewriteStmtListPairs(stmts, backend)| == |stmts| ==> rewriteStmtListPairs(stmts, backend) == stmts && noPairRedex(stmts) + ensures |rewriteStmtListPairs(stmts, backend)| < |stmts| ==> !noPairRedex(stmts) + decreases awSs(stmts, backend), 0, |stmts|, 2 +{ + if (|stmts| == 0) then + [] + else + if (|stmts| >= 2) then + var merged := tryLetMatchOnMapGet(stmts[0], stmts[1], stmts[2..]); + match merged { + case Some(i_merged_val) => + assert normS(stmts[0], backend); + assert elemsNormS(stmts[1..], backend); + assert stmts[1..][0] == stmts[1]; + assert normS(stmts[1], backend); + assert stmts[1..][1..] == stmts[2..]; + assert elemsNormS(stmts[2..], backend); + TryLetAw(stmts[0], stmts[1], stmts[2..], backend); + NormAwZeroS(i_merged_val, backend); + ElemsNormSCons(peepholeStmt(i_merged_val, backend), rewriteStmtListPairs(stmts[2..], backend), backend); + ([peepholeStmt(i_merged_val, backend)] + rewriteStmtListPairs(stmts[2..], backend)) + case None => + assert elemsNormS(stmts[1..], backend); + ElemsNormSCons(stmts[0], rewriteStmtListPairs(stmts[1..], backend), backend); + assert |rewriteStmtListPairs(stmts[1..], backend)| == |stmts[1..]| ==> [stmts[0]] + rewriteStmtListPairs(stmts[1..], backend) == stmts; + ([stmts[0]] + rewriteStmtListPairs(stmts[1..], backend)) + } + else + assert elemsNormS(stmts[1..], backend); + ElemsNormSCons(stmts[0], rewriteStmtListPairs(stmts[1..], backend), backend); + assert |rewriteStmtListPairs(stmts[1..], backend)| == |stmts[1..]| ==> [stmts[0]] + rewriteStmtListPairs(stmts[1..], backend) == stmts; + ([stmts[0]] + rewriteStmtListPairs(stmts[1..], backend)) +} + +function pairScanToFix(stmts: seq, backend: Backend): seq + requires elemsNormS(stmts, backend) + ensures normSs(pairScanToFix(stmts, backend), backend) + ensures normSs(stmts, backend) ==> pairScanToFix(stmts, backend) == stmts + decreases if normSs(stmts, backend) then 0 else 2 * |stmts| + 1, 0, |stmts|, 3 +{ + ElemsNormAwCap(stmts, backend); + assert normSs(stmts, backend) ==> awSs(stmts, backend) == 0 by { + if normSs(stmts, backend) { NormSsAwZero(stmts, backend); } + } + var once := rewriteStmtListPairs(stmts, backend); + if (|once| < |stmts|) then + assert !noPairRedex(stmts); + assert normSs(stmts, backend) ==> noPairRedex(stmts) by { if normSs(stmts, backend) { NormSsElems(stmts, backend); } } + assert !normSs(stmts, backend); + pairScanToFix(once, backend) + else + assert once == stmts && noPairRedex(stmts); + NormSsSplit(stmts, backend); + once +} + +function peepholeStmts(stmts: seq, backend: Backend): seq + ensures normSs(peepholeStmts(stmts, backend), backend) + ensures normSs(stmts, backend) ==> peepholeStmts(stmts, backend) == stmts + decreases PSs(stmts, backend), szSs(stmts), |stmts|, 4 +{ + AwSsElems(stmts, backend); + SzSsElems(stmts); + NotNormSsAwPosAll(stmts, backend); + assert normSs(stmts, backend) ==> forall j | 0 <= j < |stmts| :: awS(stmts[j], backend) == 0 by { + if normSs(stmts, backend) { NormSsAllFacts(stmts, backend); } + } + var ys_ := seq(|stmts|, i_map requires 0 <= i_map < |stmts| => var s := stmts[i_map]; peepholeStmt(s, backend)); + assert forall j | 0 <= j < |stmts| :: ys_[j] == peepholeStmt(stmts[j], backend); + PointwiseElemsNormS(ys_, backend); + assert forall j | 0 <= j < |stmts| :: normS(stmts[j], backend) ==> ys_[j] == stmts[j]; + StmtsMapIdNormal(stmts, ys_, backend); + assert normSs(stmts, backend) ==> elemsNormS(stmts, backend) by { if normSs(stmts, backend) { NormSsElems(stmts, backend); } } + assert ys_ == seq(|stmts|, i_map requires 0 <= i_map < |stmts| => var s := stmts[i_map]; peepholeStmt(s, backend)); + pairScanToFix(seq(|stmts|, i_map requires 0 <= i_map < |stmts| => var s := stmts[i_map]; peepholeStmt(s, backend)), backend) +} + +function peepholeExpr(e: Expr, backend: Backend): Expr + ensures normE(peepholeExpr(e, backend), backend) + ensures normE(e, backend) ==> peepholeExpr(e, backend) == e + decreases awE(e, backend), szE(e), 0, 1 +{ + var cur := rewriteChildrenExpr(e, backend); + var hit := applyExprRules(cur, backend); + match hit { + case Some(i_hit_val) => + assert normE(e, backend) ==> normKidsE(e, backend); + assert !normE(e, backend); + NormKidsAwZeroE(cur, backend); + ApplyRulesAw(cur, backend); + assert awE(cur, backend) <= chargeE(e); + assert awE(e, backend) >= chargeE(e); + peepholeExpr(i_hit_val, backend) + case None => + assert normE(e, backend) ==> normKidsE(e, backend); + assert normKidsE(cur, backend); + assert applyExprRules(cur, backend).None?; + assert normE(cur, backend); + cur + } +} + +function rewriteChildrenExpr(e: Expr, backend: Backend): Expr + ensures normKidsE(rewriteChildrenExpr(e, backend), backend) + ensures chargeE(rewriteChildrenExpr(e, backend)) == chargeE(e) + ensures normKidsE(e, backend) ==> rewriteChildrenExpr(e, backend) == e + decreases awE(e, backend), szE(e), 0, 0 +{ + NormKidsAwZeroGuardE(e, backend); + match e { + case var_(i_e_name) => + e + case num(i_e_value) => + e + case bigint(i_e_value) => + e + case bool_(i_e_value) => + e + case str(i_e_value) => + e + case emptyMap => + e + case emptySet => + e + case havoc(i_e_type) => + e + case default(i_e_type) => + e + case constructor_(i_e_name, i_e_type, i_e_args) => + AwSeqEElems(i_e_args, backend); + SzSeqEElems(i_e_args); + var ys_ := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend)); + assert forall j | 0 <= j < |i_e_args| :: ys_[j] == peepholeExpr(i_e_args[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |i_e_args| :: normE(i_e_args[j], backend) ==> ys_[j] == i_e_args[j]; + SeqEMapIdNormal(i_e_args, ys_, backend); + e.(args := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend))) + case mapLiteral(i_e_entries) => + AwEntriesElems(i_e_entries, backend); + SzEntriesElems(i_e_entries); + var ys_ := seq(|i_e_entries|, i_map requires 0 <= i_map < |i_e_entries| => var en := i_e_entries[i_map]; MapEntry(peepholeExpr(en.key, backend), peepholeExpr(en.value, backend))); + assert forall j | 0 <= j < |i_e_entries| :: ys_[j] == MapEntry(peepholeExpr(i_e_entries[j].key, backend), peepholeExpr(i_e_entries[j].value, backend)); + PointwiseNormEntries(ys_, backend); + assert forall j | 0 <= j < |i_e_entries| :: (normE(i_e_entries[j].key, backend) ==> ys_[j].key == i_e_entries[j].key) && (normE(i_e_entries[j].value, backend) ==> ys_[j].value == i_e_entries[j].value); + EntriesMapIdNormal(i_e_entries, ys_, backend); + e.(entries := seq(|i_e_entries|, i_map requires 0 <= i_map < |i_e_entries| => var en := i_e_entries[i_map]; MapEntry(peepholeExpr(en.key, backend), peepholeExpr(en.value, backend)))) + case binop(i_e_op, i_e_left, i_e_right) => + e.(left := peepholeExpr(i_e_left, backend), right := peepholeExpr(i_e_right, backend)) + case unop(i_e_op, i_e_expr) => + e.(expr := peepholeExpr(i_e_expr, backend)) + case implies(i_e_premises, i_e_conclusion) => + AwSeqEElems(i_e_premises, backend); + SzSeqEElems(i_e_premises); + var ys_ := seq(|i_e_premises|, i_map requires 0 <= i_map < |i_e_premises| => var p := i_e_premises[i_map]; peepholeExpr(p, backend)); + assert forall j | 0 <= j < |i_e_premises| :: ys_[j] == peepholeExpr(i_e_premises[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |ys_| :: normE(ys_[j], backend); + assert forall j | 0 <= j < |i_e_premises| :: normE(i_e_premises[j], backend) ==> ys_[j] == i_e_premises[j]; + SeqEMapIdNormal(i_e_premises, ys_, backend); + e.(premises := seq(|i_e_premises|, i_map requires 0 <= i_map < |i_e_premises| => var p := i_e_premises[i_map]; peepholeExpr(p, backend)), conclusion := peepholeExpr(i_e_conclusion, backend)) + case app(i_e_fn, i_e_args, i_e_ctorOf) => + AwSeqEElems(i_e_args, backend); + SzSeqEElems(i_e_args); + var ys_ := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend)); + assert forall j | 0 <= j < |i_e_args| :: ys_[j] == peepholeExpr(i_e_args[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |ys_| :: normE(ys_[j], backend); + assert forall j | 0 <= j < |i_e_args| :: normE(i_e_args[j], backend) ==> ys_[j] == i_e_args[j]; + SeqEMapIdNormal(i_e_args, ys_, backend); + e.(args := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend))) + case field(i_e_obj, i_e_field, i_e_fromUnion, i_e_ctor, i_e_datatypeField) => + e.(obj := peepholeExpr(i_e_obj, backend)) + case toNat(i_e_expr) => + e.(expr := peepholeExpr(i_e_expr, backend)) + case toReal(i_e_expr) => + e.(expr := peepholeExpr(i_e_expr, backend)) + case index(i_e_arr, i_e_idx) => + e.(arr := peepholeExpr(i_e_arr, backend), idx := peepholeExpr(i_e_idx, backend)) + case tupleLiteral(i_e_elems) => + AwSeqEElems(i_e_elems, backend); + SzSeqEElems(i_e_elems); + var ys_ := seq(|i_e_elems|, i_map requires 0 <= i_map < |i_e_elems| => var el := i_e_elems[i_map]; peepholeExpr(el, backend)); + assert forall j | 0 <= j < |i_e_elems| :: ys_[j] == peepholeExpr(i_e_elems[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |ys_| :: normE(ys_[j], backend); + assert forall j | 0 <= j < |i_e_elems| :: normE(i_e_elems[j], backend) ==> ys_[j] == i_e_elems[j]; + SeqEMapIdNormal(i_e_elems, ys_, backend); + e.(elems := seq(|i_e_elems|, i_map requires 0 <= i_map < |i_e_elems| => var el := i_e_elems[i_map]; peepholeExpr(el, backend))) + case tupleProj(i_e_obj, i_e_index, i_e_arity) => + e.(obj := peepholeExpr(i_e_obj, backend)) + case record(i_e_spread, i_e_fields, i_e_ctor, i_e_ctorOf) => + AwFieldsElems(i_e_fields, backend); + SzFieldsElems(i_e_fields); + assert i_e_spread.Some? ==> awKidsE(e, backend) >= awE(i_e_spread.value, backend); + assert i_e_spread.Some? ==> szE(e) > szE(i_e_spread.value); + var ys_ := seq(|i_e_fields|, i_map requires 0 <= i_map < |i_e_fields| => var fi := i_e_fields[i_map]; fi.(value := peepholeExpr(fi.value, backend))); + assert forall j | 0 <= j < |i_e_fields| :: ys_[j].name == i_e_fields[j].name && ys_[j].value == peepholeExpr(i_e_fields[j].value, backend); + PointwiseNormFields(ys_, backend); + assert forall j | 0 <= j < |i_e_fields| :: normE(i_e_fields[j].value, backend) ==> ys_[j].value == i_e_fields[j].value; + FieldsMapIdNormal(i_e_fields, ys_, backend); + e.(spread := (match i_e_spread { case Some(i_e_spread_val) => Option.Some(peepholeExpr(i_e_spread_val, backend)) case None => Option.None }), fields := seq(|i_e_fields|, i_map requires 0 <= i_map < |i_e_fields| => var fi := i_e_fields[i_map]; fi.(value := peepholeExpr(fi.value, backend)))) + case arrayLiteral(i_e_elems) => + AwSeqEElems(i_e_elems, backend); + SzSeqEElems(i_e_elems); + var ys_ := seq(|i_e_elems|, i_map requires 0 <= i_map < |i_e_elems| => var el := i_e_elems[i_map]; peepholeExpr(el, backend)); + assert forall j | 0 <= j < |i_e_elems| :: ys_[j] == peepholeExpr(i_e_elems[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |ys_| :: normE(ys_[j], backend); + assert forall j | 0 <= j < |i_e_elems| :: normE(i_e_elems[j], backend) ==> ys_[j] == i_e_elems[j]; + SeqEMapIdNormal(i_e_elems, ys_, backend); + e.(elems := seq(|i_e_elems|, i_map requires 0 <= i_map < |i_e_elems| => var el := i_e_elems[i_map]; peepholeExpr(el, backend))) + case if_(i_e_cond, i_e_then, i_e_else) => + e.(cond := peepholeExpr(i_e_cond, backend), then_ := peepholeExpr(i_e_then, backend), else_ := peepholeExpr(i_e_else, backend)) + case match_(i_e_scrutinee, i_e_arms) => + var scr := peepholeExpr(i_e_scrutinee, backend); + AwArmsElems(i_e_arms, backend); + SzArmsElems(i_e_arms); + var ys_ := seq(|i_e_arms|, i_map requires 0 <= i_map < |i_e_arms| => var a := i_e_arms[i_map]; a.(body := peepholeExpr(a.body, backend))); + assert forall j | 0 <= j < |i_e_arms| :: ys_[j].pattern == i_e_arms[j].pattern && ys_[j].body == peepholeExpr(i_e_arms[j].body, backend); + PointwiseNormArms(ys_, backend); + assert forall j | 0 <= j < |i_e_arms| :: normE(i_e_arms[j].body, backend) ==> ys_[j].body == i_e_arms[j].body; + ArmsMapIdNormal(i_e_arms, ys_, backend); + e.(scrutinee := scr, arms := seq(|i_e_arms|, i_map requires 0 <= i_map < |i_e_arms| => var a := i_e_arms[i_map]; a.(body := peepholeExpr(a.body, backend)))) + case forall_(i_e_var, i_e_type, i_e_body) => + e.(body_forall := peepholeExpr(i_e_body, backend)) + case exists_(i_e_var, i_e_type, i_e_body) => + e.(body_exists := peepholeExpr(i_e_body, backend)) + case let(i_e_name, i_e_value, i_e_body) => + e.(value_let := peepholeExpr(i_e_value, backend), body_let := peepholeExpr(i_e_body, backend)) + case methodCall(i_e_obj, i_e_objTy, i_e_method, i_e_args, i_e_monadic) => + AwSeqEElems(i_e_args, backend); + SzSeqEElems(i_e_args); + var ys_ := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend)); + assert forall j | 0 <= j < |i_e_args| :: ys_[j] == peepholeExpr(i_e_args[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |ys_| :: normE(ys_[j], backend); + assert forall j | 0 <= j < |i_e_args| :: normE(i_e_args[j], backend) ==> ys_[j] == i_e_args[j]; + SeqEMapIdNormal(i_e_args, ys_, backend); + e.(obj := peepholeExpr(i_e_obj, backend), args := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend))) + case lambda(i_e_params, i_e_body) => + e.(body_lambda := peepholeStmts(i_e_body, backend)) + } +} + +function peepholeStmt(s: Stmt, backend: Backend): Stmt + ensures normS(peepholeStmt(s, backend), backend) + ensures normS(s, backend) ==> peepholeStmt(s, backend) == s + decreases awS(s, backend), szS(s), 0, 1 +{ + var cur := rewriteChildrenStmt(s, backend); + var r := ruleMatchOnMapGetStmt(cur); + match r { + case Some(i_r_val) => + assert normS(s, backend) ==> normKidsS(s, backend); + assert !normS(s, backend); + NormKidsAwZeroS(cur, backend); + RuleStmtAw(cur, backend); + assert awS(cur, backend) <= chargeS(s); + assert awS(s, backend) >= chargeS(s); + peepholeStmt(i_r_val, backend) + case None => + assert normS(s, backend) ==> normKidsS(s, backend); + assert normKidsS(cur, backend); + assert ruleMatchOnMapGetStmt(cur).None?; + assert normS(cur, backend); + cur + } +} + +function rewriteChildrenStmt(s: Stmt, backend: Backend): Stmt + ensures normKidsS(rewriteChildrenStmt(s, backend), backend) + ensures chargeS(rewriteChildrenStmt(s, backend)) == chargeS(s) + ensures normKidsS(s, backend) ==> rewriteChildrenStmt(s, backend) == s + decreases awS(s, backend), szS(s), 0, 0 +{ + NormKidsAwZeroGuardS(s, backend); + match s { + case let(i_s_name, i_s_type, i_s_mutable, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case assign(i_s_target, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case bind(i_s_target, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case let_bind(i_s_name, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case return_(i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case break_ => + s + case continue_ => + s + case if_(i_s_cond, i_s_then, i_s_else) => + s.(cond := peepholeExpr(i_s_cond, backend), then_ := peepholeStmts(i_s_then, backend), else_ := peepholeStmts(i_s_else, backend)) + case match_(i_s_scrutinee, i_s_arms) => + var scr := peepholeExpr(i_s_scrutinee, backend); + SzSArmsElems(i_s_arms); + AwSArmsElems(i_s_arms, backend); + var ys_ := seq(|i_s_arms|, i_map requires 0 <= i_map < |i_s_arms| => var a := i_s_arms[i_map]; a.(body := peepholeStmts(a.body, backend))); + assert forall j | 0 <= j < |i_s_arms| :: ys_[j].pattern == i_s_arms[j].pattern && ys_[j].body == peepholeStmts(i_s_arms[j].body, backend); + PointwiseNormSArms(ys_, backend); + assert forall j | 0 <= j < |i_s_arms| :: normSs(i_s_arms[j].body, backend) ==> ys_[j].body == i_s_arms[j].body; + SArmsMapIdNormal(i_s_arms, ys_, backend); + s.(scrutinee := scr, arms := seq(|i_s_arms|, i_map requires 0 <= i_map < |i_s_arms| => var a := i_s_arms[i_map]; a.(body := peepholeStmts(a.body, backend)))) + case while_(i_s_cond, i_s_invariants, i_s_decreasing, i_s_doneWith, i_s_body) => + AwSeqEElems(i_s_invariants, backend); + SzSeqEElems(i_s_invariants); + var ys_ := seq(|i_s_invariants|, i_map requires 0 <= i_map < |i_s_invariants| => var i := i_s_invariants[i_map]; peepholeExpr(i, backend)); + assert forall j | 0 <= j < |i_s_invariants| :: ys_[j] == peepholeExpr(i_s_invariants[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |ys_| :: normE(ys_[j], backend); + assert forall j | 0 <= j < |i_s_invariants| :: normE(i_s_invariants[j], backend) ==> ys_[j] == i_s_invariants[j]; + SeqEMapIdNormal(i_s_invariants, ys_, backend); + s.(cond := peepholeExpr(i_s_cond, backend), invariants := seq(|i_s_invariants|, i_map requires 0 <= i_map < |i_s_invariants| => var i := i_s_invariants[i_map]; peepholeExpr(i, backend)), body := peepholeStmts(i_s_body, backend)) + case forin(i_s_idx, i_s_bound, i_s_invariants, i_s_body) => + AwSeqEElems(i_s_invariants, backend); + SzSeqEElems(i_s_invariants); + var ys_ := seq(|i_s_invariants|, i_map requires 0 <= i_map < |i_s_invariants| => var i := i_s_invariants[i_map]; peepholeExpr(i, backend)); + assert forall j | 0 <= j < |i_s_invariants| :: ys_[j] == peepholeExpr(i_s_invariants[j], backend); + PointwiseNormSeqE(ys_, backend); + assert forall j | 0 <= j < |ys_| :: normE(ys_[j], backend); + assert forall j | 0 <= j < |i_s_invariants| :: normE(i_s_invariants[j], backend) ==> ys_[j] == i_s_invariants[j]; + SeqEMapIdNormal(i_s_invariants, ys_, backend); + s.(bound := peepholeExpr(i_s_bound, backend), invariants := seq(|i_s_invariants|, i_map requires 0 <= i_map < |i_s_invariants| => var i := i_s_invariants[i_map]; peepholeExpr(i, backend)), body := peepholeStmts(i_s_body, backend)) + case ghostLet(i_s_name, i_s_type, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case ghostAssign(i_s_target, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case assert_(i_s_expr, i_s_assumed) => + s.(expr := peepholeExpr(i_s_expr, backend)) + } +} + +function peepholeModule(mod: Module, backend: Backend): Module +{ + mod.(decls := (var s_map := mod.decls; seq(|s_map|, i_map requires 0 <= i_map < |s_map| => var d := s_map[i_map]; peepholeDecl(d, backend)))) +} + +function peepholeMethod(m: FnMethod, backend: Backend): FnMethod +{ + var re := (x: Expr) => peepholeExpr(x, backend); + m.(body := peepholeStmts(m.body, backend), requires_ := (var s_map := m.requires_; seq(|s_map|, i_map requires 0 <= i_map < |s_map| => (re)(s_map[i_map]))), ensures_ := (var s_map := m.ensures_; seq(|s_map|, i_map requires 0 <= i_map < |s_map| => (re)(s_map[i_map]))), decreases_ := (match m.decreases_ { case Some(i_m_decreases_val) => Option.Some(re(i_m_decreases_val)) case None => Option.None })) +} + +function peepholeDecl(d: Decl, backend: Backend): Decl +{ + var re := (x: Expr) => peepholeExpr(x, backend); + match d { + case def(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures, i_d_decreases, i_d_body) => + d.(body_def := re(i_d_body), requires_ := seq(|i_d_requires|, i_map requires 0 <= i_map < |i_d_requires| => (re)(i_d_requires[i_map])), ensures_ := seq(|i_d_ensures|, i_map requires 0 <= i_map < |i_d_ensures| => (re)(i_d_ensures[i_map])), decreases_ := (match i_d_decreases { case Some(i_d_decreases_val) => Option.Some(re(i_d_decreases_val)) case None => Option.None })) + case def_by_method(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures, i_d_decreases, i_d_methodBody) => + d.(methodBody := peepholeStmts(i_d_methodBody, backend), requires_ := seq(|i_d_requires|, i_map requires 0 <= i_map < |i_d_requires| => (re)(i_d_requires[i_map])), ensures_ := seq(|i_d_ensures|, i_map requires 0 <= i_map < |i_d_ensures| => (re)(i_d_ensures[i_map])), decreases_ := (match i_d_decreases { case Some(i_d_decreases_val) => Option.Some(re(i_d_decreases_val)) case None => Option.None })) + case method_(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures, i_d_decreases, i_d_body) => + d.(body_method := peepholeStmts(i_d_body, backend), requires_ := seq(|i_d_requires|, i_map requires 0 <= i_map < |i_d_requires| => (re)(i_d_requires[i_map])), ensures_ := seq(|i_d_ensures|, i_map requires 0 <= i_map < |i_d_ensures| => (re)(i_d_ensures[i_map])), decreases_ := (match i_d_decreases { case Some(i_d_decreases_val) => Option.Some(re(i_d_decreases_val)) case None => Option.None })) + case namespace(i_d_name, i_d_decls) => + d.(decls := seq(|i_d_decls|, i_map requires 0 <= i_map < |i_d_decls| => var x := i_d_decls[i_map]; peepholeDecl(x, backend))) + case class_(i_d_name, i_d_fields, i_d_methods) => + d.(methods := seq(|i_d_methods|, i_map requires 0 <= i_map < |i_d_methods| => var m := i_d_methods[i_map]; peepholeMethod(m, backend))) + case const_(i_d_name, i_d_type, i_d_value) => + d.(value := re(i_d_value)) + case inductive_(i_d_name, i_d_typeParams, i_d_constructors, i_d_deriving) => + d + case structure(i_d_name, i_d_typeParams, i_d_fields, i_d_deriving) => + d + case type_alias(i_d_name, i_d_target) => + d + case opaque_type(i_d_name) => + d + case extern(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures) => + d + } +} + +// ═══ Hand-authored: normal forms (P1 — normalization/termination) ═══════ +// A term is normal for a backend when no rewrite rule fires on it or any +// subterm; a statement list additionally has no adjacent pair redex, mirroring +// rewriteStmtListPairs' scan. + +predicate normE(e: Expr, b: Backend) + decreases e, 1 +{ + applyExprRules(e, b).None? && normKidsE(e, b) +} + +predicate normKidsE(e: Expr, b: Backend) + decreases e, 0 +{ + match e { + case var_(_) => true + case num(_) => true + case bigint(_) => true + case bool_(_) => true + case str(_) => true + case constructor_(_, _, args) => normSeqE(args, b) + case binop(_, l, r) => normE(l, b) && normE(r, b) + case unop(_, x) => normE(x, b) + case app(_, args, _) => normSeqE(args, b) + case field(obj, _, _, _, _) => normE(obj, b) + case toNat(x) => normE(x, b) + case toReal(x) => normE(x, b) + case index(a, i) => normE(a, b) && normE(i, b) + case tupleLiteral(elems) => normSeqE(elems, b) + case tupleProj(obj, _, _) => normE(obj, b) + case record(spread, fields, _, _) => normOptE(spread, b) && normFields(fields, b) + case arrayLiteral(elems) => normSeqE(elems, b) + case emptyMap => true + case emptySet => true + case mapLiteral(entries) => normEntries(entries, b) + case methodCall(obj, _, _, args, _) => normE(obj, b) && normSeqE(args, b) + case lambda(_, body) => normSs(body, b) + case if_(c, t, f) => normE(c, b) && normE(t, b) && normE(f, b) + case match_(scr, arms) => normE(scr, b) && normArms(arms, b) + case forall_(_, _, x) => normE(x, b) + case exists_(_, _, x) => normE(x, b) + case implies(ps, c) => normSeqE(ps, b) && normE(c, b) + case let(_, v, x) => normE(v, b) && normE(x, b) + case havoc(_) => true + case default(_) => true + } +} + +predicate normOptE(o: Option, b: Backend) +{ + match o { case Some(e) => normE(e, b) case None => true } +} + +predicate normSeqE(es: seq, b: Backend) +{ + |es| == 0 || (normE(es[0], b) && normSeqE(es[1..], b)) +} + +predicate normArm(a: MatchArm, b: Backend) { normE(a.body, b) } + +predicate normArms(arms: seq, b: Backend) +{ + |arms| == 0 || (normArm(arms[0], b) && normArms(arms[1..], b)) +} + +predicate normField(f: RecordField, b: Backend) { normE(f.value, b) } + +predicate normFields(fs: seq, b: Backend) +{ + |fs| == 0 || (normField(fs[0], b) && normFields(fs[1..], b)) +} + +predicate normEntry(en: MapEntry, b: Backend) { normE(en.key, b) && normE(en.value, b) } + +predicate normEntries(es: seq, b: Backend) +{ + |es| == 0 || (normEntry(es[0], b) && normEntries(es[1..], b)) +} + +predicate normSArm(a: StmtMatchArm, b: Backend) { normSs(a.body, b) } + +predicate normSArms(arms: seq, b: Backend) +{ + |arms| == 0 || (normSArm(arms[0], b) && normSArms(arms[1..], b)) +} + +predicate normS(s: Stmt, b: Backend) + decreases s, 1 +{ + ruleMatchOnMapGetStmt(s).None? && normKidsS(s, b) +} + +predicate normKidsS(s: Stmt, b: Backend) + decreases s, 0 +{ + match s { + case let(_, _, _, v) => normE(v, b) + case assign(_, v) => normE(v, b) + case bind(_, v) => normE(v, b) + case let_bind(_, v) => normE(v, b) + case return_(v) => normE(v, b) + case break_ => true + case continue_ => true + case if_(c, t, f) => normE(c, b) && normSs(t, b) && normSs(f, b) + case match_(scr, arms) => normE(scr, b) && normSArms(arms, b) + case while_(c, invs, dec, dw, body) => normE(c, b) && normSeqE(invs, b) && normSs(body, b) + case forin(_, bound, invs, body) => normE(bound, b) && normSeqE(invs, b) && normSs(body, b) + case ghostLet(_, _, v) => normE(v, b) + case ghostAssign(_, v) => normE(v, b) + case assert_(x, _) => normE(x, b) + } +} + +predicate normSs(ss: seq, b: Backend) +{ + if |ss| == 0 then true + else if |ss| >= 2 && tryLetMatchOnMapGet(ss[0], ss[1], ss[2..]).Some? then false + else normS(ss[0], b) && normSs(ss[1..], b) +} + +// ═══ Hand-authored: active weight (termination measure) ═════════════════ +// awE/awS/awSs charge only the non-normal part of a term: normal subtrees +// weigh zero, so rule-duplicated receivers (always already normalized by the +// bottom-up engine) cannot grow the measure. Root charges order the rule +// chain: match/let (3) → if (2) → other (1) → normal (0). PSs pads statement +// lists so a mapped list's weight stays below its input's padding. + +function chargeE(e: Expr): nat { + if e.match_? || e.let? then 3 else if e.if_? then 2 else 1 +} + +function chargeS(s: Stmt): nat { + if s.match_? then 3 else 1 +} + +function awE(e: Expr, b: Backend): nat + decreases e, 2 +{ + if normE(e, b) then 0 else chargeE(e) + awKidsE(e, b) +} + +function awKidsE(e: Expr, b: Backend): nat + decreases e, 1 +{ + match e { + case var_(_) => 0 + case num(_) => 0 + case bigint(_) => 0 + case bool_(_) => 0 + case str(_) => 0 + case constructor_(_, _, args) => awSeqE(args, b) + case binop(_, l, r) => awE(l, b) + awE(r, b) + case unop(_, x) => awE(x, b) + case app(_, args, _) => awSeqE(args, b) + case field(obj, _, _, _, _) => awE(obj, b) + case toNat(x) => awE(x, b) + case toReal(x) => awE(x, b) + case index(a, i) => awE(a, b) + awE(i, b) + case tupleLiteral(elems) => awSeqE(elems, b) + case tupleProj(obj, _, _) => awE(obj, b) + case record(spread, fields, _, _) => awOptE(spread, b) + awFields(fields, b) + case arrayLiteral(elems) => awSeqE(elems, b) + case emptyMap => 0 + case emptySet => 0 + case mapLiteral(entries) => awEntries(entries, b) + case methodCall(obj, _, _, args, _) => awE(obj, b) + awSeqE(args, b) + case lambda(_, body) => PSs(body, b) + case if_(c, t, f) => awE(c, b) + awE(t, b) + awE(f, b) + case match_(scr, arms) => awE(scr, b) + awArms(arms, b) + case forall_(_, _, x) => awE(x, b) + case exists_(_, _, x) => awE(x, b) + case implies(ps, c) => awSeqE(ps, b) + awE(c, b) + case let(_, v, x) => awE(v, b) + awE(x, b) + case havoc(_) => 0 + case default(_) => 0 + } +} + +function awOptE(o: Option, b: Backend): nat { + match o { case Some(e) => awE(e, b) case None => 0 } +} + +function awSeqE(es: seq, b: Backend): nat { + if |es| == 0 then 0 else awE(es[0], b) + awSeqE(es[1..], b) +} + +function awArm(a: MatchArm, b: Backend): nat { awE(a.body, b) } + +function awArms(arms: seq, b: Backend): nat { + if |arms| == 0 then 0 else awArm(arms[0], b) + awArms(arms[1..], b) +} + +function awField(f: RecordField, b: Backend): nat { awE(f.value, b) } + +function awFields(fs: seq, b: Backend): nat { + if |fs| == 0 then 0 else awField(fs[0], b) + awFields(fs[1..], b) +} + +function awEntry(en: MapEntry, b: Backend): nat { awE(en.key, b) + awE(en.value, b) } + +function awEntries(es: seq, b: Backend): nat { + if |es| == 0 then 0 else awEntry(es[0], b) + awEntries(es[1..], b) +} + +function awSArm(a: StmtMatchArm, b: Backend): nat { PSs(a.body, b) } + +function awSArms(arms: seq, b: Backend): nat { + if |arms| == 0 then 0 else awSArm(arms[0], b) + awSArms(arms[1..], b) +} + +function awS(s: Stmt, b: Backend): nat + decreases s, 2 +{ + if normS(s, b) then 0 else chargeS(s) + awKidsS(s, b) +} + +function awKidsS(s: Stmt, b: Backend): nat + decreases s, 1 +{ + match s { + case let(_, _, _, v) => awE(v, b) + case assign(_, v) => awE(v, b) + case bind(_, v) => awE(v, b) + case let_bind(_, v) => awE(v, b) + case return_(v) => awE(v, b) + case break_ => 0 + case continue_ => 0 + case if_(c, t, f) => awE(c, b) + PSs(t, b) + PSs(f, b) + case match_(scr, arms) => awE(scr, b) + awSArms(arms, b) + case while_(c, invs, dec, dw, body) => awE(c, b) + awSeqE(invs, b) + PSs(body, b) + case forin(_, bound, invs, body) => awE(bound, b) + awSeqE(invs, b) + PSs(body, b) + case ghostLet(_, _, v) => awE(v, b) + case ghostAssign(_, v) => awE(v, b) + case assert_(x, _) => awE(x, b) + } +} + +// Padded list weight: what peepholeStmts' measure starts from. Zero exactly on +// fully-normal lists; otherwise dominates any elementwise-normalized list. +function PSs(ss: seq, b: Backend): nat + decreases ss, 1 +{ + if normSs(ss, b) then 0 else awSs(ss, b) + 2 * |ss| + 1 +} + +function awSs(ss: seq, b: Backend): nat + decreases ss, 0 +{ + if |ss| == 0 then 0 + else if |ss| >= 2 && tryLetMatchOnMapGet(ss[0], ss[1], ss[2..]).Some? then + 4 + awS(ss[0], b) + awS(ss[1], b) + awSs(ss[2..], b) + else awS(ss[0], b) + awSs(ss[1..], b) +} + +// ═══ Hand-authored: supporting lemmas ═══════════════════════════════════ + +lemma NormAwZeroE(e: Expr, b: Backend) + requires normE(e, b) + ensures awE(e, b) == 0 +{ +} + +lemma NormAwZeroS(s: Stmt, b: Backend) + requires normS(s, b) + ensures awS(s, b) == 0 +{ +} + +lemma NormSsPSsZero(ss: seq, b: Backend) + requires normSs(ss, b) + ensures PSs(ss, b) == 0 +{ +} + +lemma NormSeqEElem(es: seq, b: Backend) + requires normSeqE(es, b) + ensures forall j | 0 <= j < |es| :: normE(es[j], b) +{ + if |es| > 0 { + NormSeqEElem(es[1..], b); + assert forall j | 1 <= j < |es| :: es[j] == es[1..][j-1]; + } +} + +lemma NormArmsElem(arms: seq, b: Backend) + requires normArms(arms, b) + ensures forall j | 0 <= j < |arms| :: normE(arms[j].body, b) +{ + if |arms| > 0 { + NormArmsElem(arms[1..], b); + assert forall j | 1 <= j < |arms| :: arms[j] == arms[1..][j-1]; + } +} + +lemma NormSArmsElem(arms: seq, b: Backend) + requires normSArms(arms, b) + ensures forall j | 0 <= j < |arms| :: normSs(arms[j].body, b) +{ + if |arms| > 0 { + NormSArmsElem(arms[1..], b); + assert forall j | 1 <= j < |arms| :: arms[j] == arms[1..][j-1]; + } +} + +lemma NormSeqEAwZero(es: seq, b: Backend) + requires normSeqE(es, b) + ensures awSeqE(es, b) == 0 +{ + if |es| > 0 { + NormAwZeroE(es[0], b); + NormSeqEAwZero(es[1..], b); + } +} + +lemma NormArmsAwZero(arms: seq, b: Backend) + requires normArms(arms, b) + ensures awArms(arms, b) == 0 +{ + if |arms| > 0 { + NormAwZeroE(arms[0].body, b); + NormArmsAwZero(arms[1..], b); + } +} + +lemma NormSArmsAwZero(arms: seq, b: Backend) + requires normSArms(arms, b) + ensures awSArms(arms, b) == 0 +{ + if |arms| > 0 { + NormSsPSsZero(arms[0].body, b); + NormSArmsAwZero(arms[1..], b); + } +} + +lemma NormFieldsAwZero(fs: seq, b: Backend) + requires normFields(fs, b) + ensures awFields(fs, b) == 0 +{ + if |fs| > 0 { + NormAwZeroE(fs[0].value, b); + NormFieldsAwZero(fs[1..], b); + } +} + +lemma NormEntriesAwZero(es: seq, b: Backend) + requires normEntries(es, b) + ensures awEntries(es, b) == 0 +{ + if |es| > 0 { + NormAwZeroE(es[0].key, b); + NormAwZeroE(es[0].value, b); + NormEntriesAwZero(es[1..], b); + } +} + +lemma NormKidsAwZeroE(e: Expr, b: Backend) + requires normKidsE(e, b) + ensures awKidsE(e, b) == 0 +{ + match e { + case constructor_(_, _, args) => NormSeqEAwZero(args, b); + case binop(_, l, r) => { NormAwZeroE(l, b); NormAwZeroE(r, b); } + case unop(_, x) => NormAwZeroE(x, b); + case app(_, args, _) => NormSeqEAwZero(args, b); + case field(obj, _, _, _, _) => NormAwZeroE(obj, b); + case toNat(x) => NormAwZeroE(x, b); + case toReal(x) => NormAwZeroE(x, b); + case index(a, i) => { NormAwZeroE(a, b); NormAwZeroE(i, b); } + case tupleLiteral(elems) => NormSeqEAwZero(elems, b); + case tupleProj(obj, _, _) => NormAwZeroE(obj, b); + case record(spread, fields, _, _) => { + match spread { case Some(sv) => NormAwZeroE(sv, b); case None => } + NormFieldsAwZero(fields, b); + } + case arrayLiteral(elems) => NormSeqEAwZero(elems, b); + case mapLiteral(entries) => NormEntriesAwZero(entries, b); + case methodCall(obj, _, _, args, _) => { NormAwZeroE(obj, b); NormSeqEAwZero(args, b); } + case lambda(_, body) => NormSsPSsZero(body, b); + case if_(c, t, f) => { NormAwZeroE(c, b); NormAwZeroE(t, b); NormAwZeroE(f, b); } + case match_(scr, arms) => { NormAwZeroE(scr, b); NormArmsAwZero(arms, b); } + case forall_(_, _, x) => NormAwZeroE(x, b); + case exists_(_, _, x) => NormAwZeroE(x, b); + case implies(ps, c) => { NormSeqEAwZero(ps, b); NormAwZeroE(c, b); } + case let(_, v, x) => { NormAwZeroE(v, b); NormAwZeroE(x, b); } + case _ => + } +} + +lemma NormKidsAwZeroS(s: Stmt, b: Backend) + requires normKidsS(s, b) + ensures awKidsS(s, b) == 0 +{ + match s { + case let(_, _, _, v) => NormAwZeroE(v, b); + case assign(_, v) => NormAwZeroE(v, b); + case bind(_, v) => NormAwZeroE(v, b); + case let_bind(_, v) => NormAwZeroE(v, b); + case return_(v) => NormAwZeroE(v, b); + case if_(c, t, f) => { NormAwZeroE(c, b); NormSsPSsZero(t, b); NormSsPSsZero(f, b); } + case match_(scr, arms) => { NormAwZeroE(scr, b); NormSArmsAwZero(arms, b); } + case while_(c, invs, dec, dw, body) => { + NormAwZeroE(c, b); + NormSeqEAwZero(invs, b); + NormSsPSsZero(body, b); + } + case forin(_, bound, invs, body) => { NormAwZeroE(bound, b); NormSeqEAwZero(invs, b); NormSsPSsZero(body, b); } + case ghostLet(_, _, v) => NormAwZeroE(v, b); + case ghostAssign(_, v) => NormAwZeroE(v, b); + case assert_(x, _) => NormAwZeroE(x, b); + case _ => + } +} + +lemma NormSsCons(x: Stmt, rest: seq, b: Backend) + requires normS(x, b) && normSs(rest, b) + requires |rest| == 0 || tryLetMatchOnMapGet(x, rest[0], rest[1..]).None? + ensures normSs([x] + rest, b) +{ + assert ([x] + rest)[0] == x; + assert ([x] + rest)[1..] == rest; + if |rest| > 0 { + assert ([x] + rest)[1] == rest[0]; + assert ([x] + rest)[2..] == rest[1..]; + } +} + +// ═══ Hand-authored: rule lemmas — each fired rule strictly shrinks aw ═══ + +lemma IsMapGetShape(e: Expr) + requires isMapGet(e).Some? + ensures e.methodCall? && |e.args| == 1 + ensures isMapGet(e).value.obj == e.obj && isMapGet(e).value.key == e.args[0] && isMapGet(e).value.objTy == e.objTy +{ +} + +lemma SomeNoneArmsShape(arms: seq) + requires getSomeNoneArms(arms).Some? + ensures |arms| == 2 + ensures (getSomeNoneArms(arms).value.someArm == arms[0] && getSomeNoneArms(arms).value.noneArm == arms[1]) + || (getSomeNoneArms(arms).value.someArm == arms[1] && getSomeNoneArms(arms).value.noneArm == arms[0]) +{ +} + +lemma SomeNoneStmtArmsShape(arms: seq) + requires getSomeNoneStmtArms(arms).Some? + ensures |arms| == 2 + ensures (getSomeNoneStmtArms(arms).value.someArm == arms[0] && getSomeNoneStmtArms(arms).value.noneArm == arms[1]) + || (getSomeNoneStmtArms(arms).value.someArm == arms[1] && getSomeNoneStmtArms(arms).value.noneArm == arms[0]) +{ +} + +// Shared tail of rules 1 and 6: the rebuilt `if k in m …` is normal-kided and +// weighs at most 2, given normal pieces. +lemma MapGetRebuildAw(g: IsMapGetResult, binder: Option, sb: Expr, nb: Expr, b: Backend) + requires normE(g.obj, b) && normE(g.key, b) && normE(sb, b) && normE(nb, b) + ensures var idx := index(g.obj, g.key); + var someBody := match binder { case Some(bn) => (if |bn| > 0 then Expr.let(bn, idx, sb) else sb) case None => sb }; + var has := methodCall(g.obj, g.objTy, "has", [g.key], false); + normKidsE(Expr.if_(has, someBody, nb), b) && awE(Expr.if_(has, someBody, nb), b) <= 2 +{ + var idx := index(g.obj, g.key); + var someBody := match binder { case Some(bn) => (if |bn| > 0 then Expr.let(bn, idx, sb) else sb) case None => sb }; + var has := methodCall(g.obj, g.objTy, "has", [g.key], false); + assert [g.key][1..] == []; + assert normSeqE([g.key], b); + assert applyExprRules(has, b).None?; + assert normE(has, b); + assert normE(idx, b); + assert applyExprRules(someBody, b).None?; + assert normE(someBody, b); + var r := Expr.if_(has, someBody, nb); + assert normKidsE(r, b); + NormAwZeroE(has, b); + NormAwZeroE(someBody, b); + NormAwZeroE(nb, b); + assert awKidsE(r, b) == 0; +} + +lemma Rule1Aw(e: Expr, b: Backend) + requires ruleMatchOnMapGetExpr(e).Some? + requires normKidsE(e, b) + ensures normKidsE(ruleMatchOnMapGetExpr(e).value, b) + ensures awE(ruleMatchOnMapGetExpr(e).value, b) < awE(e, b) +{ + var scr := e.scrutinee; + IsMapGetShape(scr); + var g := isMapGet(scr).value; + SomeNoneArmsShape(e.arms); + var sa := getSomeNoneArms(e.arms).value; + NormArmsElem(e.arms, b); + assert e == Expr.match_(e.scrutinee, e.arms); + assert normE(scr, b); + assert scr == methodCall(scr.obj, scr.objTy, scr.method_, scr.args, scr.monadic); + assert normKidsE(scr, b); + assert normE(g.obj, b); + NormSeqEElem(scr.args, b); + assert normE(g.key, b); + if sa.someArm == e.arms[0] { + assert normE(sa.someArm.body, b); + assert normE(sa.noneArm.body, b); + } else { + assert sa.someArm == e.arms[1]; + assert normE(sa.someArm.body, b); + assert normE(sa.noneArm.body, b); + } + MapGetRebuildAw(g, sa.binder, sa.someArm.body, sa.noneArm.body, b); + assert applyExprRules(e, b).Some?; + assert !normE(e, b); + NormKidsAwZeroE(e, b); + assert awE(e, b) == 3; +} + +lemma Rule6Aw(e: Expr, b: Backend) + requires ruleLetMatchOnMapGetExpr(e).Some? + requires normKidsE(e, b) + ensures normKidsE(ruleLetMatchOnMapGetExpr(e).value, b) + ensures awE(ruleLetMatchOnMapGetExpr(e).value, b) < awE(e, b) +{ + var m := e.body_let; + IsMapGetShape(e.value_let); + var g := isMapGet(e.value_let).value; + SomeNoneArmsShape(m.arms); + var sa := getSomeNoneArms(m.arms).value; + assert e == Expr.let(e.name, e.value_let, e.body_let); + assert normE(e.value_let, b) && normE(m, b); + assert e.value_let == methodCall(e.value_let.obj, e.value_let.objTy, e.value_let.method_, e.value_let.args, e.value_let.monadic); + assert normKidsE(e.value_let, b); + assert m == Expr.match_(m.scrutinee, m.arms); + assert normKidsE(m, b); + NormArmsElem(m.arms, b); + assert normE(g.obj, b); + NormSeqEElem(e.value_let.args, b); + assert normE(g.key, b); + if sa.someArm == m.arms[0] { + assert normE(sa.someArm.body, b); + assert normE(sa.noneArm.body, b); + } else { + assert sa.someArm == m.arms[1]; + assert normE(sa.someArm.body, b); + assert normE(sa.noneArm.body, b); + } + MapGetRebuildAw(g, sa.binder, sa.someArm.body, sa.noneArm.body, b); + assert applyExprRules(e, b).Some?; + assert !normE(e, b); + NormKidsAwZeroE(e, b); + assert awE(e, b) == 3; +} + +lemma {:vcs_split_on_every_assert} Rule4Aw(e: Expr, b: Backend) + requires b == dafny + requires ruleIfFalseElseTrue(e).Some? + requires normKidsE(e, b) + ensures normKidsE(ruleIfFalseElseTrue(e).value, b) + ensures awE(ruleIfFalseElseTrue(e).value, b) < awE(e, b) +{ + assert e == Expr.if_(e.cond, e.then_, e.else_); + var r := ruleIfFalseElseTrue(e).value; + assert r == unop("¬", e.cond); + assert normE(e.cond, b); + assert applyExprRules(r, b).None?; + assert normE(r, b); + assert applyExprRules(e, b).Some?; + assert chargeE(e) == 2; + NormKidsAwZeroE(e, b); + assert awE(e, b) == 2; +} + +lemma {:vcs_split_on_every_assert} Rule5Aw(e: Expr, b: Backend) + requires b == dafny + requires ruleIfFalseElseTrue(e).None? + requires ruleIfIdentity(e).Some? + requires normKidsE(e, b) + ensures normKidsE(ruleIfIdentity(e).value, b) + ensures awE(ruleIfIdentity(e).value, b) < awE(e, b) +{ + assert e == Expr.if_(e.cond, e.then_, e.else_); + var r := ruleIfIdentity(e).value; + assert r == e.cond; + assert normE(r, b); + NormAwZeroE(r, b); + assert applyExprRules(e, b).Some?; + // Before NormKidsAwZeroE: its awKidsE decomposition in context makes the + // head-charge ctor test cost 30x (9.5s batch, times out on CI's runner). + assert chargeE(e) == 2; + NormKidsAwZeroE(e, b); + assert awE(e, b) == 2; +} + +lemma {:vcs_split_on_every_assert} Rule2Aw(e: Expr, b: Backend) + requires b == dafny + requires ruleMatchOnMapGetExpr(e).None? && ruleLetMatchOnMapGetExpr(e).None? + requires ruleIfFalseElseTrue(e).None? && ruleIfIdentity(e).None? + requires ruleIfThenFalse(e).Some? + requires normKidsE(e, b) + ensures normKidsE(ruleIfThenFalse(e).value, b) + ensures awE(ruleIfThenFalse(e).value, b) < awE(e, b) +{ + assert e == Expr.if_(e.cond, e.then_, e.else_); + var r := ruleIfThenFalse(e).value; + assert r == binop("∧", e.cond, e.then_); + assert normE(e.cond, b) && normE(e.then_, b); + assert applyExprRules(r, b).None?; + assert normE(r, b); + assert applyExprRules(e, b).Some?; + assert chargeE(e) == 2; + NormKidsAwZeroE(e, b); + assert awE(e, b) == 2; +} + +lemma {:vcs_split_on_every_assert} Rule3Aw(e: Expr, b: Backend) + requires b == dafny + requires ruleMatchOnMapGetExpr(e).None? && ruleLetMatchOnMapGetExpr(e).None? + requires ruleIfFalseElseTrue(e).None? && ruleIfIdentity(e).None? && ruleIfThenFalse(e).None? + requires ruleIfTrueElse(e).Some? + requires normKidsE(e, b) + ensures normKidsE(ruleIfTrueElse(e).value, b) + ensures awE(ruleIfTrueElse(e).value, b) < awE(e, b) +{ + assert e == Expr.if_(e.cond, e.then_, e.else_); + var r := ruleIfTrueElse(e).value; + assert r == binop("∨", e.cond, e.else_); + assert normE(e.cond, b) && normE(e.else_, b); + assert applyExprRules(r, b).None?; + assert normE(r, b); + assert applyExprRules(e, b).Some?; + assert chargeE(e) == 2; + NormKidsAwZeroE(e, b); + assert awE(e, b) == 2; +} + +lemma ApplyRulesAw(e: Expr, b: Backend) + requires normKidsE(e, b) + requires applyExprRules(e, b).Some? + ensures normKidsE(applyExprRules(e, b).value, b) + ensures awE(applyExprRules(e, b).value, b) < awE(e, b) +{ + if ruleMatchOnMapGetExpr(e).Some? { + Rule1Aw(e, b); + assert applyExprRules(e, b) == ruleMatchOnMapGetExpr(e); + } else if ruleLetMatchOnMapGetExpr(e).Some? { + Rule6Aw(e, b); + assert applyExprRules(e, b) == ruleLetMatchOnMapGetExpr(e); + } else { + assert b == dafny; + if ruleIfFalseElseTrue(e).Some? { + Rule4Aw(e, b); + assert applyExprRules(e, b) == ruleIfFalseElseTrue(e); + } else if ruleIfIdentity(e).Some? { + Rule5Aw(e, b); + assert applyExprRules(e, b) == ruleIfIdentity(e); + } else if ruleIfThenFalse(e).Some? { + Rule2Aw(e, b); + assert applyExprRules(e, b) == ruleIfThenFalse(e); + } else { + Rule3Aw(e, b); + assert applyExprRules(e, b) == ruleIfTrueElse(e); + } + } +} + +// ═══ Hand-authored: structural size (second decreases component) ════════ + +function szOptE(o: Option): nat { + match o { case Some(e) => szE(e) case None => 0 } +} + +function szSeqE(es: seq): nat { + if |es| == 0 then 0 else szE(es[0]) + szSeqE(es[1..]) +} + +function szArm(a: MatchArm): nat { 1 + szE(a.body) } + +function szArms(arms: seq): nat { + if |arms| == 0 then 0 else szArm(arms[0]) + szArms(arms[1..]) +} + +function szSArm(a: StmtMatchArm): nat { 1 + szSs(a.body) } + +function szSArms(arms: seq): nat { + if |arms| == 0 then 0 else szSArm(arms[0]) + szSArms(arms[1..]) +} + +function szField(f: RecordField): nat { 1 + szE(f.value) } + +function szFields(fs: seq): nat { + if |fs| == 0 then 0 else szField(fs[0]) + szFields(fs[1..]) +} + +function szEntry(en: MapEntry): nat { 1 + szE(en.key) + szE(en.value) } + +function szEntries(es: seq): nat { + if |es| == 0 then 0 else szEntry(es[0]) + szEntries(es[1..]) +} + +function szE(e: Expr): nat { + match e { + case var_(_) => 1 + case num(_) => 1 + case bigint(_) => 1 + case bool_(_) => 1 + case str(_) => 1 + case constructor_(_, _, args) => 1 + szSeqE(args) + case binop(_, l, r) => 1 + szE(l) + szE(r) + case unop(_, x) => 1 + szE(x) + case app(_, args, _) => 1 + szSeqE(args) + case field(obj, _, _, _, _) => 1 + szE(obj) + case toNat(x) => 1 + szE(x) + case toReal(x) => 1 + szE(x) + case index(a, i) => 1 + szE(a) + szE(i) + case tupleLiteral(elems) => 1 + szSeqE(elems) + case tupleProj(obj, _, _) => 1 + szE(obj) + case record(spread, fields, _, _) => 1 + szOptE(spread) + szFields(fields) + case arrayLiteral(elems) => 1 + szSeqE(elems) + case emptyMap => 1 + case emptySet => 1 + case mapLiteral(entries) => 1 + szEntries(entries) + case methodCall(obj, _, _, args, _) => 1 + szE(obj) + szSeqE(args) + case lambda(_, body) => 1 + szSs(body) + case if_(c, t, f) => 1 + szE(c) + szE(t) + szE(f) + case match_(scr, arms) => 1 + szE(scr) + szArms(arms) + case forall_(_, _, x) => 1 + szE(x) + case exists_(_, _, x) => 1 + szE(x) + case implies(ps, c) => 1 + szSeqE(ps) + szE(c) + case let(_, v, x) => 1 + szE(v) + szE(x) + case havoc(_) => 1 + case default(_) => 1 + } +} + +function szS(s: Stmt): nat { + match s { + case let(_, _, _, v) => 1 + szE(v) + case assign(_, v) => 1 + szE(v) + case bind(_, v) => 1 + szE(v) + case let_bind(_, v) => 1 + szE(v) + case return_(v) => 1 + szE(v) + case break_ => 1 + case continue_ => 1 + case if_(c, t, f) => 1 + szE(c) + szSs(t) + szSs(f) + case match_(scr, arms) => 1 + szE(scr) + szSArms(arms) + case while_(c, invs, dec, dw, body) => 1 + szE(c) + szSeqE(invs) + szOptE(dec) + szOptE(dw) + szSs(body) + case forin(_, bound, invs, body) => 1 + szE(bound) + szSeqE(invs) + szSs(body) + case ghostLet(_, _, v) => 1 + szE(v) + case ghostAssign(_, v) => 1 + szE(v) + case assert_(x, _) => 1 + szE(x) + } +} + +function szSs(ss: seq): nat { + if |ss| == 0 then 0 else szS(ss[0]) + szSs(ss[1..]) +} + +// ═══ Hand-authored: list-normality helpers for the fixed-point scan ═════ + +// Elementwise statement normality (no pair-redex claim). +predicate elemsNormS(ss: seq, b: Backend) +{ + |ss| == 0 || (normS(ss[0], b) && elemsNormS(ss[1..], b)) +} + +// The pair-redex-freedom half of normSs. +predicate noPairRedex(ss: seq) +{ + |ss| < 2 || (tryLetMatchOnMapGet(ss[0], ss[1], ss[2..]).None? && noPairRedex(ss[1..])) +} + +lemma NormSsSplit(ss: seq, b: Backend) + requires elemsNormS(ss, b) && noPairRedex(ss) + ensures normSs(ss, b) +{ + if |ss| > 0 { + NormSsSplit(ss[1..], b); + } +} + +lemma NormSsElems(ss: seq, b: Backend) + requires normSs(ss, b) + ensures elemsNormS(ss, b) && noPairRedex(ss) +{ + if |ss| > 0 { + NormSsElems(ss[1..], b); + } +} + +// Cap: a list of normal statements weighs at most 2·|ss| (each redex charges +// 4 but consumes two positions; normal elements weigh 0). +lemma ElemsNormAwCap(ss: seq, b: Backend) + requires elemsNormS(ss, b) + ensures awSs(ss, b) <= 2 * |ss| +{ + if |ss| == 0 { + } else if |ss| >= 2 && tryLetMatchOnMapGet(ss[0], ss[1], ss[2..]).Some? { + NormAwZeroS(ss[0], b); + NormAwZeroS(ss[1], b); + assert elemsNormS(ss[1..], b); + assert elemsNormS(ss[1..][1..], b); + assert ss[1..][1..] == ss[2..]; + ElemsNormAwCap(ss[2..], b); + } else { + NormAwZeroS(ss[0], b); + ElemsNormAwCap(ss[1..], b); + } +} + +// awSs == 0 on an elementwise-normal list means it is fully normal. +lemma AwZeroNormSs(ss: seq, b: Backend) + requires elemsNormS(ss, b) + requires awSs(ss, b) == 0 + ensures normSs(ss, b) +{ + if |ss| > 0 { + assert !(|ss| >= 2 && tryLetMatchOnMapGet(ss[0], ss[1], ss[2..]).Some?); + AwZeroNormSs(ss[1..], b); + } +} + +// ═══ Hand-authored: statement-rule lemmas and remaining bounds ══════════ + +lemma IsMapGetShapeS(e: Expr) + requires isMapGet(e).Some? + ensures normE(e, dafny) == normE(e, dafny) +{ +} + +lemma StmtRebuildNorm(g: IsMapGetResult, binder: Option, valTy: Ty, sb: seq, nb: seq, b: Backend) + requires normE(g.obj, b) && normE(g.key, b) && normSs(sb, b) && normSs(nb, b) + ensures var idx := index(g.obj, g.key); + var someBody := match binder { case Some(bn) => (if |bn| > 0 then [Stmt.let(bn, valTy, false, idx)] + sb else sb) case None => sb }; + var has := methodCall(g.obj, g.objTy, "has", [g.key], false); + normS(Stmt.if_(has, someBody, nb), b) && awS(Stmt.if_(has, someBody, nb), b) == 0 +{ + var idx := index(g.obj, g.key); + var someBody := match binder { case Some(bn) => (if |bn| > 0 then [Stmt.let(bn, valTy, false, idx)] + sb else sb) case None => sb }; + var has := methodCall(g.obj, g.objTy, "has", [g.key], false); + assert [g.key][1..] == []; + assert normSeqE([g.key], b); + assert applyExprRules(has, b).None?; + assert normE(has, b); + assert normE(idx, b); + if binder.Some? && |binder.value| > 0 { + var ls := Stmt.let(binder.value, valTy, false, idx); + assert ruleMatchOnMapGetStmt(ls).None?; + assert normS(ls, b); + if |sb| > 0 { + assert tryLetMatchOnMapGet(ls, sb[0], sb[1..]).None?; + } + NormSsElems(sb, b); + NormSsCons(ls, sb, b); + assert normSs(someBody, b); + } else { + assert someBody == sb; + } + var r := Stmt.if_(has, someBody, nb); + assert ruleMatchOnMapGetStmt(r).None?; + assert normS(r, b); + NormAwZeroS(r, b); +} + +lemma RuleStmtAw(s: Stmt, b: Backend) + requires ruleMatchOnMapGetStmt(s).Some? + requires normKidsS(s, b) + ensures normS(ruleMatchOnMapGetStmt(s).value, b) + ensures awS(ruleMatchOnMapGetStmt(s).value, b) < awS(s, b) +{ + var scr := s.scrutinee; + IsMapGetShape(scr); + var g := isMapGet(scr).value; + SomeNoneStmtArmsShape(s.arms); + var sa := getSomeNoneStmtArms(s.arms).value; + NormSArmsElem(s.arms, b); + assert s == Stmt.match_(s.scrutinee, s.arms); + assert normE(scr, b); + assert scr == methodCall(scr.obj, scr.objTy, scr.method_, scr.args, scr.monadic); + assert normKidsE(scr, b); + assert normE(g.obj, b); + NormSeqEElem(scr.args, b); + assert normE(g.key, b); + if sa.someArm == s.arms[0] { + assert normSs(sa.someArm.body, b); + assert normSs(sa.noneArm.body, b); + } else { + assert sa.someArm == s.arms[1]; + assert normSs(sa.someArm.body, b); + assert normSs(sa.noneArm.body, b); + } + var valTy := if g.objTy.map_? then g.objTy.value else Ty.unknown; + StmtRebuildNorm(g, sa.binder, valTy, sa.someArm.body, sa.noneArm.body, b); + assert !normS(s, b); + NormKidsAwZeroS(s, b); + assert awS(s, b) == 3; +} + +lemma TryLetAw(s1: Stmt, s2: Stmt, rest: seq, b: Backend) + requires tryLetMatchOnMapGet(s1, s2, rest).Some? + requires normS(s1, b) && normS(s2, b) + ensures normS(tryLetMatchOnMapGet(s1, s2, rest).value, b) + ensures awS(tryLetMatchOnMapGet(s1, s2, rest).value, b) == 0 +{ + IsMapGetShape(s1.value); + var g := isMapGet(s1.value).value; + SomeNoneStmtArmsShape(s2.arms); + var sa := getSomeNoneStmtArms(s2.arms).value; + NormSArmsElem(s2.arms, b); + assert s1 == Stmt.let(s1.name, s1.type_, s1.mutable, s1.value); + assert normE(s1.value, b); + assert s1.value == methodCall(s1.value.obj, s1.value.objTy, s1.value.method_, s1.value.args, s1.value.monadic); + assert normKidsE(s1.value, b); + assert normE(g.obj, b); + NormSeqEElem(s1.value.args, b); + assert normE(g.key, b); + assert s2 == Stmt.match_(s2.scrutinee, s2.arms); + assert normKidsS(s2, b); + if sa.someArm == s2.arms[0] { + assert normSs(sa.someArm.body, b); + assert normSs(sa.noneArm.body, b); + } else { + assert sa.someArm == s2.arms[1]; + assert normSs(sa.someArm.body, b); + assert normSs(sa.noneArm.body, b); + } + var valTy := if g.objTy.map_? then g.objTy.value else Ty.unknown; + StmtRebuildNorm(g, sa.binder, valTy, sa.someArm.body, sa.noneArm.body, b); +} + +// Pointwise normality builds sequence normality. +lemma PointwiseNormSeqE(ys: seq, b: Backend) + requires forall j | 0 <= j < |ys| :: normE(ys[j], b) + ensures normSeqE(ys, b) +{ + if |ys| > 0 { PointwiseNormSeqE(ys[1..], b); } +} + +lemma PointwiseNormArms(ys: seq, b: Backend) + requires forall j | 0 <= j < |ys| :: normE(ys[j].body, b) + ensures normArms(ys, b) +{ + if |ys| > 0 { PointwiseNormArms(ys[1..], b); } +} + +lemma PointwiseNormFields(ys: seq, b: Backend) + requires forall j | 0 <= j < |ys| :: normE(ys[j].value, b) + ensures normFields(ys, b) +{ + if |ys| > 0 { PointwiseNormFields(ys[1..], b); } +} + +lemma PointwiseNormSArms(ys: seq, b: Backend) + requires forall j | 0 <= j < |ys| :: normSs(ys[j].body, b) + ensures normSArms(ys, b) +{ + if |ys| > 0 { PointwiseNormSArms(ys[1..], b); } +} + +lemma PointwiseElemsNormS(ys: seq, b: Backend) + requires forall j | 0 <= j < |ys| :: normS(ys[j], b) + ensures elemsNormS(ys, b) +{ + if |ys| > 0 { PointwiseElemsNormS(ys[1..], b); } +} + +// Element bounds for the measure components. +lemma AwSeqEElems(es: seq, b: Backend) + ensures forall j | 0 <= j < |es| :: awE(es[j], b) <= awSeqE(es, b) +{ + if |es| > 0 { + AwSeqEElems(es[1..], b); + assert forall j | 1 <= j < |es| :: es[j] == es[1..][j-1]; + } +} + +lemma AwArmsElems(arms: seq, b: Backend) + ensures forall j | 0 <= j < |arms| :: awE(arms[j].body, b) <= awArms(arms, b) +{ + if |arms| > 0 { + AwArmsElems(arms[1..], b); + assert forall j | 1 <= j < |arms| :: arms[j] == arms[1..][j-1]; + } +} + +lemma AwFieldsElems(fs: seq, b: Backend) + ensures forall j | 0 <= j < |fs| :: awE(fs[j].value, b) <= awFields(fs, b) +{ + if |fs| > 0 { + AwFieldsElems(fs[1..], b); + assert forall j | 1 <= j < |fs| :: fs[j] == fs[1..][j-1]; + } +} + +lemma AwSsElems(ss: seq, b: Backend) + ensures forall j | 0 <= j < |ss| :: awS(ss[j], b) <= awSs(ss, b) +{ + if |ss| >= 2 && tryLetMatchOnMapGet(ss[0], ss[1], ss[2..]).Some? { + AwSsElems(ss[2..], b); + assert forall j | 2 <= j < |ss| :: ss[j] == ss[2..][j-2]; + } else if |ss| > 0 { + AwSsElems(ss[1..], b); + assert forall j | 1 <= j < |ss| :: ss[j] == ss[1..][j-1]; + } +} + +lemma SzSeqEElems(es: seq) + ensures forall j | 0 <= j < |es| :: szE(es[j]) <= szSeqE(es) +{ + if |es| > 0 { + SzSeqEElems(es[1..]); + assert forall j | 1 <= j < |es| :: es[j] == es[1..][j-1]; + } +} + +lemma SzArmsElems(arms: seq) + ensures forall j | 0 <= j < |arms| :: szE(arms[j].body) < szArms(arms) +{ + if |arms| > 0 { + SzArmsElems(arms[1..]); + assert forall j | 1 <= j < |arms| :: arms[j] == arms[1..][j-1]; + } +} + +lemma SzFieldsElems(fs: seq) + ensures forall j | 0 <= j < |fs| :: szE(fs[j].value) < szFields(fs) +{ + if |fs| > 0 { + SzFieldsElems(fs[1..]); + assert forall j | 1 <= j < |fs| :: fs[j] == fs[1..][j-1]; + } +} + +lemma SzSArmsElems(arms: seq) + ensures forall j | 0 <= j < |arms| :: szSs(arms[j].body) < szSArms(arms) +{ + if |arms| > 0 { + SzSArmsElems(arms[1..]); + assert forall j | 1 <= j < |arms| :: arms[j] == arms[1..][j-1]; + } +} + +lemma SzSsElems(ss: seq) + ensures forall j | 0 <= j < |ss| :: szS(ss[j]) <= szSs(ss) +{ + if |ss| > 0 { + SzSsElems(ss[1..]); + assert forall j | 1 <= j < |ss| :: ss[j] == ss[1..][j-1]; + } +} + +// A fully normal list weighs zero; a non-normal one weighs at least one. +lemma NormSsAwZero(ss: seq, b: Backend) + requires normSs(ss, b) + ensures awSs(ss, b) == 0 +{ + if |ss| > 0 { + NormAwZeroS(ss[0], b); + NormSsAwZero(ss[1..], b); + } +} + +lemma NotNormSsAwPos(ss: seq, b: Backend) + requires !normSs(ss, b) + ensures awSs(ss, b) >= 1 +{ + if |ss| >= 2 && tryLetMatchOnMapGet(ss[0], ss[1], ss[2..]).Some? { + } else if !normS(ss[0], b) { + assert awS(ss[0], b) >= 1; + } else { + NotNormSsAwPos(ss[1..], b); + } +} + + +// ═══ Hand-authored: map-identity lemmas and cons/guard helpers ══════════ + +lemma ElemsNormSCons(x: Stmt, rest: seq, b: Backend) + requires normS(x, b) && elemsNormS(rest, b) + ensures elemsNormS([x] + rest, b) +{ + assert ([x] + rest)[0] == x; + assert ([x] + rest)[1..] == rest; +} + +// Guards invoked at walker entry so both the normal-input (weight-0 ties) and +// non-normal cases have the kid-weight facts in scope. +lemma NormKidsAwZeroGuardE(e: Expr, b: Backend) + ensures normKidsE(e, b) ==> awKidsE(e, b) == 0 + ensures !normE(e, b) ==> awE(e, b) == chargeE(e) + awKidsE(e, b) + ensures normE(e, b) ==> normKidsE(e, b) && awE(e, b) == 0 +{ + if normKidsE(e, b) { NormKidsAwZeroE(e, b); } +} + +lemma NormKidsAwZeroGuardS(s: Stmt, b: Backend) + ensures normKidsS(s, b) ==> awKidsS(s, b) == 0 + ensures !normS(s, b) ==> awS(s, b) == chargeS(s) + awKidsS(s, b) + ensures normS(s, b) ==> normKidsS(s, b) && awS(s, b) == 0 +{ + if normKidsS(s, b) { NormKidsAwZeroS(s, b); } +} + +lemma NotNormSsAwPosAll(ss: seq, b: Backend) + ensures !normSs(ss, b) ==> awSs(ss, b) >= 1 +{ + if !normSs(ss, b) { NotNormSsAwPos(ss, b); } +} + +lemma AwSArmsElems(arms: seq, b: Backend) + ensures forall j | 0 <= j < |arms| :: PSs(arms[j].body, b) <= awSArms(arms, b) +{ + if |arms| > 0 { + AwSArmsElems(arms[1..], b); + assert forall j | 1 <= j < |arms| :: arms[j] == arms[1..][j-1]; + } +} + +lemma SeqEMapIdNormal(xs: seq, ys: seq, b: Backend) + requires |ys| == |xs| + requires forall j | 0 <= j < |xs| :: normE(xs[j], b) ==> ys[j] == xs[j] + ensures normSeqE(xs, b) ==> ys == xs +{ + if normSeqE(xs, b) { + NormSeqEElem(xs, b); + assert forall j | 0 <= j < |xs| :: ys[j] == xs[j]; + assert ys == xs; + } +} + +lemma ArmsMapIdNormal(xs: seq, ys: seq, b: Backend) + requires |ys| == |xs| + requires forall j | 0 <= j < |xs| :: ys[j].pattern == xs[j].pattern && (normE(xs[j].body, b) ==> ys[j].body == xs[j].body) + ensures normArms(xs, b) ==> ys == xs +{ + if normArms(xs, b) { + NormArmsElem(xs, b); + assert forall j | 0 <= j < |xs| :: ys[j] == xs[j]; + assert ys == xs; + } +} + +lemma NormFieldsElem(fs: seq, b: Backend) + requires normFields(fs, b) + ensures forall j | 0 <= j < |fs| :: normE(fs[j].value, b) +{ + if |fs| > 0 { + NormFieldsElem(fs[1..], b); + assert forall j | 1 <= j < |fs| :: fs[j] == fs[1..][j-1]; + } +} + +lemma FieldsMapIdNormal(xs: seq, ys: seq, b: Backend) + requires |ys| == |xs| + requires forall j | 0 <= j < |xs| :: ys[j].name == xs[j].name && (normE(xs[j].value, b) ==> ys[j].value == xs[j].value) + ensures normFields(xs, b) ==> ys == xs +{ + if normFields(xs, b) { + NormFieldsElem(xs, b); + assert forall j | 0 <= j < |xs| :: ys[j] == xs[j]; + assert ys == xs; + } +} + +lemma SArmsMapIdNormal(xs: seq, ys: seq, b: Backend) + requires |ys| == |xs| + requires forall j | 0 <= j < |xs| :: ys[j].pattern == xs[j].pattern && (normSs(xs[j].body, b) ==> ys[j].body == xs[j].body) + ensures normSArms(xs, b) ==> ys == xs +{ + if normSArms(xs, b) { + NormSArmsElem(xs, b); + assert forall j | 0 <= j < |xs| :: ys[j] == xs[j]; + assert ys == xs; + } +} + +lemma ElemsNormSElem(ss: seq, b: Backend) + requires elemsNormS(ss, b) + ensures forall j | 0 <= j < |ss| :: normS(ss[j], b) +{ + if |ss| > 0 { + ElemsNormSElem(ss[1..], b); + assert forall j | 1 <= j < |ss| :: ss[j] == ss[1..][j-1]; + } +} + +lemma StmtsMapIdNormal(xs: seq, ys: seq, b: Backend) + requires |ys| == |xs| + requires forall j | 0 <= j < |xs| :: normS(xs[j], b) ==> ys[j] == xs[j] + ensures elemsNormS(xs, b) ==> ys == xs +{ + if elemsNormS(xs, b) { + ElemsNormSElem(xs, b); + assert forall j | 0 <= j < |xs| :: ys[j] == xs[j]; + assert ys == xs; + } +} + + +lemma NormSsAllFacts(ss: seq, b: Backend) + requires normSs(ss, b) + ensures forall j | 0 <= j < |ss| :: normS(ss[j], b) && awS(ss[j], b) == 0 +{ + NormSsElems(ss, b); + ElemsNormSElem(ss, b); + forall j | 0 <= j < |ss| + ensures awS(ss[j], b) == 0 + { + NormAwZeroS(ss[j], b); + } +} + + +lemma AwEntriesElems(es: seq, b: Backend) + ensures forall j | 0 <= j < |es| :: awE(es[j].key, b) <= awEntries(es, b) && awE(es[j].value, b) <= awEntries(es, b) +{ + if |es| > 0 { + AwEntriesElems(es[1..], b); + assert forall j | 1 <= j < |es| :: es[j] == es[1..][j-1]; + } +} + +lemma SzEntriesElems(es: seq) + ensures forall j | 0 <= j < |es| :: szE(es[j].key) < szEntries(es) && szE(es[j].value) < szEntries(es) +{ + if |es| > 0 { + SzEntriesElems(es[1..]); + assert forall j | 1 <= j < |es| :: es[j] == es[1..][j-1]; + } +} + +lemma PointwiseNormEntries(ys: seq, b: Backend) + requires forall j | 0 <= j < |ys| :: normE(ys[j].key, b) && normE(ys[j].value, b) + ensures normEntries(ys, b) +{ + if |ys| > 0 { PointwiseNormEntries(ys[1..], b); } +} + +lemma NormEntriesElem(es: seq, b: Backend) + requires normEntries(es, b) + ensures forall j | 0 <= j < |es| :: normE(es[j].key, b) && normE(es[j].value, b) +{ + if |es| > 0 { + NormEntriesElem(es[1..], b); + assert forall j | 1 <= j < |es| :: es[j] == es[1..][j-1]; + } +} + +lemma EntriesMapIdNormal(xs: seq, ys: seq, b: Backend) + requires |ys| == |xs| + requires forall j | 0 <= j < |xs| :: (normE(xs[j].key, b) ==> ys[j].key == xs[j].key) && (normE(xs[j].value, b) ==> ys[j].value == xs[j].value) + ensures normEntries(xs, b) ==> ys == xs +{ + if normEntries(xs, b) { + NormEntriesElem(xs, b); + assert forall j | 0 <= j < |xs| :: ys[j] == xs[j]; + assert ys == xs; + } +} diff --git a/tools/src/peephole.dfy.gen b/tools/src/peephole.dfy.gen new file mode 100644 index 0000000..13ed20e --- /dev/null +++ b/tools/src/peephole.dfy.gen @@ -0,0 +1,557 @@ +// Generated by lsc from peephole.ts + +datatype Option = None | Some(value: T) + +function {:axiom} patternCtor(p: MatchPattern): Option + +function {:axiom} patternBinders(p: MatchPattern): seq + +function {:axiom} usesName(e: Expr, name: string): bool + +function {:axiom} usesNameInStmts(stmts: seq, name: string): bool + +datatype MethodCall = MethodCall(kind: string, obj: Expr, objTy: Ty, method_: string, args: seq, monadic: bool) + +datatype SomeNoneArms = SomeNoneArms(someArm: MatchArm, noneArm: MatchArm, binder: Option) + +datatype SomeNoneStmtArms = SomeNoneStmtArms(someArm: StmtMatchArm, noneArm: StmtMatchArm, binder: Option) + +datatype Backend = lean | dafny + +datatype Expr = var_(name: string) | num(value_num: int) | bigint(value_bigint: string) | bool_(value_bool: bool) | str(value_str: string) | constructor_(name: string, type_constructor: Option, args: seq) | binop(op: string, left: Expr, right: Expr) | unop(op: string, expr: Expr) | app(fn: string, args: seq, ctorOf: Option) | field(obj: Expr, field: string, fromUnion: Option, ctor: Option, datatypeField: Option) | toNat(expr: Expr) | toReal(expr: Expr) | index(arr: Expr, idx: Expr) | tupleLiteral(elems: seq) | tupleProj(obj: Expr, index: int, arity: int) | record(spread: Option, fields: seq, ctor: Option, ctorOf: Option) | arrayLiteral(elems: seq) | emptyMap | emptySet | mapLiteral(entries: seq) | methodCall(obj: Expr, objTy: Ty, method_: string, args: seq, monadic: bool) | lambda(params: seq, body_lambda: seq) | if_(cond: Expr, then_: Expr, else_: Expr) | match_(scrutinee: Expr, arms: seq) | forall_(var_: string, type_forall: Ty, body_forall: Expr) | exists_(var_: string, type_exists: Ty, body_exists: Expr) | implies(premises: seq, conclusion: Expr) | let(name: string, value_let: Expr, body_let: Expr) | havoc(type_havoc: Ty) | default(type_default: Ty) + +datatype MapEntry = MapEntry(key: Expr, value: Expr) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +datatype Param = Param(name: string, type_: Ty) + +datatype Stmt = let(name: string, type_: Ty, mutable: bool, value: Expr) | assign(target: string, value: Expr) | bind(target: string, value: Expr) | let_bind(name: string, value: Expr) | return_(value: Expr) | break_ | continue_ | if_(cond: Expr, then_: seq, else_: seq) | match_(scrutinee: Expr, arms: seq) | while_(cond: Expr, invariants: seq, decreasing: Option, doneWith: Option, body: seq) | forin(idx: string, bound: Expr, invariants: seq, body: seq) | ghostLet(name: string, type_: Ty, value: Expr) | ghostAssign(target: string, value: Expr) | assert_(expr: Expr, assumed: Option) + +datatype StmtMatchArm = StmtMatchArm(pattern: MatchPattern, body: seq) + +datatype MatchPattern = wild | ctor(ctor: string, binders: seq) + +datatype MatchArm = MatchArm(pattern: MatchPattern, body: Expr) + +datatype RecordField = RecordField(name: string, value: Expr) + +datatype Module = Module(comment: string, imports: seq, options: seq, decls: seq) + +datatype EmitOption = EmitOption(key: string, value: string) + +datatype Decl = inductive_(name: string, typeParams_inductive: Option>, constructors: seq, deriving: seq) | structure(name: string, typeParams_structure: Option>, fields: seq, deriving: seq) | def(name: string, typeParams_def: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_def: Expr) | def_by_method(name: string, typeParams_def_by_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) | method_(name: string, typeParams_method: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body_method: seq) | namespace(name: string, decls: seq) | class_(name: string, fields: seq, methods: seq) | const_(name: string, type_: Ty, value: Expr) | type_alias(name: string, target: Ty) | opaque_type(name: string) | extern(name: string, typeParams_extern: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype Inductive = Inductive(kind: string, name: string, typeParams: Option>, constructors: seq, deriving: seq) + +datatype CtorInfo = CtorInfo(name: string, fields: seq) + +datatype Structure = Structure(kind: string, name: string, typeParams: Option>, fields: seq, deriving: seq) + +datatype FnDef = FnDef(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: Expr) + +datatype FnDefByMethod = FnDefByMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, methodBody: seq) + +datatype FnMethod = FnMethod(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq, decreases_: Option, body: seq) + +datatype Namespace = Namespace(kind: string, name: string, decls: seq) + +datatype ClassDecl = ClassDecl(kind: string, name: string, fields: seq, methods: seq) + +datatype ConstDecl = ConstDecl(kind: string, name: string, type_: Ty, value: Expr) + +datatype TypeAlias = TypeAlias(kind: string, name: string, target: Ty) + +datatype OpaqueType = OpaqueType(kind: string, name: string) + +datatype ExternDecl = ExternDecl(kind: string, name: string, typeParams: seq, params: seq, returnType: Ty, requires_: seq, ensures_: seq) + +datatype IsMapGetResult = IsMapGetResult(obj: Expr, key: Expr, objTy: Ty) + +function isMapGet(e: Expr): Option +{ + if ((((!e.methodCall?) || (!e.objTy.map_?)) || (e.method_ != "get")) || (|e.args| != 1)) then + None + else + Some(IsMapGetResult(e.obj, e.args[0], e.objTy)) +} + +function parseSomeBinder(p: MatchPattern): Option +{ + if (match patternCtor(p) { case Some(i_value) => (i_value != "some") case None => true }) then + None + else + var bs := patternBinders(p); + if (|bs| == 0) then + None + else + if (bs[0] == "_") then + Option.None + else + Option.Some(bs[0]) +} + +function getSomeNoneArms(arms: seq): Option +{ + if (|arms| != 2) then + None + else + var c0 := patternCtor(arms[0].pattern); + var c1 := patternCtor(arms[1].pattern); + var someArm := (if (match c0 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[1]) else Option.None)); + var noneArm := (if (match c0 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[1]) else Option.None)); + match someArm { + case Some(i_someArm_val) => + match noneArm { + case Some(i_noneArm_val) => + Some(SomeNoneArms(i_someArm_val, i_noneArm_val, parseSomeBinder(i_someArm_val.pattern))) + case None => + None + } + case None => + None + } +} + +function getSomeNoneStmtArms(arms: seq): Option +{ + if (|arms| != 2) then + None + else + var c0 := patternCtor(arms[0].pattern); + var c1 := patternCtor(arms[1].pattern); + var someArm := (if (match c0 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "some") case None => false }) then Option.Some(arms[1]) else Option.None)); + var noneArm := (if (match c0 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[0]) else (if (match c1 { case Some(i_value) => (i_value == "none") case None => false }) then Option.Some(arms[1]) else Option.None)); + match someArm { + case Some(i_someArm_val) => + match noneArm { + case Some(i_noneArm_val) => + Some(SomeNoneStmtArms(i_someArm_val, i_noneArm_val, parseSomeBinder(i_someArm_val.pattern))) + case None => + None + } + case None => + None + } +} + +function ruleMatchOnMapGetExpr(e: Expr): Option +{ + match e { + case match_(i_e_scrutinee, i_e_arms) => + var get := isMapGet(i_e_scrutinee); + match get { + case Some(i_get_val) => + var arms := getSomeNoneArms(i_e_arms); + match arms { + case Some(i_arms_val) => + var idx := index(i_get_val.obj, i_get_val.key); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then Expr.let(i_arms_binder_val, idx, i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Expr.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case None => + None + } + case _ => + None + } +} + +function ruleIfFalseElseTrue(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if (isBool(i_e_then, false) && isBool(i_e_else, true)) then + Some(unop("¬", i_e_cond)) + else + None + case _ => + None + } +} + +function ruleIfIdentity(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if (isBool(i_e_then, true) && isBool(i_e_else, false)) then + Some(i_e_cond) + else + None + case _ => + None + } +} + +function ruleIfThenFalse(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if isBool(i_e_else, false) then + Some(binop("∧", i_e_cond, i_e_then)) + else + None + case _ => + None + } +} + +function ruleIfTrueElse(e: Expr): Option +{ + match e { + case if_(i_e_cond, i_e_then, i_e_else) => + if isBool(i_e_then, true) then + Some(binop("∨", i_e_cond, i_e_else)) + else + None + case _ => + None + } +} + +function ruleLetMatchOnMapGetExpr(e: Expr): Option +{ + match e { + case let(i_e_name, i_e_value, i_e_body) => + var get := isMapGet(i_e_value); + match get { + case Some(i_get_val) => + if (!i_e_body.match_?) then + None + else + var m := i_e_body; + var matchOnX := (m.scrutinee.var_? && (m.scrutinee.name == i_e_name)); + if !(matchOnX) then + None + else + var arms := getSomeNoneArms(m.arms); + match arms { + case Some(i_arms_val) => + if (usesName(i_arms_val.someArm.body, i_e_name) || usesName(i_arms_val.noneArm.body, i_e_name)) then + None + else + var idx := index(i_get_val.obj, i_get_val.key); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then Expr.let(i_arms_binder_val, idx, i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Expr.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case None => + None + } + case _ => + None + } +} + +function isBool(e: Expr, v: bool): bool +{ + (e.bool_? && (e.value_bool == v)) +} + +function applyExprRules(e: Expr, backend: Backend): Option +{ + var r1 := ruleMatchOnMapGetExpr(e); + match r1 { + case Some(i_r1_val) => + Some(i_r1_val) + case None => + var r6 := ruleLetMatchOnMapGetExpr(e); + match r6 { + case Some(i_r6_val) => + Some(i_r6_val) + case None => + if (!backend.dafny?) then + None + else + var r4 := ruleIfFalseElseTrue(e); + match r4 { + case Some(i_r4_val) => + Some(i_r4_val) + case None => + var r5 := ruleIfIdentity(e); + match r5 { + case Some(i_r5_val) => + Some(i_r5_val) + case None => + var r2 := ruleIfThenFalse(e); + match r2 { + case Some(i_r2_val) => + Some(i_r2_val) + case None => + var r3 := ruleIfTrueElse(e); + match r3 { + case Some(i_r3_val) => + Some(i_r3_val) + case None => + None + } + } + } + } + } + } +} + +function ruleMatchOnMapGetStmt(s: Stmt): Option +{ + match s { + case match_(i_s_scrutinee, i_s_arms) => + var get := isMapGet(i_s_scrutinee); + match get { + case Some(i_get_val) => + var arms := getSomeNoneStmtArms(i_s_arms); + match arms { + case Some(i_arms_val) => + var idx := index(i_get_val.obj, i_get_val.key); + var valTy := (if i_get_val.objTy.map_? then i_get_val.objTy.value else Ty.unknown); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then ([Stmt.let(i_arms_binder_val, valTy, false, idx)] + i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Stmt.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case None => + None + } + case _ => + None + } +} + +function tryLetMatchOnMapGet(s1: Stmt, s2: Stmt, restStmts: seq): Option +{ + if ((!s1.let?) || s1.mutable) then + None + else + var get := isMapGet(s1.value); + match get { + case Some(i_get_val) => + match s2 { + case match_(i_s2_scrutinee, i_s2_arms) => + var matchOnX := (i_s2_scrutinee.var_? && (i_s2_scrutinee.name == s1.name)); + if !(matchOnX) then + None + else + var arms := getSomeNoneStmtArms(i_s2_arms); + match arms { + case Some(i_arms_val) => + if usesNameInStmts(restStmts, s1.name) then + None + else + var idx := index(i_get_val.obj, i_get_val.key); + var valTy := (if i_get_val.objTy.map_? then i_get_val.objTy.value else Ty.unknown); + var someBody := (match i_arms_val.binder { case Some(i_arms_binder_val) => (if (|i_arms_binder_val| > 0) then ([Stmt.let(i_arms_binder_val, valTy, false, idx)] + i_arms_val.someArm.body) else i_arms_val.someArm.body) case None => i_arms_val.someArm.body }); + var has := methodCall(i_get_val.obj, i_get_val.objTy, "has", [i_get_val.key], false); + Some(Stmt.if_(has, someBody, i_arms_val.noneArm.body)) + case None => + None + } + case _ => + None + } + case None => + None + } +} + +function rewriteStmtListPairs(stmts: seq, backend: Backend): seq +{ + if (|stmts| == 0) then + [] + else + if (|stmts| >= 2) then + var merged := tryLetMatchOnMapGet(stmts[0], stmts[1], stmts[2..]); + match merged { + case Some(i_merged_val) => + ([peepholeStmt(i_merged_val, backend)] + rewriteStmtListPairs(stmts[2..], backend)) + case None => + ([stmts[0]] + rewriteStmtListPairs(stmts[1..], backend)) + } + else + ([stmts[0]] + rewriteStmtListPairs(stmts[1..], backend)) +} + +function pairScanToFix(stmts: seq, backend: Backend): seq +{ + var once := rewriteStmtListPairs(stmts, backend); + if (|once| < |stmts|) then + pairScanToFix(once, backend) + else + once +} + +function peepholeStmts(stmts: seq, backend: Backend): seq +{ + pairScanToFix(seq(|stmts|, i_map requires 0 <= i_map < |stmts| => var s := stmts[i_map]; peepholeStmt(s, backend)), backend) +} + +function peepholeExpr(e: Expr, backend: Backend): Expr +{ + var cur := rewriteChildrenExpr(e, backend); + var hit := applyExprRules(cur, backend); + match hit { + case Some(i_hit_val) => + peepholeExpr(i_hit_val, backend) + case None => + cur + } +} + +function rewriteChildrenExpr(e: Expr, backend: Backend): Expr +{ + match e { + case var_(i_e_name) => + e + case num(i_e_value) => + e + case bigint(i_e_value) => + e + case bool_(i_e_value) => + e + case str(i_e_value) => + e + case emptyMap => + e + case emptySet => + e + case havoc(i_e_type) => + e + case default(i_e_type) => + e + case constructor_(i_e_name, i_e_type, i_e_args) => + e.(args := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend))) + case mapLiteral(i_e_entries) => + e.(entries := seq(|i_e_entries|, i_map requires 0 <= i_map < |i_e_entries| => var en := i_e_entries[i_map]; MapEntry(peepholeExpr(en.key, backend), peepholeExpr(en.value, backend)))) + case binop(i_e_op, i_e_left, i_e_right) => + e.(left := peepholeExpr(i_e_left, backend), right := peepholeExpr(i_e_right, backend)) + case unop(i_e_op, i_e_expr) => + e.(expr := peepholeExpr(i_e_expr, backend)) + case implies(i_e_premises, i_e_conclusion) => + e.(premises := seq(|i_e_premises|, i_map requires 0 <= i_map < |i_e_premises| => var p := i_e_premises[i_map]; peepholeExpr(p, backend)), conclusion := peepholeExpr(i_e_conclusion, backend)) + case app(i_e_fn, i_e_args, i_e_ctorOf) => + e.(args := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend))) + case field(i_e_obj, i_e_field, i_e_fromUnion, i_e_ctor, i_e_datatypeField) => + e.(obj := peepholeExpr(i_e_obj, backend)) + case toNat(i_e_expr) => + e.(expr := peepholeExpr(i_e_expr, backend)) + case toReal(i_e_expr) => + e.(expr := peepholeExpr(i_e_expr, backend)) + case index(i_e_arr, i_e_idx) => + e.(arr := peepholeExpr(i_e_arr, backend), idx := peepholeExpr(i_e_idx, backend)) + case tupleLiteral(i_e_elems) => + e.(elems := seq(|i_e_elems|, i_map requires 0 <= i_map < |i_e_elems| => var el := i_e_elems[i_map]; peepholeExpr(el, backend))) + case tupleProj(i_e_obj, i_e_index, i_e_arity) => + e.(obj := peepholeExpr(i_e_obj, backend)) + case record(i_e_spread, i_e_fields, i_e_ctor, i_e_ctorOf) => + e.(spread := (match i_e_spread { case Some(i_e_spread_val) => Option.Some(peepholeExpr(i_e_spread_val, backend)) case None => Option.None }), fields := seq(|i_e_fields|, i_map requires 0 <= i_map < |i_e_fields| => var fi := i_e_fields[i_map]; fi.(value := peepholeExpr(fi.value, backend)))) + case arrayLiteral(i_e_elems) => + e.(elems := seq(|i_e_elems|, i_map requires 0 <= i_map < |i_e_elems| => var el := i_e_elems[i_map]; peepholeExpr(el, backend))) + case if_(i_e_cond, i_e_then, i_e_else) => + e.(cond := peepholeExpr(i_e_cond, backend), then_ := peepholeExpr(i_e_then, backend), else_ := peepholeExpr(i_e_else, backend)) + case match_(i_e_scrutinee, i_e_arms) => + var scr := peepholeExpr(i_e_scrutinee, backend); + e.(scrutinee := scr, arms := seq(|i_e_arms|, i_map requires 0 <= i_map < |i_e_arms| => var a := i_e_arms[i_map]; a.(body := peepholeExpr(a.body, backend)))) + case forall_(i_e_var, i_e_type, i_e_body) => + e.(body_forall := peepholeExpr(i_e_body, backend)) + case exists_(i_e_var, i_e_type, i_e_body) => + e.(body_exists := peepholeExpr(i_e_body, backend)) + case let(i_e_name, i_e_value, i_e_body) => + e.(value_let := peepholeExpr(i_e_value, backend), body_let := peepholeExpr(i_e_body, backend)) + case methodCall(i_e_obj, i_e_objTy, i_e_method, i_e_args, i_e_monadic) => + e.(obj := peepholeExpr(i_e_obj, backend), args := seq(|i_e_args|, i_map requires 0 <= i_map < |i_e_args| => var a := i_e_args[i_map]; peepholeExpr(a, backend))) + case lambda(i_e_params, i_e_body) => + e.(body_lambda := peepholeStmts(i_e_body, backend)) + } +} + +function peepholeStmt(s: Stmt, backend: Backend): Stmt +{ + var cur := rewriteChildrenStmt(s, backend); + var r := ruleMatchOnMapGetStmt(cur); + match r { + case Some(i_r_val) => + peepholeStmt(i_r_val, backend) + case None => + cur + } +} + +function rewriteChildrenStmt(s: Stmt, backend: Backend): Stmt +{ + match s { + case let(i_s_name, i_s_type, i_s_mutable, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case assign(i_s_target, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case bind(i_s_target, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case let_bind(i_s_name, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case return_(i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case break_ => + s + case continue_ => + s + case if_(i_s_cond, i_s_then, i_s_else) => + s.(cond := peepholeExpr(i_s_cond, backend), then_ := peepholeStmts(i_s_then, backend), else_ := peepholeStmts(i_s_else, backend)) + case match_(i_s_scrutinee, i_s_arms) => + var scr := peepholeExpr(i_s_scrutinee, backend); + s.(scrutinee := scr, arms := seq(|i_s_arms|, i_map requires 0 <= i_map < |i_s_arms| => var a := i_s_arms[i_map]; a.(body := peepholeStmts(a.body, backend)))) + case while_(i_s_cond, i_s_invariants, i_s_decreasing, i_s_doneWith, i_s_body) => + s.(cond := peepholeExpr(i_s_cond, backend), invariants := seq(|i_s_invariants|, i_map requires 0 <= i_map < |i_s_invariants| => var i := i_s_invariants[i_map]; peepholeExpr(i, backend)), body := peepholeStmts(i_s_body, backend)) + case forin(i_s_idx, i_s_bound, i_s_invariants, i_s_body) => + s.(bound := peepholeExpr(i_s_bound, backend), invariants := seq(|i_s_invariants|, i_map requires 0 <= i_map < |i_s_invariants| => var i := i_s_invariants[i_map]; peepholeExpr(i, backend)), body := peepholeStmts(i_s_body, backend)) + case ghostLet(i_s_name, i_s_type, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case ghostAssign(i_s_target, i_s_value) => + s.(value := peepholeExpr(i_s_value, backend)) + case assert_(i_s_expr, i_s_assumed) => + s.(expr := peepholeExpr(i_s_expr, backend)) + } +} + +function peepholeModule(mod: Module, backend: Backend): Module +{ + mod.(decls := (var s_map := mod.decls; seq(|s_map|, i_map requires 0 <= i_map < |s_map| => var d := s_map[i_map]; peepholeDecl(d, backend)))) +} + +function peepholeMethod(m: FnMethod, backend: Backend): FnMethod +{ + var re := (x: Expr) => peepholeExpr(x, backend); + m.(body := peepholeStmts(m.body, backend), requires_ := (var s_map := m.requires_; seq(|s_map|, i_map requires 0 <= i_map < |s_map| => (re)(s_map[i_map]))), ensures_ := (var s_map := m.ensures_; seq(|s_map|, i_map requires 0 <= i_map < |s_map| => (re)(s_map[i_map]))), decreases_ := (match m.decreases_ { case Some(i_m_decreases_val) => Option.Some(re(i_m_decreases_val)) case None => Option.None })) +} + +function peepholeDecl(d: Decl, backend: Backend): Decl +{ + var re := (x: Expr) => peepholeExpr(x, backend); + match d { + case def(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures, i_d_decreases, i_d_body) => + d.(body_def := re(i_d_body), requires_ := seq(|i_d_requires|, i_map requires 0 <= i_map < |i_d_requires| => (re)(i_d_requires[i_map])), ensures_ := seq(|i_d_ensures|, i_map requires 0 <= i_map < |i_d_ensures| => (re)(i_d_ensures[i_map])), decreases_ := (match i_d_decreases { case Some(i_d_decreases_val) => Option.Some(re(i_d_decreases_val)) case None => Option.None })) + case def_by_method(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures, i_d_decreases, i_d_methodBody) => + d.(methodBody := peepholeStmts(i_d_methodBody, backend), requires_ := seq(|i_d_requires|, i_map requires 0 <= i_map < |i_d_requires| => (re)(i_d_requires[i_map])), ensures_ := seq(|i_d_ensures|, i_map requires 0 <= i_map < |i_d_ensures| => (re)(i_d_ensures[i_map])), decreases_ := (match i_d_decreases { case Some(i_d_decreases_val) => Option.Some(re(i_d_decreases_val)) case None => Option.None })) + case method_(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures, i_d_decreases, i_d_body) => + d.(body_method := peepholeStmts(i_d_body, backend), requires_ := seq(|i_d_requires|, i_map requires 0 <= i_map < |i_d_requires| => (re)(i_d_requires[i_map])), ensures_ := seq(|i_d_ensures|, i_map requires 0 <= i_map < |i_d_ensures| => (re)(i_d_ensures[i_map])), decreases_ := (match i_d_decreases { case Some(i_d_decreases_val) => Option.Some(re(i_d_decreases_val)) case None => Option.None })) + case namespace(i_d_name, i_d_decls) => + d.(decls := seq(|i_d_decls|, i_map requires 0 <= i_map < |i_d_decls| => var x := i_d_decls[i_map]; peepholeDecl(x, backend))) + case class_(i_d_name, i_d_fields, i_d_methods) => + d.(methods := seq(|i_d_methods|, i_map requires 0 <= i_map < |i_d_methods| => var m := i_d_methods[i_map]; peepholeMethod(m, backend))) + case const_(i_d_name, i_d_type, i_d_value) => + d.(value := re(i_d_value)) + case inductive_(i_d_name, i_d_typeParams, i_d_constructors, i_d_deriving) => + d + case structure(i_d_name, i_d_typeParams, i_d_fields, i_d_deriving) => + d + case type_alias(i_d_name, i_d_target) => + d + case opaque_type(i_d_name) => + d + case extern(i_d_name, i_d_typeParams, i_d_params, i_d_returnType, i_d_requires, i_d_ensures) => + d + } +} diff --git a/tools/src/peephole.ts b/tools/src/peephole.ts index 78aeb96..880b6bf 100644 --- a/tools/src/peephole.ts +++ b/tools/src/peephole.ts @@ -13,46 +13,14 @@ * * Statement-level rule 1 also handles match-statement on m.get. */ -import type { Expr, Stmt, Decl, Module, MatchArm, StmtMatchArm, MatchPattern } from "./ir.js"; +import type { Expr, Stmt, Decl, Module, FnMethod, MatchArm, StmtMatchArm, MatchPattern } from "./ir.js"; import { patternCtor, patternBinders, usesName, usesNameInStmts } from "./ir.js"; -// ── Generic walkers (same shape as transform.ts) ───────────── - -function mapExpr(e: Expr, f: (e: Expr) => Expr | null): Expr { - const hit = f(e); - if (hit) return hit; - const r = (x: Expr) => mapExpr(x, f); - switch (e.kind) { - case "var": case "num": case "bigint": case "bool": case "str": case "constructor": - case "emptyMap": case "emptySet": case "havoc": case "default": case "mapLiteral": return e; - case "binop": return { ...e, left: r(e.left), right: r(e.right) }; - case "unop": return { ...e, expr: r(e.expr) }; - case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) }; - case "app": return { ...e, args: e.args.map(r) }; - case "field": return { ...e, obj: r(e.obj) }; - case "toNat": return { ...e, expr: r(e.expr) }; - case "toReal": return { ...e, expr: r(e.expr) }; - case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) }; - case "tupleLiteral": return { ...e, elems: e.elems.map(r) }; - case "tupleProj": return { ...e, obj: r(e.obj) }; - case "record": return { ...e, spread: e.spread ? r(e.spread) : null, - fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) }; - case "arrayLiteral": return { ...e, elems: e.elems.map(r) }; - case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) }; - case "match": { - const scr = r(e.scrutinee); - return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) }; - } - case "forall": return { ...e, body: r(e.body) }; - case "exists": return { ...e, body: r(e.body) }; - case "let": return { ...e, value: r(e.value), body: r(e.body) }; - case "methodCall": return { ...e, obj: r(e.obj), args: e.args.map(r) }; - case "lambda": return e; - } -} - // (Note: peephole rules now bind once via let/var rather than substitute, -// so semantics are preserved under any mutation. No substVar helpers needed.) +// so semantics are preserved under any mutation. No substVar helpers needed. +// Reference checks are ir.ts's usesName/usesNameInStmts — conservative in the +// right direction for the drop-the-binding gates: also counting app/ctor name +// references and lambda bodies can only suppress a rewrite, never misapply one.) // ── Shape detection ────────────────────────────────────────── @@ -67,16 +35,35 @@ function isMapGet(e: Expr): { obj: Expr; key: Expr; objTy: MethodCall["objTy"] } /** Binder of a Some arm — its name, or null for `.some _` / a non-`some` pattern. */ function parseSomeBinder(p: MatchPattern): string | null { if (patternCtor(p) !== "some") return null; - const b = patternBinders(p)[0]; - return b === undefined || b === "_" ? null : b; + const bs = patternBinders(p); + if (bs.length === 0) return null; + return bs[0] === "_" ? null : bs[0]; +} + +/** Identify a Some/None match's arms (R3: monomorphic per arm type — the + * generic's `body` would be `Expr` in one instantiation and `Stmt[]` in the + * other, and the erasure doctrine has no honest single bound). */ +interface SomeNoneArms { someArm: MatchArm; noneArm: MatchArm; binder: string | null } + +function getSomeNoneArms(arms: MatchArm[]): SomeNoneArms | null { + if (arms.length !== 2) return null; + const c0 = patternCtor(arms[0].pattern); + const c1 = patternCtor(arms[1].pattern); + const someArm = c0 === "some" ? arms[0] : c1 === "some" ? arms[1] : null; + const noneArm = c0 === "none" ? arms[0] : c1 === "none" ? arms[1] : null; + if (someArm === null || noneArm === null) return null; + return { someArm, noneArm, binder: parseSomeBinder(someArm.pattern) }; } -/** Identify a Some/None match's arms. */ -function getSomeNoneArms(arms: A[]): { someArm: A; noneArm: A; binder: string | null } | null { +interface SomeNoneStmtArms { someArm: StmtMatchArm; noneArm: StmtMatchArm; binder: string | null } + +function getSomeNoneStmtArms(arms: StmtMatchArm[]): SomeNoneStmtArms | null { if (arms.length !== 2) return null; - const someArm = arms.find(a => patternCtor(a.pattern) === "some"); - const noneArm = arms.find(a => patternCtor(a.pattern) === "none"); - if (!someArm || !noneArm) return null; + const c0 = patternCtor(arms[0].pattern); + const c1 = patternCtor(arms[1].pattern); + const someArm = c0 === "some" ? arms[0] : c1 === "some" ? arms[1] : null; + const noneArm = c0 === "none" ? arms[0] : c1 === "none" ? arms[1] : null; + if (someArm === null || noneArm === null) return null; return { someArm, noneArm, binder: parseSomeBinder(someArm.pattern) }; } @@ -138,7 +125,8 @@ function ruleLetMatchOnMapGetExpr(e: Expr): Expr | null { if (!get) return null; if (e.body.kind !== "match") return null; const m = e.body; - const matchOnX = m.scrutinee.kind === "var" && m.scrutinee.name === e.name; + const matchOnX = + m.scrutinee.kind === "var" && m.scrutinee.name === e.name; if (!matchOnX) return null; const arms = getSomeNoneArms(m.arms); if (!arms) return null; @@ -163,17 +151,30 @@ function isBool(e: Expr, v: boolean): boolean { // `if n = 0 then true else ... allExpensesValid expenses (n - 1) ...` // where the recursive call needs the if-condition to bound `n > 0`. // So they're applied only for Dafny. -const MAP_GET_RULES = [ - ruleMatchOnMapGetExpr, - ruleLetMatchOnMapGetExpr, -]; -const BOOL_RULES = [ - ruleIfFalseElseTrue, - ruleIfIdentity, - ruleIfThenFalse, - ruleIfTrueElse, -]; -let EXPR_RULES: ((e: Expr) => Expr | null)[] = [...MAP_GET_RULES, ...BOOL_RULES]; +type Backend = "lean" | "dafny"; + +/** Direct rule chain (R1): first hit wins, in the old EXPR_RULES order — + * map-get rules, then (Dafny only, see comment above) the boolean + * simplifications. A chain of direct calls over a threaded backend flag + * rather than a rule table: the subset has no function-valued collections + * (`rules[i](e)` is out of fragment) and loops lower to methods, so this + * shape is what remains. */ +function applyExprRules(e: Expr, backend: Backend): Expr | null { + const r1 = ruleMatchOnMapGetExpr(e); + if (r1 !== null) return r1; + const r6 = ruleLetMatchOnMapGetExpr(e); + if (r6 !== null) return r6; + if (backend !== "dafny") return null; + const r4 = ruleIfFalseElseTrue(e); + if (r4 !== null) return r4; + const r5 = ruleIfIdentity(e); + if (r5 !== null) return r5; + const r2 = ruleIfThenFalse(e); + if (r2 !== null) return r2; + const r3 = ruleIfTrueElse(e); + if (r3 !== null) return r3; + return null; +} // ── Statement rewrite rules ────────────────────────────────── @@ -185,7 +186,7 @@ function ruleMatchOnMapGetStmt(s: Stmt): Stmt | null { if (s.kind !== "match") return null; const get = isMapGet(s.scrutinee); if (!get) return null; - const arms = getSomeNoneArms(s.arms); + const arms = getSomeNoneStmtArms(s.arms); if (!arms) return null; const idx: Expr = { kind: "index", arr: get.obj, idx: get.key }; const valTy = get.objTy.kind === "map" ? get.objTy.value : { kind: "unknown" as const }; @@ -196,9 +197,6 @@ function ruleMatchOnMapGetStmt(s: Stmt): Stmt | null { return { kind: "if", cond: has, then: someBody, else: arms.noneArm.body }; } -const STMT_RULES = [ruleMatchOnMapGetStmt]; - -// ── Variable use detection ─────────────────────────────────── // ── Statement-list rules (pairs of adjacent stmts) ────────── @@ -211,9 +209,10 @@ function tryLetMatchOnMapGet(s1: Stmt, s2: Stmt, restStmts: Stmt[]): Stmt | null const get = isMapGet(s1.value); if (!get) return null; if (s2.kind !== "match") return null; - const matchOnX = s2.scrutinee.kind === "var" && s2.scrutinee.name === s1.name; + const matchOnX = + s2.scrutinee.kind === "var" && s2.scrutinee.name === s1.name; if (!matchOnX) return null; - const arms = getSomeNoneArms(s2.arms); + const arms = getSomeNoneStmtArms(s2.arms); if (!arms) return null; if (usesNameInStmts(restStmts, s1.name)) return null; const idx: Expr = { kind: "index", arr: get.obj, idx: get.key }; @@ -226,149 +225,140 @@ function tryLetMatchOnMapGet(s1: Stmt, s2: Stmt, restStmts: Stmt[]): Stmt | null } /** Walk a statement list applying pair rules. */ -function rewriteStmtListPairs(stmts: Stmt[]): Stmt[] { - const result: Stmt[] = []; - let i = 0; - while (i < stmts.length) { - if (i + 1 < stmts.length) { - const merged = tryLetMatchOnMapGet(stmts[i], stmts[i + 1], stmts.slice(i + 2)); - if (merged) { - // Recurse into the new stmt's children to peephole them too - result.push(peepholeStmt(merged)); - i += 2; - continue; - } +function rewriteStmtListPairs(stmts: Stmt[], backend: Backend): Stmt[] { + if (stmts.length === 0) return []; + if (stmts.length >= 2) { + const merged = tryLetMatchOnMapGet(stmts[0], stmts[1], stmts.slice(2)); + if (merged) { + // Recurse into the new stmt's children to peephole them too + return [peepholeStmt(merged, backend), ...rewriteStmtListPairs(stmts.slice(2), backend)]; } - result.push(stmts[i]); - i++; } - return result; + return [stmts[0], ...rewriteStmtListPairs(stmts.slice(1), backend)]; +} + +/** Pair-scan to a fixed point: a merge can flip a later gate and expose a new + * adjacent pair, so rescan until a pass merges nothing. Each merge shortens + * the list, so passes are bounded by the list length. */ +function pairScanToFix(stmts: Stmt[], backend: Backend): Stmt[] { + const once = rewriteStmtListPairs(stmts, backend); + return once.length < stmts.length ? pairScanToFix(once, backend) : once; } /** Peephole a statement list: per-stmt rules first, then pair rules. */ -function peepholeStmts(stmts: Stmt[]): Stmt[] { - return rewriteStmtListPairs(stmts.map(peepholeStmt)); +function peepholeStmts(stmts: Stmt[], backend: Backend): Stmt[] { + return pairScanToFix(stmts.map(s => peepholeStmt(s, backend)), backend); } // ── Bottom-up rewrite to fixed point at each node ─────────── -function peepholeExpr(e: Expr): Expr { - // Recurse into children first - const rChildren = rewriteChildrenExpr(e); - // Apply rules at this node, looping until no rule fires - let cur = rChildren; - for (let guard = 0; guard < 100; guard++) { - let changed = false; - for (const rule of EXPR_RULES) { - const r = rule(cur); - if (r !== null) { - // Re-peephole the result (its children may now match new rules) - cur = peepholeExpr(r); - changed = true; - break; - } - } - if (!changed) break; - } - return cur; +/** Termination (proved Dafny-side, not exposed here): every rule strictly + * decreases (match-count, if-count) lexicographically — rules 1/6 trade a + * match for an if, rules 2–5 drop an if — so the rule-hit recursion is + * finite; child recursion is structural. No fuel needed: a recursive call's + * root is already at fixed point when it returns, so one hit-then-recurse + * replaces the old retry loop exactly. */ +function peepholeExpr(e: Expr, backend: Backend): Expr { + // Recurse into children first, then apply the first matching rule + const cur = rewriteChildrenExpr(e, backend); + const hit = applyExprRules(cur, backend); + return hit !== null ? peepholeExpr(hit, backend) : cur; } -function rewriteChildrenExpr(e: Expr): Expr { - const r = peepholeExpr; +// Direct recursive calls throughout (no `const r = (x) => …` shorthand): a +// call through a lambda-bound alias erases the callee's argument from the +// termination measure, so the generated Dafny cannot prove these walkers +// terminate (the closure's parameter is unbounded at its definition site). +function rewriteChildrenExpr(e: Expr, backend: Backend): Expr { switch (e.kind) { - case "var": case "num": case "bigint": case "bool": case "str": case "constructor": - case "emptyMap": case "emptySet": case "havoc": case "default": case "mapLiteral": return e; - case "binop": return { ...e, left: r(e.left), right: r(e.right) }; - case "unop": return { ...e, expr: r(e.expr) }; - case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) }; - case "app": return { ...e, args: e.args.map(r) }; - case "field": return { ...e, obj: r(e.obj) }; - case "toNat": return { ...e, expr: r(e.expr) }; - case "toReal": return { ...e, expr: r(e.expr) }; - case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) }; - case "tupleLiteral": return { ...e, elems: e.elems.map(r) }; - case "tupleProj": return { ...e, obj: r(e.obj) }; - case "record": return { ...e, spread: e.spread ? r(e.spread) : null, - fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) }; - case "arrayLiteral": return { ...e, elems: e.elems.map(r) }; - case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) }; + case "var": case "num": case "bigint": case "bool": case "str": + case "emptyMap": case "emptySet": case "havoc": case "default": return e; + case "constructor": return { ...e, args: e.args.map(a => peepholeExpr(a, backend)) }; + case "mapLiteral": return { ...e, entries: e.entries.map(en => ({ key: peepholeExpr(en.key, backend), value: peepholeExpr(en.value, backend) })) }; + case "binop": return { ...e, left: peepholeExpr(e.left, backend), right: peepholeExpr(e.right, backend) }; + case "unop": return { ...e, expr: peepholeExpr(e.expr, backend) }; + case "implies": return { ...e, premises: e.premises.map(p => peepholeExpr(p, backend)), conclusion: peepholeExpr(e.conclusion, backend) }; + case "app": return { ...e, args: e.args.map(a => peepholeExpr(a, backend)) }; + case "field": return { ...e, obj: peepholeExpr(e.obj, backend) }; + case "toNat": return { ...e, expr: peepholeExpr(e.expr, backend) }; + case "toReal": return { ...e, expr: peepholeExpr(e.expr, backend) }; + case "index": return { ...e, arr: peepholeExpr(e.arr, backend), idx: peepholeExpr(e.idx, backend) }; + case "tupleLiteral": return { ...e, elems: e.elems.map(el => peepholeExpr(el, backend)) }; + case "tupleProj": return { ...e, obj: peepholeExpr(e.obj, backend) }; + case "record": return { ...e, spread: e.spread ? peepholeExpr(e.spread, backend) : null, + fields: e.fields.map(fi => ({ ...fi, value: peepholeExpr(fi.value, backend) })) }; + case "arrayLiteral": return { ...e, elems: e.elems.map(el => peepholeExpr(el, backend)) }; + case "if": return { ...e, cond: peepholeExpr(e.cond, backend), then: peepholeExpr(e.then, backend), else: peepholeExpr(e.else, backend) }; case "match": { - const scr = r(e.scrutinee); - return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) }; + const scr = peepholeExpr(e.scrutinee, backend); + return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: peepholeExpr(a.body, backend) })) }; } - case "forall": return { ...e, body: r(e.body) }; - case "exists": return { ...e, body: r(e.body) }; - case "let": return { ...e, value: r(e.value), body: r(e.body) }; - case "methodCall": return { ...e, obj: r(e.obj), args: e.args.map(r) }; - case "lambda": return { ...e, body: peepholeStmts(e.body) }; + case "forall": return { ...e, body: peepholeExpr(e.body, backend) }; + case "exists": return { ...e, body: peepholeExpr(e.body, backend) }; + case "let": return { ...e, value: peepholeExpr(e.value, backend), body: peepholeExpr(e.body, backend) }; + case "methodCall": return { ...e, obj: peepholeExpr(e.obj, backend), args: e.args.map(a => peepholeExpr(a, backend)) }; + case "lambda": return { ...e, body: peepholeStmts(e.body, backend) }; } } -function peepholeStmt(s: Stmt): Stmt { - const rChildren = rewriteChildrenStmt(s); - let cur = rChildren; - for (let guard = 0; guard < 100; guard++) { - let changed = false; - for (const rule of STMT_RULES) { - const r = rule(cur); - if (r !== null) { - cur = peepholeStmt(r); - changed = true; - break; - } - } - if (!changed) break; - } - return cur; +function peepholeStmt(s: Stmt, backend: Backend): Stmt { + const cur = rewriteChildrenStmt(s, backend); + const r = ruleMatchOnMapGetStmt(cur); + return r !== null ? peepholeStmt(r, backend) : cur; } -function rewriteChildrenStmt(s: Stmt): Stmt { - const re = peepholeExpr; - const rs = peepholeStmts; +function rewriteChildrenStmt(s: Stmt, backend: Backend): Stmt { switch (s.kind) { - case "let": return { ...s, value: re(s.value) }; - case "assign": return { ...s, value: re(s.value) }; - case "bind": return { ...s, value: re(s.value) }; - case "let-bind": return { ...s, value: re(s.value) }; - case "return": return { ...s, value: re(s.value) }; + case "let": return { ...s, value: peepholeExpr(s.value, backend) }; + case "assign": return { ...s, value: peepholeExpr(s.value, backend) }; + case "bind": return { ...s, value: peepholeExpr(s.value, backend) }; + case "let-bind": return { ...s, value: peepholeExpr(s.value, backend) }; + case "return": return { ...s, value: peepholeExpr(s.value, backend) }; case "break": case "continue": return s; - case "if": return { ...s, cond: re(s.cond), then: rs(s.then), else: rs(s.else) }; + case "if": return { ...s, cond: peepholeExpr(s.cond, backend), then: peepholeStmts(s.then, backend), else: peepholeStmts(s.else, backend) }; case "match": { - const scr = re(s.scrutinee); - return { ...s, scrutinee: scr, arms: s.arms.map(a => ({ ...a, body: rs(a.body) })) }; + const scr = peepholeExpr(s.scrutinee, backend); + return { ...s, scrutinee: scr, arms: s.arms.map(a => ({ ...a, body: peepholeStmts(a.body, backend) })) }; } - case "while": return { ...s, cond: re(s.cond), invariants: s.invariants.map(re), body: rs(s.body) }; - case "forin": return { ...s, bound: re(s.bound), invariants: s.invariants.map(re), body: rs(s.body) }; - case "ghostLet": return { ...s, value: re(s.value) }; - case "ghostAssign": return { ...s, value: re(s.value) }; - case "assert": return { ...s, expr: re(s.expr) }; + case "while": return { ...s, cond: peepholeExpr(s.cond, backend), invariants: s.invariants.map(i => peepholeExpr(i, backend)), body: peepholeStmts(s.body, backend) }; + case "forin": return { ...s, bound: peepholeExpr(s.bound, backend), invariants: s.invariants.map(i => peepholeExpr(i, backend)), body: peepholeStmts(s.body, backend) }; + case "ghostLet": return { ...s, value: peepholeExpr(s.value, backend) }; + case "ghostAssign": return { ...s, value: peepholeExpr(s.value, backend) }; + case "assert": return { ...s, expr: peepholeExpr(s.expr, backend) }; } } // ── Module entry ──────────────────────────────────────────── -export function peepholeModule(mod: Module, backend: "lean" | "dafny" = "dafny"): Module { - EXPR_RULES = backend === "dafny" ? [...MAP_GET_RULES, ...BOOL_RULES] : MAP_GET_RULES; - return { ...mod, decls: mod.decls.map(peepholeDecl) }; +export function peepholeModule(mod: Module, backend: Backend = "dafny"): Module { + return { ...mod, decls: mod.decls.map(d => peepholeDecl(d, backend)) }; +} + +function peepholeMethod(m: FnMethod, backend: Backend): FnMethod { + const re = (x: Expr) => peepholeExpr(x, backend); + return { ...m, body: peepholeStmts(m.body, backend), + requires: m.requires.map(re), ensures: m.ensures.map(re), + decreases: m.decreases ? re(m.decreases) : null }; } -function peepholeDecl(d: Decl): Decl { +function peepholeDecl(d: Decl, backend: Backend): Decl { + const re = (x: Expr) => peepholeExpr(x, backend); switch (d.kind) { case "def": - return { ...d, body: peepholeExpr(d.body), - requires: d.requires.map(peepholeExpr), ensures: d.ensures.map(peepholeExpr), - decreases: d.decreases ? peepholeExpr(d.decreases) : null }; + return { ...d, body: re(d.body), + requires: d.requires.map(re), ensures: d.ensures.map(re), + decreases: d.decreases ? re(d.decreases) : null }; case "def-by-method": - return { ...d, methodBody: peepholeStmts(d.methodBody), - requires: d.requires.map(peepholeExpr), ensures: d.ensures.map(peepholeExpr), - decreases: d.decreases ? peepholeExpr(d.decreases) : null }; + return { ...d, methodBody: peepholeStmts(d.methodBody, backend), + requires: d.requires.map(re), ensures: d.ensures.map(re), + decreases: d.decreases ? re(d.decreases) : null }; case "method": - return { ...d, body: peepholeStmts(d.body), - requires: d.requires.map(peepholeExpr), ensures: d.ensures.map(peepholeExpr), - decreases: d.decreases ? peepholeExpr(d.decreases) : null }; - case "namespace": return { ...d, decls: d.decls.map(peepholeDecl) }; - case "class": return { ...d, methods: d.methods.map(m => peepholeDecl(m) as typeof m) }; - case "const": return { ...d, value: peepholeExpr(d.value) }; + return { ...d, body: peepholeStmts(d.body, backend), + requires: d.requires.map(re), ensures: d.ensures.map(re), + decreases: d.decreases ? re(d.decreases) : null }; + case "namespace": return { ...d, decls: d.decls.map(x => peepholeDecl(x, backend)) }; + case "class": return { ...d, methods: d.methods.map(m => peepholeMethod(m, backend)) }; + case "const": return { ...d, value: re(d.value) }; case "inductive": case "structure": case "type-alias": diff --git a/tools/src/resolve.ts b/tools/src/resolve.ts index 3e4eda4..c929e9b 100644 --- a/tools/src/resolve.ts +++ b/tools/src/resolve.ts @@ -278,7 +278,7 @@ function collectAndChainNarrowings(cond: TExpr, ctx: Ctx): Ctx { } // Positive discriminant check (`x.kind === "bool"`): record the variant so // field reads on the path resolve against that variant's field types. - const vf = variantFact(cond, { decls: ctx.typeDecls, oc: { n: 0 } }); + const vf = variantFact(cond, { decls: ctx.typeDecls, ocN: 0 }); if (vf) { const path = asTExprAccessPath(vf.scrutinee); if (path) { diff --git a/tools/src/typedecls.ts b/tools/src/typedecls.ts index 51a6342..7d6b62c 100644 --- a/tools/src/typedecls.ts +++ b/tools/src/typedecls.ts @@ -16,7 +16,9 @@ import type { Ty } from "./typedir.js"; import type { TypeDeclInfo } from "./types.js"; -export type TypeDecls = readonly TypeDeclInfo[]; +// Not `readonly TypeDeclInfo[]`: the readonly array modifier has no backend +// model, so the self-run would synthesize an opaque type for the alias. +export type TypeDecls = TypeDeclInfo[]; /** `Foo` → `Foo`. The single home of the generic-base slice; §5.1 * (structured generic args) retires it. */ diff --git a/tools/src/typedir.dfy b/tools/src/typedir.dfy new file mode 100644 index 0000000..56bc222 --- /dev/null +++ b/tools/src/typedir.dfy @@ -0,0 +1,288 @@ +// Generated by lsc from typedir.ts + +datatype Option = None | Some(value: T) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +function Ty_kind(t: Ty): string +{ + match t { + case bool_ => + "bool" + case nat_(i_0) => + "nat" + case int_(i_0) => + "int" + case real_ => + "real" + case string_(i_0) => + "string" + case void => + "void" + case array_(i_0) => + "array" + case tuple(i_0) => + "tuple" + case map_(i_0, i_1) => + "map" + case set_(i_0) => + "set" + case optional(i_0) => + "optional" + case user(i_0) => + "user" + case fn(i_0, i_1) => + "fn" + case unknown => + "unknown" + } +} + +function isBigInt(ty: Ty): bool +{ + ((ty.int_? || ty.nat_?) && !((match ty.big { case Some(i_value) => !(i_value) case None => true }))) +} + +function tysEqual(as_: seq, bs: seq): bool +{ + if (|as_| != |bs|) then + false + else + ((|as_| == 0) || (tyEqual(as_[0], bs[0]) && tysEqual(as_[1..], bs[1..]))) +} + +lemma tysEqual_ensures(as_: seq, bs: seq) + ensures ((tysEqual(as_, bs) == true) ==> (|as_| == |bs|)) +{ +} + +function stringsEqual(as_: seq, bs: seq): bool +{ + if (|as_| != |bs|) then + false + else + ((|as_| == 0) || ((as_[0] == bs[0]) && stringsEqual(as_[1..], bs[1..]))) +} + +function tyEqual(a: Ty, b: Ty): bool +{ + if (Ty_kind(a) != Ty_kind(b)) then + false + else + match a { + case array_(i_a_elem) => + tyEqual(i_a_elem, b.elem) + case set_(i_a_elem) => + tyEqual(i_a_elem, b.elem) + case tuple(i_a_elems) => + tysEqual(i_a_elems, b.elems) + case map_(i_a_key, i_a_value) => + var bm := b; + (tyEqual(i_a_key, bm.key) && tyEqual(i_a_value, bm.value)) + case optional(i_a_inner) => + tyEqual(i_a_inner, b.inner) + case user(i_a_name) => + (i_a_name == b.name) + case fn(i_a_params, i_a_result) => + var bf := b; + (tysEqual(i_a_params, bf.params) && tyEqual(i_a_result, bf.result)) + case string_(i_a_values) => + var av := i_a_values; + var bv := b.values; + match av { + case Some(i_av_val) => + match bv { + case Some(i_bv_val) => + stringsEqual(i_av_val, i_bv_val) + case None => + false + } + case None => + match bv { + case Some(i_) => + false + case None => + true + } + } + case int_(i_a_big) => + (!((match i_a_big { case Some(i_value) => !(i_value) case None => true })) == !((match b.big { case Some(i_value) => !(i_value) case None => true }))) + case nat_(i_a_big) => + (!((match i_a_big { case Some(i_value) => !(i_value) case None => true })) == !((match b.big { case Some(i_value) => !(i_value) case None => true }))) + case bool_ => + true + case real_ => + true + case void => + true + case unknown => + true + } +} + +function tyEqualRefl(a: Ty): bool +{ + tyEqual(a, a) +} + +lemma tyEqualRefl_ensures(a: Ty) + ensures (tyEqualRefl(a) == true) +{ + match a { + case tuple(elems) => tysEqualRefl_ensures(elems); + case fn(params, result) => { tysEqualRefl_ensures(params); tyEqualRefl_ensures(result); } + case array_(elem) => tyEqualRefl_ensures(elem); + case set_(elem) => tyEqualRefl_ensures(elem); + case map_(key, value) => { tyEqualRefl_ensures(key); tyEqualRefl_ensures(value); } + case optional(inner) => tyEqualRefl_ensures(inner); + case string_(values) => + match values { case Some(vs) => stringsEqualRefl_ensures(vs); case None => {} } + case _ => {} + } +} + +function tysEqualRefl(ts: seq): bool +{ + tysEqual(ts, ts) +} + +lemma tysEqualRefl_ensures(ts: seq) + ensures (tysEqualRefl(ts) == true) +{ + if |ts| > 0 { + tyEqualRefl_ensures(ts[0]); + tysEqualRefl_ensures(ts[1..]); + } +} + +function stringsEqualRefl(ss: seq): bool +{ + stringsEqual(ss, ss) +} + +lemma stringsEqualRefl_ensures(ss: seq) + ensures (stringsEqualRefl(ss) == true) +{ + if |ss| > 0 { stringsEqualRefl_ensures(ss[1..]); } +} + +function tyEqualSym(a: Ty, b: Ty): bool +{ + (tyEqual(a, b) == tyEqual(b, a)) +} + +lemma tyEqualSym_ensures(a: Ty, b: Ty) + ensures (tyEqualSym(a, b) == true) +{ + match a { + case array_(ea) => match b { case array_(eb) => tyEqualSym_ensures(ea, eb); case _ => {} } + case set_(ea) => match b { case set_(eb) => tyEqualSym_ensures(ea, eb); case _ => {} } + case optional(ia) => match b { case optional(ib) => tyEqualSym_ensures(ia, ib); case _ => {} } + case map_(ka, va) => match b { case map_(kb, vb) => { tyEqualSym_ensures(ka, kb); tyEqualSym_ensures(va, vb); } case _ => {} } + case tuple(ea) => match b { case tuple(eb) => tysEqualSym_ensures(ea, eb); case _ => {} } + case fn(pa, ra) => match b { case fn(pb, rb) => { tysEqualSym_ensures(pa, pb); tyEqualSym_ensures(ra, rb); } case _ => {} } + case string_(va) => + match b { + case string_(vb) => + match (va, vb) { case (Some(xs), Some(ys)) => stringsEqualSym_ensures(xs, ys); case _ => {} } + case _ => {} + } + case _ => {} + } +} + +function tysEqualSym(as_: seq, bs: seq): bool +{ + (tysEqual(as_, bs) == tysEqual(bs, as_)) +} + +lemma tysEqualSym_ensures(as_: seq, bs: seq) + ensures (tysEqualSym(as_, bs) == true) +{ + if |as_| == |bs| && |as_| > 0 { + tyEqualSym_ensures(as_[0], bs[0]); + tysEqualSym_ensures(as_[1..], bs[1..]); + } +} + +function stringsEqualSym(as_: seq, bs: seq): bool +{ + (stringsEqual(as_, bs) == stringsEqual(bs, as_)) +} + +lemma stringsEqualSym_ensures(as_: seq, bs: seq) + ensures (stringsEqualSym(as_, bs) == true) +{ + if |as_| == |bs| && |as_| > 0 { stringsEqualSym_ensures(as_[1..], bs[1..]); } +} + +function tyEqualTrans(a: Ty, b: Ty, c: Ty): bool + requires (tyEqual(a, b) == true) + requires (tyEqual(b, c) == true) +{ + tyEqual(a, c) +} + +lemma tyEqualTrans_ensures(a: Ty, b: Ty, c: Ty) + requires (tyEqual(a, b) == true) + requires (tyEqual(b, c) == true) + ensures (tyEqualTrans(a, b, c) == true) +{ + match a { + case array_(ea) => match b { case array_(eb) => match c { case array_(ec) => tyEqualTrans_ensures(ea, eb, ec); case _ => {} } case _ => {} } + case set_(ea) => match b { case set_(eb) => match c { case set_(ec) => tyEqualTrans_ensures(ea, eb, ec); case _ => {} } case _ => {} } + case optional(ia) => match b { case optional(ib) => match c { case optional(ic) => tyEqualTrans_ensures(ia, ib, ic); case _ => {} } case _ => {} } + case map_(ka, va) => match b { case map_(kb, vb) => match c { case map_(kc, vc) => { tyEqualTrans_ensures(ka, kb, kc); tyEqualTrans_ensures(va, vb, vc); } case _ => {} } case _ => {} } + case tuple(ea) => match b { case tuple(eb) => match c { case tuple(ec) => tysEqualTrans_ensures(ea, eb, ec); case _ => {} } case _ => {} } + case fn(pa, ra) => match b { case fn(pb, rb) => match c { case fn(pc, rc) => { tysEqualTrans_ensures(pa, pb, pc); tyEqualTrans_ensures(ra, rb, rc); } case _ => {} } case _ => {} } + case string_(va) => + match b { + case string_(vb) => + match c { + case string_(vc) => + match (va, vb, vc) { case (Some(xs), Some(ys), Some(zs)) => stringsEqualTrans_ensures(xs, ys, zs); case _ => {} } + case _ => {} + } + case _ => {} + } + case _ => {} + } +} + +function tysEqualTrans(as_: seq, bs: seq, cs: seq): bool + requires (tysEqual(as_, bs) == true) + requires (tysEqual(bs, cs) == true) +{ + tysEqual(as_, cs) +} + +lemma tysEqualTrans_ensures(as_: seq, bs: seq, cs: seq) + requires (tysEqual(as_, bs) == true) + requires (tysEqual(bs, cs) == true) + ensures (tysEqualTrans(as_, bs, cs) == true) +{ + if |as_| > 0 { + tyEqualTrans_ensures(as_[0], bs[0], cs[0]); + tysEqualTrans_ensures(as_[1..], bs[1..], cs[1..]); + } +} + +function stringsEqualTrans(as_: seq, bs: seq, cs: seq): bool + requires (stringsEqual(as_, bs) == true) + requires (stringsEqual(bs, cs) == true) +{ + stringsEqual(as_, cs) +} + +lemma stringsEqualTrans_ensures(as_: seq, bs: seq, cs: seq) + requires (stringsEqual(as_, bs) == true) + requires (stringsEqual(bs, cs) == true) + ensures (stringsEqualTrans(as_, bs, cs) == true) +{ + if |as_| > 0 { stringsEqualTrans_ensures(as_[1..], bs[1..], cs[1..]); } +} + +function isTerminatorKind(kind: string): bool +{ + ((((kind == "return") || (kind == "throw")) || (kind == "break")) || (kind == "continue")) +} diff --git a/tools/src/typedir.dfy.gen b/tools/src/typedir.dfy.gen new file mode 100644 index 0000000..90ffac9 --- /dev/null +++ b/tools/src/typedir.dfy.gen @@ -0,0 +1,228 @@ +// Generated by lsc from typedir.ts + +datatype Option = None | Some(value: T) + +datatype Ty = bool_ | nat_(big: Option) | int_(big: Option) | real_ | string_(values: Option>) | void | array_(elem: Ty) | tuple(elems: seq) | map_(key: Ty, value: Ty) | set_(elem: Ty) | optional(inner: Ty) | user(name: string) | fn(params: seq, result: Ty) | unknown + +function Ty_kind(t: Ty): string +{ + match t { + case bool_ => + "bool" + case nat_(i_0) => + "nat" + case int_(i_0) => + "int" + case real_ => + "real" + case string_(i_0) => + "string" + case void => + "void" + case array_(i_0) => + "array" + case tuple(i_0) => + "tuple" + case map_(i_0, i_1) => + "map" + case set_(i_0) => + "set" + case optional(i_0) => + "optional" + case user(i_0) => + "user" + case fn(i_0, i_1) => + "fn" + case unknown => + "unknown" + } +} + +function isBigInt(ty: Ty): bool +{ + ((ty.int_? || ty.nat_?) && !((match ty.big { case Some(i_value) => !(i_value) case None => true }))) +} + +function tysEqual(as_: seq, bs: seq): bool +{ + if (|as_| != |bs|) then + false + else + ((|as_| == 0) || (tyEqual(as_[0], bs[0]) && tysEqual(as_[1..], bs[1..]))) +} + +lemma tysEqual_ensures(as_: seq, bs: seq) + ensures ((tysEqual(as_, bs) == true) ==> (|as_| == |bs|)) +{ +} + +function stringsEqual(as_: seq, bs: seq): bool +{ + if (|as_| != |bs|) then + false + else + ((|as_| == 0) || ((as_[0] == bs[0]) && stringsEqual(as_[1..], bs[1..]))) +} + +function tyEqual(a: Ty, b: Ty): bool +{ + if (Ty_kind(a) != Ty_kind(b)) then + false + else + match a { + case array_(i_a_elem) => + tyEqual(i_a_elem, b.elem) + case set_(i_a_elem) => + tyEqual(i_a_elem, b.elem) + case tuple(i_a_elems) => + tysEqual(i_a_elems, b.elems) + case map_(i_a_key, i_a_value) => + var bm := b; + (tyEqual(i_a_key, bm.key) && tyEqual(i_a_value, bm.value)) + case optional(i_a_inner) => + tyEqual(i_a_inner, b.inner) + case user(i_a_name) => + (i_a_name == b.name) + case fn(i_a_params, i_a_result) => + var bf := b; + (tysEqual(i_a_params, bf.params) && tyEqual(i_a_result, bf.result)) + case string_(i_a_values) => + var av := i_a_values; + var bv := b.values; + match av { + case Some(i_av_val) => + match bv { + case Some(i_bv_val) => + stringsEqual(i_av_val, i_bv_val) + case None => + false + } + case None => + match bv { + case Some(i_) => + false + case None => + true + } + } + case int_(i_a_big) => + (!((match i_a_big { case Some(i_value) => !(i_value) case None => true })) == !((match b.big { case Some(i_value) => !(i_value) case None => true }))) + case nat_(i_a_big) => + (!((match i_a_big { case Some(i_value) => !(i_value) case None => true })) == !((match b.big { case Some(i_value) => !(i_value) case None => true }))) + case bool_ => + true + case real_ => + true + case void => + true + case unknown => + true + } +} + +function tyEqualRefl(a: Ty): bool +{ + tyEqual(a, a) +} + +lemma tyEqualRefl_ensures(a: Ty) + ensures (tyEqualRefl(a) == true) +{ +} + +function tysEqualRefl(ts: seq): bool +{ + tysEqual(ts, ts) +} + +lemma tysEqualRefl_ensures(ts: seq) + ensures (tysEqualRefl(ts) == true) +{ +} + +function stringsEqualRefl(ss: seq): bool +{ + stringsEqual(ss, ss) +} + +lemma stringsEqualRefl_ensures(ss: seq) + ensures (stringsEqualRefl(ss) == true) +{ +} + +function tyEqualSym(a: Ty, b: Ty): bool +{ + (tyEqual(a, b) == tyEqual(b, a)) +} + +lemma tyEqualSym_ensures(a: Ty, b: Ty) + ensures (tyEqualSym(a, b) == true) +{ +} + +function tysEqualSym(as_: seq, bs: seq): bool +{ + (tysEqual(as_, bs) == tysEqual(bs, as_)) +} + +lemma tysEqualSym_ensures(as_: seq, bs: seq) + ensures (tysEqualSym(as_, bs) == true) +{ +} + +function stringsEqualSym(as_: seq, bs: seq): bool +{ + (stringsEqual(as_, bs) == stringsEqual(bs, as_)) +} + +lemma stringsEqualSym_ensures(as_: seq, bs: seq) + ensures (stringsEqualSym(as_, bs) == true) +{ +} + +function tyEqualTrans(a: Ty, b: Ty, c: Ty): bool + requires (tyEqual(a, b) == true) + requires (tyEqual(b, c) == true) +{ + tyEqual(a, c) +} + +lemma tyEqualTrans_ensures(a: Ty, b: Ty, c: Ty) + requires (tyEqual(a, b) == true) + requires (tyEqual(b, c) == true) + ensures (tyEqualTrans(a, b, c) == true) +{ +} + +function tysEqualTrans(as_: seq, bs: seq, cs: seq): bool + requires (tysEqual(as_, bs) == true) + requires (tysEqual(bs, cs) == true) +{ + tysEqual(as_, cs) +} + +lemma tysEqualTrans_ensures(as_: seq, bs: seq, cs: seq) + requires (tysEqual(as_, bs) == true) + requires (tysEqual(bs, cs) == true) + ensures (tysEqualTrans(as_, bs, cs) == true) +{ +} + +function stringsEqualTrans(as_: seq, bs: seq, cs: seq): bool + requires (stringsEqual(as_, bs) == true) + requires (stringsEqual(bs, cs) == true) +{ + stringsEqual(as_, cs) +} + +lemma stringsEqualTrans_ensures(as_: seq, bs: seq, cs: seq) + requires (stringsEqual(as_, bs) == true) + requires (stringsEqual(bs, cs) == true) + ensures (stringsEqualTrans(as_, bs, cs) == true) +{ +} + +function isTerminatorKind(kind: string): bool +{ + ((((kind == "return") || (kind == "throw")) || (kind == "break")) || (kind == "continue")) +} diff --git a/tools/src/typedir.ts b/tools/src/typedir.ts index b0d6be8..682bf43 100644 --- a/tools/src/typedir.ts +++ b/tools/src/typedir.ts @@ -5,6 +5,11 @@ * Still TS-shaped (not Lean-shaped). */ +// Self-application (DESIGN_LS_IN_LS.md §8): this module is inside the +// LemmaScript subset and is compiled by lsc itself (LemmaScript-files.txt). +// Dafny-only until the Lean emitter learns mutual blocks (§9 step 1). +//@ backend dafny + // Type-only: erased at runtime, so the builtins ↔ typedir reference cycle // never materializes. import type { BuiltinId } from "./builtins.js"; @@ -32,20 +37,36 @@ export type Ty = /** True for an int/nat that came from a TS `bigint` (integer division semantics). */ export function isBigInt(ty: Ty): boolean { + //@ verify return (ty.kind === "int" || ty.kind === "nat") && !!ty.big; } +/** Elementwise Ty-list equality — recursive rather than an indexed-callback + * HOF, so the module stays inside the LemmaScript subset. */ +export function tysEqual(as: Ty[], bs: Ty[]): boolean { + //@ verify + //@ ensures \result === true ==> as.length === bs.length + if (as.length !== bs.length) return false; + if (as.length === 0) return true; + return tyEqual(as[0], bs[0]) && tysEqual(as.slice(1), bs.slice(1)); +} + +function stringsEqual(as: string[], bs: string[]): boolean { + //@ verify + if (as.length !== bs.length) return false; + if (as.length === 0) return true; + return as[0] === bs[0] && stringsEqual(as.slice(1), bs.slice(1)); +} + /** Structural equality on Ty. Used to decide whether a tuple type is homogeneous * (all elements equal ⇒ lower to `seq`) vs heterogeneous (⇒ keep as `tuple`). */ export function tyEqual(a: Ty, b: Ty): boolean { + //@ verify if (a.kind !== b.kind) return false; switch (a.kind) { case "array": return tyEqual(a.elem, (b as typeof a).elem); case "set": return tyEqual(a.elem, (b as typeof a).elem); - case "tuple": { - const bt = b as typeof a; - return a.elems.length === bt.elems.length && a.elems.every((e, i) => tyEqual(e, bt.elems[i])); - } + case "tuple": return tysEqual(a.elems, (b as typeof a).elems); case "map": { const bm = b as typeof a; return tyEqual(a.key, bm.key) && tyEqual(a.value, bm.value); @@ -54,19 +75,91 @@ export function tyEqual(a: Ty, b: Ty): boolean { case "user": return a.name === (b as typeof a).name; case "fn": { const bf = b as typeof a; - return a.params.length === bf.params.length - && a.params.every((p, i) => tyEqual(p, bf.params[i])) - && tyEqual(a.result, bf.result); + return tysEqual(a.params, bf.params) && tyEqual(a.result, bf.result); } case "string": { - const bs = b as typeof a; - return JSON.stringify(a.values ?? null) === JSON.stringify(bs.values ?? null); + const av = a.values; + const bv = (b as typeof a).values; + if (av === undefined) return bv === undefined; + if (bv === undefined) return false; + return stringsEqual(av, bv); } case "int": case "nat": return !!a.big === !!(b as typeof a).big; case "bool": case "real": case "void": case "unknown": return true; // no payload } } +// ── P1 lemmas (DESIGN_LS_IN_LS.md §8.4): `tyEqual` is an equivalence ── +// relation. Stated as functions so what holds is readable from +// TypeScript; each `ensures` becomes a proof obligation whose inductive +// proof is hand-authored in the companion .dfy (quorum-style). + +/** Lemma: `tyEqual` is reflexive. */ +export function tyEqualRefl(a: Ty): boolean { + //@ verify + //@ ensures \result === true + return tyEqual(a, a); +} + +/** Lemma: `tysEqual` is reflexive. */ +export function tysEqualRefl(ts: Ty[]): boolean { + //@ verify + //@ ensures \result === true + return tysEqual(ts, ts); +} + +/** Lemma: `stringsEqual` is reflexive. */ +export function stringsEqualRefl(ss: string[]): boolean { + //@ verify + //@ ensures \result === true + return stringsEqual(ss, ss); +} + +/** Lemma: `tyEqual` is symmetric. */ +export function tyEqualSym(a: Ty, b: Ty): boolean { + //@ verify + //@ ensures \result === true + return tyEqual(a, b) === tyEqual(b, a); +} + +/** Lemma: `tysEqual` is symmetric. */ +export function tysEqualSym(as: Ty[], bs: Ty[]): boolean { + //@ verify + //@ ensures \result === true + return tysEqual(as, bs) === tysEqual(bs, as); +} + +/** Lemma: `stringsEqual` is symmetric. */ +export function stringsEqualSym(as: string[], bs: string[]): boolean { + //@ verify + //@ ensures \result === true + return stringsEqual(as, bs) === stringsEqual(bs, as); +} + +/** Lemma: `tyEqual` is transitive. */ +export function tyEqualTrans(a: Ty, b: Ty, c: Ty): boolean { + //@ verify + //@ requires tyEqual(a, b) === true && tyEqual(b, c) === true + //@ ensures \result === true + return tyEqual(a, c); +} + +/** Lemma: `tysEqual` is transitive. */ +export function tysEqualTrans(as: Ty[], bs: Ty[], cs: Ty[]): boolean { + //@ verify + //@ requires tysEqual(as, bs) === true && tysEqual(bs, cs) === true + //@ ensures \result === true + return tysEqual(as, cs); +} + +/** Lemma: `stringsEqual` is transitive. */ +export function stringsEqualTrans(as: string[], bs: string[], cs: string[]): boolean { + //@ verify + //@ requires stringsEqual(as, bs) === true && stringsEqual(bs, cs) === true + //@ ensures \result === true + return stringsEqual(as, cs); +} + export type CallKind = "pure" | "method" | "spec-pure" | "unknown" /** Typed counterpart of RawChainStep. Carries the result-type at this step. @@ -81,6 +174,15 @@ export type TChainStep = // ── Expressions ────────────────────────────────────────────── +// Named payload records. Naming these (rather than inlining object-literal +// types) keeps the module inside the LemmaScript subset: inline anonymous +// record types have no backend model. Purely structural — construction +// sites are unaffected. +export interface TRecordField { name: string; value: TExpr } +export interface TExprCase { variant: string; body: TExpr } +export interface TStmtCase { variant: string; body: TStmt[] } +export interface TSwitchCase { label: string; body: TStmt[] } + export type TExpr = | { kind: "var"; name: string; ty: Ty } | { kind: "num"; value: number; ty: Ty } @@ -95,16 +197,16 @@ export type TExpr = | { kind: "field"; obj: TExpr; field: string; ty: Ty; isDiscriminant?: boolean; // true if this is a discriminant field access ofVariant?: string } // which union variant the field is read from, when narrowing determined it - | { kind: "record"; spread: TExpr | null; fields: { name: string; value: TExpr }[]; ty: Ty } + | { kind: "record"; spread: TExpr | null; fields: TRecordField[]; ty: Ty } | { kind: "arrayLiteral"; elems: TExpr[]; ty: Ty } - | { kind: "lambda"; params: { name: string; ty: Ty }[]; body: TStmt[]; ty: Ty } + | { kind: "lambda"; params: TParam[]; body: TStmt[]; ty: Ty } | { kind: "conditional"; cond: TExpr; then: TExpr; else: TExpr; ty: Ty } | { kind: "optChain"; obj: TExpr; chain: TChainStep[]; ty: Ty } | { kind: "nullish"; left: TExpr; right: TExpr; ty: Ty } | { kind: "someMatch"; scrutinee: TExpr; binder: string; binderTy: Ty; someBody: TExpr; noneBody: TExpr; ty: Ty } | { kind: "tagMatch"; scrutinee: TExpr; typeName: string; - cases: { variant: string; body: TExpr }[]; + cases: TExprCase[]; fallthrough: TExpr | null; ty: Ty } // Spec-only (from //@ annotations): // Note: \result is desugared by resolve.ts into a regular var named "\\result"; @@ -130,7 +232,7 @@ export type TStmt = doneWith: TExpr | null; body: TStmt[] } | { kind: "switch"; expr: TExpr; discriminant: string; - cases: { label: string; body: TStmt[] }[]; + cases: TSwitchCase[]; defaultBody: TStmt[] } | { kind: "forof"; names: string[]; nameTypes: Ty[]; iterable: TExpr; invariants: TExpr[]; doneWith: TExpr | null; body: TStmt[] } @@ -141,13 +243,14 @@ export type TStmt = | { kind: "someMatch"; scrutinee: TExpr; binder: string; binderTy: Ty; someBody: TStmt[]; noneBody: TStmt[] } | { kind: "tagMatch"; scrutinee: TExpr; typeName: string; - cases: { variant: string; body: TStmt[] }[]; + cases: TStmtCase[]; fallthrough: TStmt[] } /** Statement kinds that unconditionally leave the enclosing block. Shared by * resolve (block-tail narrowing) and narrow (isTerminating); works on raw and * typed IR alike since both use these kind strings. */ export function isTerminatorKind(kind: string): boolean { + //@ verify return kind === "return" || kind === "throw" || kind === "break" || kind === "continue"; } @@ -174,7 +277,7 @@ export interface TFunction { export interface TClass { name: string; - fields: { name: string; ty: Ty }[]; + fields: TParam[]; methods: TFunction[]; }