diff --git a/examples/forContinue.def.lean b/examples/forContinue.def.lean index 938aadd..29b221d 100644 --- a/examples/forContinue.def.lean +++ b/examples/forContinue.def.lean @@ -65,6 +65,34 @@ method countPositivesNonNested (grid : Array (Array Int)) return (res : Int) i := i + 1 return total +method lastPresent (xs : Array (Option Int)) return (res : Option Int) + do + let mut cur : Option Int := none + for _x_idx in [:xs.size] + invariant _x_idx ≤ xs.size + do + let x := xs[_x_idx]! + if h_x : (x).isSome = true then + let _x_val := (x).get h_x + cur := some _x_val + else + pure () + return cur + +method lastBeforeGap (xs : Array (Option Int)) return (res : Option Int) + do + let mut cur : Option Int := none + for _x_idx in [:xs.size] + invariant _x_idx ≤ xs.size + do + let x := xs[_x_idx]! + if h_x : (x).isSome = true then + let _x_val := (x).get h_x + cur := some _x_val + else + break + return cur + method countKeep (items : Array Item) return (res : Int) ensures 0 ≤ res ensures res ≤ items.size diff --git a/examples/forContinue.dfy b/examples/forContinue.dfy index c4c513b..daa0572 100644 --- a/examples/forContinue.dfy +++ b/examples/forContinue.dfy @@ -1,5 +1,7 @@ // Generated by lsc from forContinue.ts +datatype Option = None | Some(value: T) + function JSRem(a: int, b: int): int requires b != 0 { @@ -81,6 +83,44 @@ method countPositivesNonNested(grid: seq>) returns (res: int) return total; } +method lastPresent(xs: seq>) returns (res: Option) +{ + var cur := None; + var i_x_idx := 0; + while i_x_idx < |xs| + invariant (i_x_idx <= |xs|) + { + var x := xs[i_x_idx]; + match x { + case Some(i_x_val) => + cur := Some(i_x_val); + case None => + + } + i_x_idx := i_x_idx + 1; + } + return cur; +} + +method lastBeforeGap(xs: seq>) returns (res: Option) +{ + var cur := None; + var i_x_idx := 0; + while i_x_idx < |xs| + invariant (i_x_idx <= |xs|) + { + var x := xs[i_x_idx]; + match x { + case Some(i_x_val) => + cur := Some(i_x_val); + case None => + break; + } + i_x_idx := i_x_idx + 1; + } + return cur; +} + method countKeep(items: seq) returns (res: int) ensures (0 <= res) ensures (res <= |items|) diff --git a/examples/forContinue.dfy.gen b/examples/forContinue.dfy.gen index c4c513b..daa0572 100644 --- a/examples/forContinue.dfy.gen +++ b/examples/forContinue.dfy.gen @@ -1,5 +1,7 @@ // Generated by lsc from forContinue.ts +datatype Option = None | Some(value: T) + function JSRem(a: int, b: int): int requires b != 0 { @@ -81,6 +83,44 @@ method countPositivesNonNested(grid: seq>) returns (res: int) return total; } +method lastPresent(xs: seq>) returns (res: Option) +{ + var cur := None; + var i_x_idx := 0; + while i_x_idx < |xs| + invariant (i_x_idx <= |xs|) + { + var x := xs[i_x_idx]; + match x { + case Some(i_x_val) => + cur := Some(i_x_val); + case None => + + } + i_x_idx := i_x_idx + 1; + } + return cur; +} + +method lastBeforeGap(xs: seq>) returns (res: Option) +{ + var cur := None; + var i_x_idx := 0; + while i_x_idx < |xs| + invariant (i_x_idx <= |xs|) + { + var x := xs[i_x_idx]; + match x { + case Some(i_x_val) => + cur := Some(i_x_val); + case None => + break; + } + i_x_idx := i_x_idx + 1; + } + return cur; +} + method countKeep(items: seq) returns (res: int) ensures (0 <= res) ensures (res <= |items|) diff --git a/examples/forContinue.ts b/examples/forContinue.ts index 0e5d15e..3bf6474 100644 --- a/examples/forContinue.ts +++ b/examples/forContinue.ts @@ -70,6 +70,33 @@ export function countPositivesNonNested(grid: number[][]): number { return total; } +// Early `continue` on an optional guard must narrow `x` for the rest of the +// body — resolve's block-tail narrowing fires on any terminator (return/throw/ +// break/continue), the same set as narrow's isTerminating. The assignment into +// the optional `cur` then needs a Some-wrap of the narrowed `x`; regression was +// `cur := i_x_val` (ill-typed Dafny) because only `return` triggered narrowing. +export function lastPresent(xs: (number | undefined)[]): number | undefined { + //@ verify + let cur: number | undefined = undefined; + for (const x of xs) { + if (x === undefined) continue; + cur = x; + } + return cur; +} + +// Same shape with `break`: stop at the first absent entry, keeping the last +// present value seen. Pins the terminator set beyond `continue`. +export function lastBeforeGap(xs: (number | undefined)[]): number | undefined { + //@ verify + let cur: number | undefined = undefined; + for (const x of xs) { + if (x === undefined) break; + cur = x; + } + return cur; +} + // A `continue` in a non-last `switch` (→ `match`) arm: the inverter can't fold // it (not a trailing-guard `if`, and the match isn't the loop body's last stmt), // so it survives to native Dafny `continue` emission — the shape flue's narrowed diff --git a/tools/src/narrow.ts b/tools/src/narrow.ts index 7988f5a..9eee7cc 100644 --- a/tools/src/narrow.ts +++ b/tools/src/narrow.ts @@ -36,6 +36,7 @@ */ import type { TModule, TFunction, TStmt, TExpr, Ty } from "./typedir.js"; +import { isTerminatorKind } from "./typedir.js"; import type { TypeDeclInfo } from "./types.js"; import { freshName } from "./names.js"; @@ -875,8 +876,7 @@ function parseNegativeDiscriminantCond(cond: TExpr): { scrutinee: TExpr & { kind function isTerminating(stmts: TStmt[]): boolean { if (stmts.length === 0) return false; - const last = stmts[stmts.length - 1]; - return last.kind === "return" || last.kind === "throw" || last.kind === "break" || last.kind === "continue"; + return isTerminatorKind(stmts[stmts.length - 1].kind); } /** Rule (list-level): consecutive `if (x.kind === "v") ...` chain → tagMatch. diff --git a/tools/src/resolve.ts b/tools/src/resolve.ts index 08ad755..0ac459e 100644 --- a/tools/src/resolve.ts +++ b/tools/src/resolve.ts @@ -7,7 +7,7 @@ import type { RawExpr, RawStmt, RawFunction, RawModule } from "./rawir.js"; import type { Ty, TExpr, TStmt, TFunction, TModule, TParam, CallKind } from "./typedir.js"; -import { isBigInt, tyEqual } from "./typedir.js"; +import { isBigInt, tyEqual, isTerminatorKind } from "./typedir.js"; import { parseTsType, tyToCanonical } from "./types.js"; import type { TypeDeclInfo } from "./types.js"; import { parseExpr } from "./specparser.js"; @@ -1260,9 +1260,12 @@ function resolveBlock(stmts: RawStmt[], ctx: Ctx): TStmt[] { env = nextEnv; // Flow narrowing: if (x === undefined) { return } narrows x for rest of block. // Also handles compound: if (x === undefined || y === undefined) { return } + // Any terminator counts (return/throw/break/continue — same set as narrow's + // isTerminating): each exits the current block, so the rest of the block + // only runs when the guard was false. // Field chains are excluded — resolve can't substitute in statement lists; // transform's emitOptionalMatch handles field chains in statement contexts. - if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind === "return" && s.else.length === 0) { + if (s.kind === "if" && s.then.length > 0 && isTerminatorKind(s.then[s.then.length - 1].kind) && s.else.length === 0) { const narrowings = collectEarlyReturnNarrowings(s.cond, withEnv(ctx, env)); for (const n of narrowings) { env = extend(env, n.varName, n.innerTy); diff --git a/tools/src/typedir.ts b/tools/src/typedir.ts index 712d861..b92dd88 100644 --- a/tools/src/typedir.ts +++ b/tools/src/typedir.ts @@ -133,6 +133,13 @@ export type TStmt = cases: { variant: string; body: TStmt[] }[]; 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 { + return kind === "return" || kind === "throw" || kind === "break" || kind === "continue"; +} + // ── Top-level ──────────────────────────────────────────────── export interface TParam {