lemmascript 0.5.17 → 0.5.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,19 +3,14 @@
3
3
  *
4
4
  * Pipeline: resolve → narrow → transform → emit.
5
5
  *
6
- * Syntax-directed pattern matching on typed IR. Detects optional-narrowing
7
- * patterns and rewrites each into a `someMatch` IR node carrying the
8
- * scrutinee, binder, unwrapped type, and arms:
9
- * - `if (e !== undefined) S` (statement)
10
- * - `if (e === undefined) terminate; rest` (early-return + rest consumption)
11
- * - `if (e !== undefined && rest) S` (&& in if; no else)
12
- * - `if (a === undefined || b === undefined) terminate; rest` (|| chain)
13
- * - `let x = (e_opt && rest) ? a : b` (statement, impure-OK guard)
14
- * - `e !== undefined ? a : b` (ternary)
15
- * - `e !== undefined && rest ? a : b` (&& in ternary; pure rest)
16
- * - `opt ? a : b` (truthiness)
17
- * - `path !== undefined [&& rest] ==> B` (spec implication narrowing)
18
- * - `optChain(obj, field)` (`obj?.field` from extract)
6
+ * Positional drivers over condition-facts (DESIGN_LS_IN_LS.md §4): the
7
+ * *semantics* of conditions what a check establishes, `&&`/`||`-chain
8
+ * analysis, binder minting, discriminant detection live in
9
+ * `condition-facts.ts`; this pass owns *where* a condition sits (if
10
+ * statement, early return + rest consumption, ternary, implication, guard
11
+ * statement, conditional initializer, nullish/optional chains, discriminant
12
+ * chains) and rewrites each position into `someMatch` / `tagMatch` IR via
13
+ * the shared materializers.
19
14
  *
20
15
  * Following TS semantics, narrowing rules only fire for pure access paths
21
16
  * (`var(x)` or `field(purePath, name)`). Complex scrutinees (call results,
@@ -32,82 +27,27 @@
32
27
  * Walker shape: bottom-up over TExpr/TStmt. At each node, recurse children
33
28
  * via the *Recurse* helpers, then try the rules in order. List-level rules
34
29
  * (early-return, let-cond) run in `walkStmts` so they can consume the rest
35
- * of the block.
30
+ * of the block. Rule order matters — see the comments at the two dispatch
31
+ * chains.
32
+ *
33
+ * State is explicit (§6.1): a `CondCtx` (type declarations + optChain
34
+ * binder counter) threads through the walk; no module-level state.
36
35
  */
36
+ import { isTerminatorKind } from "./typedir.js";
37
37
  import { freshName } from "./names.js";
38
- // ── Optional-check detection ────────────────────────────────
39
- /** Counter for naming optChain binders. Reset per module. */
40
- let _ocCounter = 0;
41
- /** Type declarations for this module. Set in narrowModule, used by the
42
- * discriminant-narrowing rules to resolve `'key' in x` to a variant. */
43
- let _typeDecls = [];
44
- /** Detect optional checks: `e !== undefined`, `e === undefined`, or `!e` for a
45
- * pure-access-path optional-typed e. `!e` is equivalent to `=== undefined`.
46
- * Following TS, only pure access paths narrow; complex scrutinees return null. */
47
- function parseOptionalCheck(cond) {
48
- // `!e` where e is optional — a truthiness form: false iff e is absent OR its
49
- // inner value is itself falsy (so `Some(0)`/`Some("")` count as falsy too).
50
- if (cond.kind === "unop" && cond.op === "!" && cond.expr.ty.kind === "optional") {
51
- const e = cond.expr;
52
- const innerTy = cond.expr.ty.inner;
53
- const hint = binderHintFor(e);
54
- if (hint === null)
55
- return null;
56
- return { scrutinee: e, innerTy, negated: true, binderHint: freshName(hint), truthiness: true };
57
- }
58
- if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
59
- // Bare optional truthiness: `if (e)` where e: T | undefined — true iff e is
60
- // present AND its inner value is truthy.
61
- if (cond.ty.kind === "optional") {
62
- const hint = binderHintFor(cond);
63
- if (hint === null)
64
- return null;
65
- return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: freshName(hint), truthiness: true };
66
- }
67
- return null;
68
- }
69
- // Explicit `e === undefined` / `e !== undefined` — a pure presence check,
70
- // independent of the inner value (so NOT a truthiness form).
71
- let e = null;
72
- if (cond.right.kind === "var" && cond.right.name === "undefined")
73
- e = cond.left;
74
- if (cond.left.kind === "var" && cond.left.name === "undefined")
75
- e = cond.right;
76
- if (!e || e.ty.kind !== "optional")
77
- return null;
78
- const hint = binderHintFor(e);
79
- if (hint === null)
80
- return null;
81
- return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: freshName(hint), truthiness: false };
82
- }
83
- function binderHintFor(e) {
84
- // Pure access paths: var(x) or field(purePath, name).
85
- // Walks down to the var root, collecting field names. Returns
86
- // `_root_field1_field2_..._val` (or `_root_val` for a bare var).
87
- const fields = [];
88
- let cur = e;
89
- while (cur.kind === "field") {
90
- fields.unshift(cur.field);
91
- cur = cur.obj;
92
- }
93
- if (cur.kind !== "var")
94
- return null;
95
- // \result is stored as the IR var name "\\result"; sanitize for a valid identifier.
96
- const root = cur.name === "\\result" ? "result" : cur.name;
97
- return fields.length === 0 ? `_${root}_val` : `_${root}_${fields.join("_")}_val`;
98
- }
99
- // Aliased for code that historically called the simpler check.
100
- const parseSimpleOptionalCheck = parseOptionalCheck;
38
+ import { builtinSpec } from "./builtins.js";
39
+ import { presentFact, leadingPresent, leadingIsArray, flattenOr, noneDetector, binderHintFor, binderHintForMapAccess, freshOcBinder, applyChain, restoreDiscriminantFlag, arrayBoundsCond, exprEqual, isArrayFact, typeofStringFact, variantFact, negVariantFact, presentMatchStmts, presentMatchExpr, } from "./condition-facts.js";
101
40
  // ── Walkers ──────────────────────────────────────────────────
102
- function walkExpr(e) {
103
- const r = recurseExpr(e);
104
- return ruleNullish(r) ?? ruleNullishIndex(r) ?? ruleOptChainIndex(r) ?? ruleOptChain(r) ?? ruleImplOptional(r) ?? ruleImplArrayIsArray(r) ?? ruleConditionalArrayIsArray(r) ?? ruleConditionalAndArrayIsArray(r) ?? ruleConditionalAndOptional(r) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r) ?? ruleConditionalOptionalTruthy(r) ?? r;
41
+ function walkExpr(e, ctx) {
42
+ const r = recurseExpr(e, ctx);
43
+ 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;
105
44
  }
106
- function recurseExpr(e) {
107
- const re = walkExpr;
45
+ function recurseExpr(e, ctx) {
46
+ const re = (x) => walkExpr(x, ctx);
108
47
  switch (e.kind) {
109
48
  case "var":
110
49
  case "num":
50
+ case "bigint":
111
51
  case "str":
112
52
  case "bool":
113
53
  case "havoc":
@@ -120,7 +60,7 @@ function recurseExpr(e) {
120
60
  case "record": return { ...e, spread: e.spread ? re(e.spread) : null,
121
61
  fields: e.fields.map(f => ({ ...f, value: re(f.value) })) };
122
62
  case "arrayLiteral": return { ...e, elems: e.elems.map(re) };
123
- case "lambda": return { ...e, body: walkStmts(e.body) };
63
+ case "lambda": return { ...e, body: walkStmts(e.body, ctx) };
124
64
  case "conditional": return { ...e, cond: re(e.cond), then: re(e.then), else: re(e.else) };
125
65
  case "optChain": return { ...e, obj: re(e.obj),
126
66
  chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(re) }
@@ -135,9 +75,9 @@ function recurseExpr(e) {
135
75
  fallthrough: e.fallthrough ? re(e.fallthrough) : null };
136
76
  }
137
77
  }
138
- function walkStmt(s) {
78
+ function walkStmt(s, ctx) {
139
79
  // Recurse into children first, then try rules at this node.
140
- const r = recurseStmt(s);
80
+ const r = recurseStmt(s, ctx);
141
81
  // Optional narrowing fires before Array.isArray narrowing: in a chain like
142
82
  // `next && Array.isArray(next.content)` the optional check must unwrap `next`
143
83
  // *outside* the array match, since `next.content` is unreachable until then.
@@ -145,41 +85,41 @@ function walkStmt(s) {
145
85
  // array rule fires; independent narrows commute, so the order is harmless.)
146
86
  // && rules fire before the simple rule because they produce nested ifs whose
147
87
  // inner shape doesn't match the simple rule directly.
148
- return ruleIfAndOptional(r) ?? ruleIfAndArrayIsArray(r) ?? ruleIfOptionalSimple(r) ?? ruleExprStmtAndOptional(r) ?? ruleOptionalIndexBinding(r) ?? r;
88
+ return ruleIfAndOptional(r, ctx) ?? ruleIfAndArrayIsArray(r, ctx) ?? ruleIfOptionalSimple(r) ?? ruleExprStmtAndOptional(r, ctx) ?? ruleOptionalIndexBinding(r) ?? r;
149
89
  }
150
- function walkStmts(stmts) {
90
+ function walkStmts(stmts, ctx) {
151
91
  const result = [];
152
92
  for (let i = 0; i < stmts.length; i++) {
153
93
  const s = stmts[i];
154
94
  const rest = stmts.slice(i + 1);
155
95
  // Discriminant rules consume a prefix of stmts; remaining is processed normally.
156
- const tagged = ruleDiscriminantChain(stmts.slice(i)) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i));
96
+ const tagged = ruleDiscriminantChain(stmts.slice(i), ctx) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i), ctx);
157
97
  if (tagged) {
158
- result.push(walkStmt(tagged.stmt));
98
+ result.push(walkStmt(tagged.stmt, ctx));
159
99
  i += tagged.consumed - 1;
160
100
  continue;
161
101
  }
162
- const consumed = ruleEarlyReturnOrChain(s, rest) ?? ruleEarlyReturnConsume(s, rest) ?? ruleEarlyReturnOptChainCompare(s, rest);
102
+ const consumed = ruleEarlyReturnOrChain(s, rest, ctx) ?? ruleEarlyReturnConsume(s, rest) ?? ruleEarlyReturnOptChainCompare(s, rest, ctx);
163
103
  if (consumed) {
164
- result.push(walkStmt(consumed));
104
+ result.push(walkStmt(consumed, ctx));
165
105
  return result;
166
106
  }
167
107
  // walkStmt first — narrow's expression rules may rewrite the let init from
168
108
  // `conditional` to `someMatch`, in which case the let-cond desugar shouldn't fire.
169
- const walked = walkStmt(s);
109
+ const walked = walkStmt(s, ctx);
170
110
  const expanded = ruleLetCondAndOptional(walked);
171
111
  if (expanded) {
172
112
  for (const x of expanded)
173
- result.push(walkStmt(x));
113
+ result.push(walkStmt(x, ctx));
174
114
  continue;
175
115
  }
176
116
  result.push(walked);
177
117
  }
178
118
  return result;
179
119
  }
180
- function recurseStmt(s) {
181
- const re = walkExpr;
182
- const rs = walkStmts;
120
+ function recurseStmt(s, ctx) {
121
+ const re = (x) => walkExpr(x, ctx);
122
+ const rs = (x) => walkStmts(x, ctx);
183
123
  switch (s.kind) {
184
124
  case "let": return { ...s, init: re(s.init) };
185
125
  case "assign": return { ...s, value: re(s.value) };
@@ -210,40 +150,36 @@ function recurseStmt(s) {
210
150
  fallthrough: rs(s.fallthrough) };
211
151
  }
212
152
  }
213
- const canBeFalsy = (c) => c.truthiness && ["int", "nat", "string", "bool"].includes(c.innerTy.kind);
214
- const bound = (c) => ({ kind: "var", name: c.binderHint, ty: c.innerTy });
215
- // ── Rules ───────────────────────────────────────────────────
216
- /** Rule: `if (e !== undefined) then else` where e is a simple optional var or
217
- * `obj.field` chain, and the Some branch is non-empty.
218
- * → `someMatch e { Some(_e_val) => then, None => else }`. */
153
+ function isTerminating(stmts) {
154
+ if (stmts.length === 0)
155
+ return false;
156
+ return isTerminatorKind(stmts[stmts.length - 1].kind);
157
+ }
158
+ // ── Presence drivers ────────────────────────────────────────
159
+ /** Driver: `if (e !== undefined) then else` — presence fact in if position.
160
+ * → `someMatch e { Some(_e_val) => then, None => else }`. Requires a
161
+ * non-empty Some branch. */
219
162
  function ruleIfOptionalSimple(s) {
220
163
  if (s.kind !== "if")
221
164
  return null;
222
- const check = parseSimpleOptionalCheck(s.cond);
165
+ const check = presentFact(s.cond);
223
166
  if (!check)
224
167
  return null;
225
168
  const someBody = check.negated ? s.else : s.then;
226
169
  const noneBody = check.negated ? s.then : s.else;
227
170
  if (someBody.length === 0)
228
171
  return null;
229
- return {
230
- kind: "someMatch",
231
- scrutinee: check.scrutinee, binderTy: check.innerTy,
232
- binder: check.binderHint,
233
- someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: someBody, else: noneBody }] : someBody,
234
- noneBody,
235
- };
172
+ return presentMatchStmts(check, someBody, noneBody);
236
173
  }
237
- /** Rule: `if (e === undefined) terminate; rest` (early return / throw / break).
238
- * `someMatch e { Some(_e_val) => rest, None => terminate }`.
239
- * Fires when the Some branch is empty AND there's a non-empty rest of the
240
- * block — pulling the continuation into the narrowed scope. */
174
+ /** Driver: `if (e === undefined) terminate; rest` presence fact in
175
+ * early-return position, consuming the rest of the block into the
176
+ * narrowed scope. Fires when the Some branch is empty. */
241
177
  function ruleEarlyReturnConsume(s, rest) {
242
178
  if (s.kind !== "if")
243
179
  return null;
244
180
  if (rest.length === 0)
245
181
  return null;
246
- const check = parseSimpleOptionalCheck(s.cond);
182
+ const check = presentFact(s.cond);
247
183
  if (!check)
248
184
  return null;
249
185
  const someBranch = check.negated ? s.else : s.then;
@@ -252,58 +188,15 @@ function ruleEarlyReturnConsume(s, rest) {
252
188
  return null;
253
189
  if (!isTerminating(noneBranch))
254
190
  return null;
255
- return {
256
- kind: "someMatch",
257
- scrutinee: check.scrutinee, binderTy: check.innerTy,
258
- binder: check.binderHint,
259
- someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: rest, else: noneBranch }] : rest,
260
- noneBody: noneBranch,
261
- };
191
+ return presentMatchStmts(check, rest, noneBranch);
262
192
  }
263
- /** Flatten a nested `||` chain into its leaf conditions. */
264
- function flattenOr(e) {
265
- if (e.kind === "binop" && e.op === "||")
266
- return [...flattenOr(e.left), ...flattenOr(e.right)];
267
- return [e];
268
- }
269
- function classifyDisjunct(leaf) {
270
- // `x?.chain !== lit` — `undefined !== lit` is true when x is None.
271
- if (leaf.kind === "binop" && leaf.op === "!==") {
272
- const oc = leaf.left.kind === "optChain" ? leaf.left : leaf.right.kind === "optChain" ? leaf.right : null;
273
- if (oc && oc.kind === "optChain" && oc.obj.ty.kind === "optional") {
274
- const hint = binderHintFor(oc.obj);
275
- if (hint === null)
276
- return null;
277
- const binder = freshName(hint);
278
- const unwrapped = applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain);
279
- if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
280
- const base = unwrapped.obj.ty.name.replace(/<.*/, "");
281
- const decl = _typeDecls.find(d => d.name === base);
282
- if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field)
283
- unwrapped.isDiscriminant = true;
284
- }
285
- const lit = leaf.left === oc ? leaf.right : leaf.left;
286
- return { scrutinee: oc.obj, innerTy: oc.obj.ty.inner, binder, residual: { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } } };
287
- }
288
- }
289
- // `!x` / `x === undefined`.
290
- const chk = parseOptionalCheck(leaf);
291
- if (chk && chk.negated) {
292
- const residual = canBeFalsy(chk)
293
- ? { kind: "unop", op: "!", expr: { kind: "var", name: chk.binderHint, ty: chk.innerTy }, ty: { kind: "bool" } }
294
- : null;
295
- return { scrutinee: chk.scrutinee, innerTy: chk.innerTy, binder: chk.binderHint, residual };
296
- }
297
- return null;
298
- }
299
- /** Rule: `if (D1 || … || Dn) terminate; rest`. Each `Di` that detects some optional
300
- * `x` is None (`!x`, `x === undefined`, `x?.chain !== lit`) narrows that `x` to Some
301
- * across `rest`; the rest — value guards reading a narrowed `x` directly, plus the
302
- * detectors' Some-case residuals — become a trailing early-return. Sound: reaching
303
- * `rest` means every disjunct was false, so every detected optional is present.
304
- * Covers `if (!x || x.f !== v) continue` / `if (x?.t !== 'm' || x.g) break`.
305
- * Closes the resolve.ts:602 TODO ("|| narrowing"). */
306
- function ruleEarlyReturnOrChain(s, rest) {
193
+ /** Driver: `if (D1 || || Dn) terminate; rest` — De-Morgan position. Each `Di`
194
+ * that detects some optional `x` is None (`!x`, `x === undefined`,
195
+ * `x?.chain !== lit`) narrows that `x` to Some across `rest`; the rest —
196
+ * value guards reading a narrowed `x` directly, plus the detectors' Some-case
197
+ * residuals — become a trailing early-return. Sound: reaching `rest` means
198
+ * every disjunct was false, so every detected optional is present. */
199
+ function ruleEarlyReturnOrChain(s, rest, ctx) {
307
200
  if (s.kind !== "if")
308
201
  return null;
309
202
  if (rest.length === 0)
@@ -319,7 +212,7 @@ function ruleEarlyReturnOrChain(s, rest) {
319
212
  const residualLeaves = [];
320
213
  const seen = new Set();
321
214
  for (const leaf of leaves) {
322
- const d = classifyDisjunct(leaf);
215
+ const d = noneDetector(leaf, ctx);
323
216
  if (!d) {
324
217
  residualLeaves.push(leaf);
325
218
  continue;
@@ -343,17 +236,15 @@ function ruleEarlyReturnOrChain(s, rest) {
343
236
  }
344
237
  return inner[0];
345
238
  }
346
- /** Rule: `if (opt?.chain !== lit) terminate; rest` where `opt` is optional.
347
- * `opt?.chain` is `undefined` when `opt` is None, and `undefined !== lit` is
348
- * true, so the None case takes the terminating branch — falling through to
349
- * `rest` proves `opt` is Some. Rewrite to
239
+ /** Driver: `if (opt?.chain !== lit) terminate; rest` bound-optional
240
+ * early-return. `opt?.chain` is `undefined` when `opt` is None, and
241
+ * `undefined !== lit` is true, so the None case takes the terminating branch —
242
+ * falling through to `rest` proves `opt` is Some. Rewrite to
350
243
  * someMatch opt { Some(v) => [if (v.chain !== lit) terminate; rest]; None => terminate }
351
- * narrowing `opt` to `v` across `rest` (transform substitutes the scrutinee) and
352
- * handing the now-non-optional inner guard to the ordinary rules (e.g.
353
- * discriminant narrowing). Bound-optional companion to ruleEarlyReturnConsume,
354
- * which handles only a bare presence check (`opt !== undefined`). Restricted to
244
+ * narrowing `opt` to `v` across `rest` and handing the now-non-optional inner
245
+ * guard to the ordinary rules (e.g. discriminant narrowing). Restricted to
355
246
  * `!==` so the None case is guaranteed to terminate. */
356
- function ruleEarlyReturnOptChainCompare(s, rest) {
247
+ function ruleEarlyReturnOptChainCompare(s, rest, ctx) {
357
248
  if (s.kind !== "if")
358
249
  return null;
359
250
  if (rest.length === 0)
@@ -374,16 +265,7 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
374
265
  const binder = freshName(hint);
375
266
  const binderVar = { kind: "var", name: binder, ty: innerTy };
376
267
  const unwrapped = applyChain(binderVar, oc.chain);
377
- // applyChain rebuilds the field without the `isDiscriminant` flag resolve sets
378
- // on a direct `x.disc`; restore it when the unwrapped access is the binder
379
- // union's discriminant, so the inner guard feeds discriminant narrowing.
380
- if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
381
- const base = unwrapped.obj.ty.name.replace(/<.*/, "");
382
- const decl = _typeDecls.find(d => d.name === base);
383
- if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field) {
384
- unwrapped.isDiscriminant = true;
385
- }
386
- }
268
+ restoreDiscriminantFlag(unwrapped, ctx.decls);
387
269
  const innerGuard = { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } };
388
270
  // Keep `rest` as trailing statements (not an else branch) — `s.then` terminates,
389
271
  // so `if (g) terminate; rest` ≡ `if (g) terminate else rest`, and the trailing
@@ -392,99 +274,37 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
392
274
  const someBody = [{ kind: "if", cond: innerGuard, then: s.then, else: [] }, ...rest];
393
275
  return { kind: "someMatch", scrutinee: oc.obj, binder, binderTy: innerTy, someBody, noneBody: s.then };
394
276
  }
395
- /** Rule (expression): `e !== undefined ? a : b`. */
277
+ /** Driver (expression): `e !== undefined ? a : b` — presence fact in
278
+ * ternary position. */
396
279
  function ruleConditionalOptionalSimple(e) {
397
280
  if (e.kind !== "conditional")
398
281
  return null;
399
- const check = parseOptionalCheck(e.cond);
282
+ const check = presentFact(e.cond);
400
283
  if (!check)
401
284
  return null;
402
285
  const someBody = check.negated ? e.else : e.then;
403
286
  const noneBody = check.negated ? e.then : e.else;
404
- return {
405
- kind: "someMatch",
406
- scrutinee: check.scrutinee, binderTy: check.innerTy,
407
- binder: check.binderHint,
408
- someBody: canBeFalsy(check) ? { kind: "conditional", cond: bound(check), then: someBody, else: noneBody, ty: e.ty } : someBody,
409
- noneBody,
410
- ty: e.ty,
411
- };
412
- }
413
- /** Rule (expression): `Array.isArray(x) ==> B` or `!Array.isArray(x) ==> B` —
414
- * premise narrowing for spec implications. Mirrors `ruleImplOptional` but for
415
- * synth array-union discriminators.
416
- * → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch).
417
- * The other variant becomes a vacuous-true fallthrough (the implication is
418
- * trivially satisfied when the premise is false). */
419
- function ruleImplArrayIsArray(e) {
420
- if (e.kind !== "binop" || e.op !== "==>")
421
- return null;
422
- const pos = parseArrayIsArrayCall(e.left);
423
- const neg = e.left.kind === "unop" && e.left.op === "!"
424
- ? parseArrayIsArrayCall(e.left.expr)
425
- : null;
426
- const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
427
- if (!matched)
428
- return null;
429
- return {
430
- kind: "tagMatch",
431
- scrutinee: matched.scrutinee,
432
- typeName: matched.typeName,
433
- cases: [{ variant: matched.variant, body: walkExpr(e.right) }],
434
- fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } },
435
- ty: { kind: "bool" },
436
- };
437
- }
438
- /** Rule (expression): `Array.isArray(x) ? a : b` — ternary narrowing for
439
- * synth array-unions. Mirrors `ruleImplArrayIsArray` but at the conditional
440
- * position rather than the `==>` position.
441
- * → `tagMatch x { ArrayBranch => walkExpr(a) } fallthrough walkExpr(b)`
442
- * (or NonArrayBranch when the condition is negated).
443
- * Inside the matched arm, bare references to `x` are rewritten to the
444
- * variant's payload field (e.g. `x.arr`) by `transformExpr` when emitting
445
- * the tagMatch — same mechanism `ruleImplArrayIsArray` already relies on. */
446
- function ruleConditionalArrayIsArray(e) {
447
- if (e.kind !== "conditional")
448
- return null;
449
- const pos = parseArrayIsArrayCall(e.cond);
450
- // `typeof x === "string"` is a positive check like `Array.isArray`, but selects
451
- // the NonArrayBranch — its then-branch is the matched-variant body.
452
- const tof = pos ? null : parseTypeofStringCheck(e.cond);
453
- const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
454
- ? parseArrayIsArrayCall(e.cond.expr)
455
- : null;
456
- const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
457
- if (!matched)
458
- return null;
459
- const positive = pos ?? tof;
460
- const thenBody = positive ? e.then : e.else;
461
- const elseBody = positive ? e.else : e.then;
462
- return {
463
- kind: "tagMatch",
464
- scrutinee: matched.scrutinee,
465
- typeName: matched.typeName,
466
- cases: [{ variant: matched.variant, body: walkExpr(thenBody) }],
467
- fallthrough: walkExpr(elseBody),
468
- ty: e.ty,
469
- };
287
+ return presentMatchExpr(check, someBody, noneBody, e.ty);
470
288
  }
471
- /** Rule (expression): `(path !== undefined [&& rest]) ==> B` — premise narrowing
472
- * for spec implications (ensures/requires). The premise's optional checks
473
- * bind narrowed values that the conclusion can use.
289
+ /** Driver (expression): `(path !== undefined [&& rest]) ==> B` — presence
290
+ * fact in implication position (spec premises). The premise's optional
291
+ * checks bind narrowed values that the conclusion can use.
474
292
  * → `someMatch path { Some(_p_val) => (rest ==> B), None => true }`.
475
- * Walks the inner ==> recursively so chained checks (`a !== undefined && a.b !== undefined ==> ...`) become nested someMatches. */
476
- function ruleImplOptional(e) {
293
+ * Walks the inner ==> recursively so chained checks become nested
294
+ * someMatches. Spec form: no falsy gate (historical shape — presence
295
+ * suffices in the premise). */
296
+ function ruleImplOptional(e, ctx) {
477
297
  if (e.kind !== "binop" || e.op !== "==>")
478
298
  return null;
479
299
  let check;
480
300
  let restCond = null;
481
- const extracted = extractLeftmostOptionalCheck(e.left);
301
+ const extracted = leadingPresent(e.left);
482
302
  if (extracted) {
483
303
  check = extracted.check;
484
304
  restCond = extracted.restCond;
485
305
  }
486
306
  else {
487
- const c = parseSimpleOptionalCheck(e.left);
307
+ const c = presentFact(e.left);
488
308
  if (!c || c.negated)
489
309
  return null;
490
310
  check = c;
@@ -495,42 +315,23 @@ function ruleImplOptional(e) {
495
315
  return {
496
316
  kind: "someMatch",
497
317
  scrutinee: check.scrutinee, binderTy: check.innerTy,
498
- binder: check.binderHint,
499
- someBody: walkExpr(innerBody),
318
+ binder: check.binder,
319
+ someBody: walkExpr(innerBody, ctx),
500
320
  noneBody: { kind: "bool", value: true, ty: { kind: "bool" } },
501
321
  ty: { kind: "bool" },
502
322
  };
503
323
  }
504
- /** Apply an optional chain's steps (field / index / call) to a base expr —
505
- * shared by `ruleOptChain` (base = binder) and `ruleOptChainIndex` (base = arr[i]). */
506
- function applyChain(body, chain) {
507
- for (const step of chain) {
508
- if (step.kind === "field")
509
- body = { kind: "field", obj: body, field: step.name, ty: step.ty };
510
- else if (step.kind === "index")
511
- body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
512
- else
513
- body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
514
- }
515
- return body;
516
- }
517
- /** `0 <= idx && idx < arr.length` — the in-bounds guard for an array index. */
518
- function arrayBoundsCond(arr, idx) {
519
- const len = { kind: "field", obj: arr, field: "length", ty: { kind: "int" } };
520
- const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
521
- const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
522
- return { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
523
- }
524
- /** Rule (expression): `left ?? right` — nullish coalescing.
324
+ /** Driver (expression): `left ?? right` nullish coalescing.
525
325
  * → `someMatch left { Some(_v) => _v, None => right }`.
526
- * Single-evaluation: scrutinee may be any expression. */
527
- function ruleNullish(e) {
326
+ * Single-evaluation: scrutinee may be any expression. Presence-only
327
+ * semantics (`??` tests null/undefined, not falsiness) — no falsy gate. */
328
+ function ruleNullish(e, ctx) {
528
329
  if (e.kind !== "nullish")
529
330
  return null;
530
331
  if (e.left.ty.kind !== "optional")
531
332
  return null;
532
333
  const innerTy = e.left.ty.inner;
533
- const binder = freshName(`_oc${_ocCounter++}_val`);
334
+ const binder = freshOcBinder(ctx);
534
335
  return {
535
336
  kind: "someMatch",
536
337
  scrutinee: e.left, binder, binderTy: innerTy,
@@ -539,12 +340,12 @@ function ruleNullish(e) {
539
340
  ty: e.ty,
540
341
  };
541
342
  }
542
- /** Rule (expression): `arr[i] ?? right` — nullish coalescing on an array index.
543
- * Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`, undefined exactly
544
- * when out of bounds, so → `(0 <= i && i < arr.length) ? arr[i] : right`. The
545
- * guarded `then` keeps the seq index in bounds for the backend. (Map index is
546
- * already optional-typed and handled by ruleNullish above; this is the array
547
- * case, whose element type stays non-optional in expression position.) */
343
+ /** Driver (expression): `arr[i] ?? right` — in-bounds fact in nullish
344
+ * position. Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`,
345
+ * undefined exactly when out of bounds, so
346
+ * `(0 <= i && i < arr.length) ? arr[i] : right`. The guarded `then`
347
+ * keeps the seq index in bounds for the backend. (Map index is already
348
+ * optional-typed and handled by ruleNullish above.) */
548
349
  function ruleNullishIndex(e) {
549
350
  if (e.kind !== "nullish")
550
351
  return null;
@@ -555,14 +356,12 @@ function ruleNullishIndex(e) {
555
356
  const cond = arrayBoundsCond(e.left.obj, e.left.idx);
556
357
  return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
557
358
  }
558
- /** Rule (expression): `arr[i]?.<chain>` — optional chaining on an array index,
559
- * the optChain sibling of ruleNullishIndex. `arr[i]` is `T | undefined`,
560
- * undefined exactly out of bounds, so → `(0 <= i && i < arr.length) ? <chain on
561
- * arr[i]> : undefined`. The conditional's optional type makes transform wrap the
562
- * in-bounds chain result in Some and the OOB branch in None the same Option<…>
563
- * a directly-optional scrutinee yields via ruleOptChain, just bounds-guarded.
564
- * (ruleOptChain itself bails here: an array index is typed as the non-optional
565
- * element type, so its `?.` never reaches that rule.) */
359
+ /** Driver (expression): `arr[i]?.<chain>` — in-bounds fact in optChain
360
+ * position, the optChain sibling of ruleNullishIndex.
361
+ * → `(0 <= i && i < arr.length) ? <chain on arr[i]> : undefined`. The
362
+ * conditional's optional type makes transform wrap the in-bounds chain
363
+ * result in Some and the OOB branch in None. (ruleOptChain itself bails
364
+ * here: an array index is typed as the non-optional element type.) */
566
365
  function ruleOptChainIndex(e) {
567
366
  if (e.kind !== "optChain")
568
367
  return null;
@@ -575,15 +374,13 @@ function ruleOptChainIndex(e) {
575
374
  const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
576
375
  return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
577
376
  }
578
- /** Rule (statement): reconcile a `const e = arr[i]` whose binding is optional
579
- * (`e: T | undefined`) but whose array-index initializer is total (`T`). Model
580
- * the index as its JS semantics `e := (0 <= i && i < arr.length) ? arr[i] :
581
- * undefined`so `e` is a real `Option<T>` and a later `e?.f` someMatch is
582
- * well-typed; an in-bounds proof makes the None branch dead, so safely-indexed
583
- * code verifies as if total. Bound-form sibling of ruleOptChainIndex /
584
- * ruleNullishIndex. Fires purely on the optional-binding/total-index shape (the
585
- * usual source is `noUncheckedIndexedAccess`, but the flag itself is never
586
- * checked). Skipped when the element type is already optional: no mismatch. */
377
+ /** Driver (statement): reconcile a `const e = arr[i]` whose binding is optional
378
+ * (`e: T | undefined`) but whose array-index initializer is total (`T`)
379
+ * in-bounds fact in initializer position. Model the index as its JS
380
+ * semantics — `e := inBounds ? arr[i] : undefined` so `e` is a real
381
+ * `Option<T>` and a later `e?.f` someMatch is well-typed; an in-bounds proof
382
+ * makes the None branch dead. Skipped when the element type is already
383
+ * optional: no mismatch. */
587
384
  function ruleOptionalIndexBinding(s) {
588
385
  if (s.kind !== "let")
589
386
  return null;
@@ -601,17 +398,17 @@ function ruleOptionalIndexBinding(s) {
601
398
  const guarded = { kind: "conditional", cond, then: init, else: undef, ty: s.ty };
602
399
  return { ...s, init: guarded };
603
400
  }
604
- /** Rule (expression): `obj?.<chain>` — single-eval optional chain.
401
+ /** Driver (expression): `obj?.<chain>` — single-eval optional chain.
605
402
  * → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`.
606
403
  * The someBody applies the chain to the binder directly (field/call/index),
607
404
  * so transform doesn't substitute. Scrutinee can be any expression. */
608
- function ruleOptChain(e) {
405
+ function ruleOptChain(e, ctx) {
609
406
  if (e.kind !== "optChain")
610
407
  return null;
611
408
  if (e.obj.ty.kind !== "optional")
612
409
  return null;
613
410
  const innerTy = e.obj.ty.inner;
614
- const binder = freshName(`_oc${_ocCounter++}_val`);
411
+ const binder = freshOcBinder(ctx);
615
412
  const body = applyChain({ kind: "var", name: binder, ty: innerTy }, e.chain);
616
413
  const noneBody = { kind: "var", name: "undefined", ty: { kind: "void" } };
617
414
  return {
@@ -620,41 +417,12 @@ function ruleOptChain(e) {
620
417
  someBody: body, noneBody, ty: e.ty,
621
418
  };
622
419
  }
623
- /** Minimal structural equality on the IR shapes we narrow against: var, field
624
- * chain, and index (with pure key). Enough to recognize `m[k]` on both sides
625
- * of a `k in m ? m[k] : default` ternary. */
626
- function exprEqual(a, b) {
627
- if (a.kind !== b.kind)
628
- return false;
629
- if (a.kind === "var" && b.kind === "var")
630
- return a.name === b.name;
631
- if (a.kind === "field" && b.kind === "field")
632
- return a.field === b.field && exprEqual(a.obj, b.obj);
633
- if (a.kind === "index" && b.kind === "index")
634
- return exprEqual(a.obj, b.obj) && exprEqual(a.idx, b.idx);
635
- return false;
636
- }
637
- /** Produce a reader-friendly binder hint for `m[k]` when both m and k are
638
- * access-path shaped (var / field chain). Falls back to a generic counter
639
- * name for computed keys. */
640
- function binderHintForMapAccess(m, k) {
641
- const mHint = binderHintFor(m);
642
- const kHint = binderHintFor(k);
643
- if (mHint && kHint) {
644
- // mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`.
645
- const mStem = mHint.replace(/_val$/, "");
646
- const kStem = kHint.replace(/^_/, "").replace(/_val$/, "");
647
- return freshName(`${mStem}_${kStem}_val`);
648
- }
649
- return freshName(`_oc${_ocCounter++}_val`);
650
- }
651
- /** Rule (expression): `k in m ? m[k] : default` where m is map-typed.
652
- * The then-branch must be exactly `m[k]` (same obj, same key). This mirrors
653
- * the discriminant-`in` path (line 438) but gated on `map` instead of `user`.
420
+ /** Driver (expression): `k in m ? m[k] : default` key-membership fact in
421
+ * ternary position, m map-typed. The then-branch must be exactly `m[k]`.
654
422
  * → `someMatch m[k] { Some(_m_k_val) => _m_k_val, None => default }`.
655
423
  * The existing Dafny peephole collapses the result to
656
424
  * `if k in m then m[k] else default`. */
657
- function ruleConditionalInMap(e) {
425
+ function ruleConditionalInMap(e, ctx) {
658
426
  if (e.kind !== "conditional")
659
427
  return null;
660
428
  if (e.cond.kind !== "binop" || e.cond.op !== "in")
@@ -678,7 +446,7 @@ function ruleConditionalInMap(e) {
678
446
  if (e.then.ty.kind !== "optional")
679
447
  return null;
680
448
  const innerTy = m.ty.value;
681
- const binder = binderHintForMapAccess(m, k);
449
+ const binder = binderHintForMapAccess(m, k, ctx);
682
450
  return {
683
451
  kind: "someMatch",
684
452
  scrutinee: e.then, binder, binderTy: innerTy,
@@ -686,8 +454,9 @@ function ruleConditionalInMap(e) {
686
454
  noneBody: e.else, ty: innerTy,
687
455
  };
688
456
  }
689
- /** Rule (expression): `opt ? a : b` (truthiness — cond itself is optional).
690
- * Only fires for simple var or simple `obj.field` cond. */
457
+ /** Driver (expression): `opt ? a : b` (truthiness — cond itself is optional).
458
+ * Only fires for simple var or simple `obj.field` cond. Historical shape:
459
+ * no falsy gate on the bound value (unlike the other truthiness positions). */
691
460
  function ruleConditionalOptionalTruthy(e) {
692
461
  if (e.kind !== "conditional")
693
462
  return null;
@@ -704,293 +473,45 @@ function ruleConditionalOptionalTruthy(e) {
704
473
  someBody: e.then, noneBody: e.else, ty: e.ty,
705
474
  };
706
475
  }
707
- /** Find the leftmost `parse`-matching conjunct anywhere in an `&&` chain,
708
- * returning it plus the remaining conjunction. Conjunct order doesn't carry
709
- * semantic weight, so either side is fine. Shared by the optional and
710
- * Array.isArray chain extractors — they differ only in `parse`.
711
- * `(x !== undefined && b) && c` → { check, restCond: b && c }. */
712
- function extractLeftmostCheck(cond, parse) {
713
- if (cond.kind !== "binop" || cond.op !== "&&")
714
- return null;
715
- const left = parse(cond.left);
716
- if (left)
717
- return { check: left, restCond: cond.right };
718
- const right = parse(cond.right);
719
- if (right)
720
- return { check: right, restCond: cond.left };
721
- if (cond.left.kind === "binop" && cond.left.op === "&&") {
722
- const inner = extractLeftmostCheck(cond.left, parse);
723
- if (inner)
724
- return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
725
- }
726
- if (cond.right.kind === "binop" && cond.right.op === "&&") {
727
- const inner = extractLeftmostCheck(cond.right, parse);
728
- if (inner)
729
- return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
730
- }
731
- return null;
732
- }
733
- /** `&&`-chain extractor for a positive optional check. */
734
- function extractLeftmostOptionalCheck(cond) {
735
- return extractLeftmostCheck(cond, e => {
736
- const c = parseSimpleOptionalCheck(e);
737
- return c && !c.negated ? c : null;
738
- });
739
- }
740
- /** Rule: `if (x !== undefined && rest) then` (no else) where x is a pure
741
- * access path.
476
+ /** Driver: `if (x !== undefined && rest) then` (no else) presence fact in
477
+ * guarded-if position.
742
478
  * → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`.
743
- * Walks the inner if back through narrow so that nested optional checks in rest
744
- * (`a !== undefined && a.b !== undefined && ...`) also become someMatches. */
745
- function ruleIfAndOptional(s) {
479
+ * Walks the inner if back through narrow so that nested optional checks in
480
+ * rest also become someMatches. */
481
+ function ruleIfAndOptional(s, ctx) {
746
482
  if (s.kind !== "if")
747
483
  return null;
748
484
  if (s.else.length !== 0)
749
485
  return null;
750
- const extracted = extractLeftmostOptionalCheck(s.cond);
486
+ const extracted = leadingPresent(s.cond);
751
487
  if (!extracted)
752
488
  return null;
753
489
  const { check, restCond } = extracted;
754
490
  const innerIf = { kind: "if", cond: restCond, then: s.then, else: [] };
755
- const someBody = canBeFalsy(check)
756
- ? [{ kind: "if", cond: bound(check), then: [walkStmt(innerIf)], else: [] }]
757
- : [walkStmt(innerIf)];
758
- return {
759
- kind: "someMatch",
760
- scrutinee: check.scrutinee, binderTy: check.innerTy,
761
- binder: check.binderHint,
762
- someBody,
763
- noneBody: [],
764
- };
491
+ return presentMatchStmts(check, [walkStmt(innerIf, ctx)], []);
765
492
  }
766
- /** Rule: a bare expression statement `x !== undefined && rest` (the `if`-less
767
- * guard idiom, TS-equivalent to `if (x !== undefined) rest;`) where `x` is a
768
- * pure access path.
493
+ /** Driver: a bare expression statement `x !== undefined && rest` (the
494
+ * `if`-less guard idiom, TS-equivalent to `if (x !== undefined) rest;`).
769
495
  * → `someMatch x { Some(_x_val) => rest;, None => {} }`.
770
- * Runs `rest` for effect inside the narrowed scope. Wrapping `rest` as an
771
- * expr-statement and walking it lets chained checks
772
- * (`a !== undefined && a.b !== undefined && a.b.f()`) nest into someMatches.
773
- * Unlike the ternary rule (`ruleConditionalAndOptional`), a method call in
774
- * `rest` is fine here: a statement-level someMatch arm keeps it in statement
775
- * position, so transform never ANF-lifts it out of the arm (which would drop
776
- * the guard and reference the un-narrowed optional). */
777
- function ruleExprStmtAndOptional(s) {
496
+ * Runs `rest` for effect inside the narrowed scope. Unlike the ternary
497
+ * driver (`ruleConditionalAndOptional`), a method call in `rest` is fine
498
+ * here: a statement-level someMatch arm keeps it in statement position, so
499
+ * transform never ANF-lifts it out of the arm. */
500
+ function ruleExprStmtAndOptional(s, ctx) {
778
501
  if (s.kind !== "expr")
779
502
  return null;
780
503
  if (s.expr.kind !== "binop" || s.expr.op !== "&&")
781
504
  return null;
782
- const extracted = extractLeftmostOptionalCheck(s.expr);
505
+ const extracted = leadingPresent(s.expr);
783
506
  if (!extracted)
784
507
  return null;
785
508
  const { check, restCond } = extracted;
786
509
  const innerStmt = { kind: "expr", expr: restCond };
787
- const someBody = canBeFalsy(check)
788
- ? [{ kind: "if", cond: bound(check), then: [walkStmt(innerStmt)], else: [] }]
789
- : [walkStmt(innerStmt)];
790
- return {
791
- kind: "someMatch",
792
- scrutinee: check.scrutinee, binderTy: check.innerTy,
793
- binder: check.binderHint,
794
- someBody,
795
- noneBody: [],
796
- };
797
- }
798
- // ── Discriminant narrowing ──────────────────────────────────
799
- /** Detect `Array.isArray(<path>)` where `<path>` is a var or a chain of
800
- * field accesses rooted at a var, and the path's type is a synthesized
801
- * array-union (discriminant `"__isArray__"`). Returns the variant name to
802
- * narrow to. The scrutinee is whatever path the user wrote — downstream
803
- * transforms substitute it inside the matched arm. */
804
- function parseArrayIsArrayCall(call) {
805
- if (call.kind !== "call")
806
- return null;
807
- if (call.fn.kind !== "field" || call.fn.field !== "isArray")
808
- return null;
809
- if (call.fn.obj.kind !== "var" || call.fn.obj.name !== "Array")
810
- return null;
811
- if (call.args.length !== 1)
812
- return null;
813
- const arg = call.args[0];
814
- if (!isNarrowablePath(arg) || arg.ty.kind !== "user")
815
- return null;
816
- const baseTyName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
817
- const decl = _typeDecls.find(d => d.name === baseTyName);
818
- if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
819
- return null;
820
- return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
821
- }
822
- /** Detect `typeof <path> === "string"` where `<path>`'s type is a synth array-
823
- * union (`U | T[]`) AND its `NonArrayBranch` payload `U` is itself `string`.
824
- * The runtime `=== "string"` test matches that branch only when `U` is string —
825
- * for any other non-array payload (`number | T[]`, …) it never holds, so we must
826
- * NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */
827
- function parseTypeofStringCheck(e) {
828
- if (e.kind !== "binop" || e.op !== "===")
829
- return null;
830
- const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr
831
- : e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null;
832
- const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null;
833
- if (!tof || lit !== "string")
834
- return null;
835
- if (!isNarrowablePath(tof) || tof.ty.kind !== "user")
836
- return null;
837
- const baseTyName = tof.ty.name.includes("<") ? tof.ty.name.slice(0, tof.ty.name.indexOf("<")) : tof.ty.name;
838
- const decl = _typeDecls.find(d => d.name === baseTyName);
839
- if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
840
- return null;
841
- const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type;
842
- if (valTy?.kind !== "string")
843
- return null; // guard: the non-array branch must actually be `string`
844
- return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" };
845
- }
846
- /** A "narrowable path" is a var or a chain of field accesses rooted at a var
847
- * — i.e., pure and structurally addressable, so transforms can substitute
848
- * occurrences inside a matched arm without worrying about re-evaluation. */
849
- function isNarrowablePath(e) {
850
- if (e.kind === "var")
851
- return true;
852
- if (e.kind === "field")
853
- return isNarrowablePath(e.obj);
854
- return false;
855
- }
856
- /** `&&`-chain extractor for `Array.isArray(path)` (positive form only — a negated
857
- * `!Array.isArray(...)` would narrow to the wrong variant for then-body consumers,
858
- * so those are left to the untouched-conditional path). */
859
- function extractLeftmostArrayIsArrayCheck(cond) {
860
- return extractLeftmostCheck(cond, parseArrayIsArrayCall);
861
- }
862
- /** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth
863
- * array-union) as a positive discriminant check. Returns the scrutinee var
864
- * (with its type), type name, and variant. */
865
- function parseDiscriminantCond(cond) {
866
- // Pattern: x.discriminant === "variant"
867
- if (cond.kind === "binop" && cond.op === "===" && cond.right.kind === "str" &&
868
- cond.left.kind === "field" && cond.left.isDiscriminant &&
869
- cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
870
- return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
871
- }
872
- // Pattern: 'key' in x — narrows x to the unique variant containing `key`.
873
- if (cond.kind === "binop" && cond.op === "in" &&
874
- cond.left.kind === "str" && cond.right.kind === "var" &&
875
- cond.right.ty.kind === "user") {
876
- const key = cond.left.value;
877
- const typeName = cond.right.ty.name;
878
- const baseTyName = typeName.includes("<") ? typeName.slice(0, typeName.indexOf("<")) : typeName;
879
- const decl = _typeDecls.find(d => d.name === baseTyName);
880
- if (decl?.kind === "discriminated-union" && decl.variants) {
881
- const matches = decl.variants.filter(v => v.fields.some(f => f.name === key));
882
- if (matches.length === 1) {
883
- return { scrutinee: cond.right, typeName, variant: matches[0].name };
884
- }
885
- }
886
- }
887
- // Pattern: Array.isArray(x) — narrows x to the ArrayBranch variant of a
888
- // synthesized array-union (discriminant "__isArray__"). Statement-level
889
- // discriminant chains (`if (Array.isArray(x)) {...} else if (...)`) still
890
- // require a bare-var scrutinee since the existing var-name-keyed
891
- // replacement machinery in transform.ts only handles that shape; path
892
- // scrutinees (e.g. `m.content`) are handled exclusively by
893
- // `ruleConditionalArrayIsArray` and the expression-form tagMatch path.
894
- const arrCheck = parseArrayIsArrayCall(cond);
895
- if (arrCheck && arrCheck.scrutinee.kind === "var") {
896
- return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: arrCheck.variant };
897
- }
898
- return null;
899
- }
900
- /** Detect `x.kind !== "variant"` (negative discriminant check) or
901
- * `!Array.isArray(x)` (synth array-union, narrows to NonArrayBranch). */
902
- function parseNegativeDiscriminantCond(cond) {
903
- if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" &&
904
- cond.left.kind === "field" && cond.left.isDiscriminant &&
905
- cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
906
- return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
907
- }
908
- // Pattern: !Array.isArray(x) — narrows x to the NonArrayBranch variant.
909
- // Same var-scrutinee restriction as parseDiscriminantCond.
910
- if (cond.kind === "unop" && cond.op === "!") {
911
- const arrCheck = parseArrayIsArrayCall(cond.expr);
912
- if (arrCheck && arrCheck.scrutinee.kind === "var") {
913
- return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: "NonArrayBranch" };
914
- }
915
- }
916
- return null;
917
- }
918
- function isTerminating(stmts) {
919
- if (stmts.length === 0)
920
- return false;
921
- const last = stmts[stmts.length - 1];
922
- return last.kind === "return" || last.kind === "throw" || last.kind === "break" || last.kind === "continue";
923
- }
924
- /** Rule (list-level): consecutive `if (x.kind === "v") ...` chain → tagMatch.
925
- * Walks consecutive top-level ifs on the same discriminator var; the first
926
- * one with an else-branch ends the chain (else becomes fallthrough; if-else-if
927
- * flattens into more cases). Returns the tagMatch and how many stmts consumed. */
928
- function ruleDiscriminantChain(stmts) {
929
- if (stmts.length === 0 || stmts[0].kind !== "if")
930
- return null;
931
- const first = parseDiscriminantCond(stmts[0].cond);
932
- if (!first)
933
- return null;
934
- const cases = [];
935
- function collectElse(s) {
936
- const p = parseDiscriminantCond(s.cond);
937
- if (!p || p.scrutinee.name !== first.scrutinee.name)
938
- return [s];
939
- cases.push({ variant: p.variant, body: s.then });
940
- if (s.else.length === 0)
941
- return [];
942
- if (s.else.length === 1 && s.else[0].kind === "if")
943
- return collectElse(s.else[0]);
944
- return s.else;
945
- }
946
- let consumed = 0;
947
- for (let i = 0; i < stmts.length; i++) {
948
- const s = stmts[i];
949
- if (s.kind !== "if")
950
- break;
951
- const p = parseDiscriminantCond(s.cond);
952
- if (!p || p.scrutinee.name !== first.scrutinee.name)
953
- break;
954
- cases.push({ variant: p.variant, body: s.then });
955
- consumed = i + 1;
956
- if (s.else.length > 0) {
957
- const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
958
- return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
959
- cases, fallthrough: ft }, consumed };
960
- }
961
- }
962
- if (cases.length === 0)
963
- return null;
964
- // If every case terminates, the trailing statements are the default arm
965
- // (preserving the clean dispatch-as-expression shape). Otherwise the tail runs
966
- // after the match for every variant, so leave it to the caller (empty default)
967
- // rather than mis-routing it into the default arm only.
968
- if (cases.every(c => isTerminating(c.body))) {
969
- return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
970
- cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
971
- }
972
- return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
973
- cases, fallthrough: [] }, consumed };
510
+ return presentMatchStmts(check, [walkStmt(innerStmt, ctx)], []);
974
511
  }
975
- /** Rule (list-level): `if (x.kind !== "v") terminate; rest` tagMatch
976
- * with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */
977
- function ruleDiscriminantNegEarlyReturn(stmts) {
978
- if (stmts.length < 2)
979
- return null;
980
- const first = stmts[0];
981
- if (first.kind !== "if" || first.else.length > 0)
982
- return null;
983
- if (!isTerminating(first.then))
984
- return null;
985
- const cond = parseNegativeDiscriminantCond(first.cond);
986
- if (!cond)
987
- return null;
988
- return { stmt: { kind: "tagMatch", scrutinee: cond.scrutinee, typeName: cond.typeName,
989
- cases: [{ variant: cond.variant, body: stmts.slice(1) }], fallthrough: first.then },
990
- consumed: stmts.length };
991
- }
992
- /** Rule (statement): `let x = (e_opt && rest) ? a : b` where rest may contain
993
- * method calls. → `var x: T := b; someMatch e_opt { Some(_v) => { if rest { x := a } } }`.
512
+ /** Driver (statement): `let x = (e_opt && rest) ? a : b` presence fact in
513
+ * conditional-initializer position, where rest may contain method calls.
514
+ * → `var x: T := b; someMatch e_opt { Some(_v) => { if rest { x := a } } }`.
994
515
  * Statement-level form is needed because Dafny doesn't allow method calls
995
516
  * inside match expression arms. */
996
517
  function ruleLetCondAndOptional(s) {
@@ -998,45 +519,32 @@ function ruleLetCondAndOptional(s) {
998
519
  return null;
999
520
  if (s.init.kind !== "conditional")
1000
521
  return null;
1001
- const extracted = extractLeftmostOptionalCheck(s.init.cond);
522
+ const extracted = leadingPresent(s.init.cond);
1002
523
  if (!extracted)
1003
524
  return null;
1004
525
  const { check, restCond } = extracted;
1005
526
  const assignIf = { kind: "if", cond: restCond,
1006
527
  then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] };
1007
- const someBody = canBeFalsy(check)
1008
- ? [{ kind: "if", cond: bound(check), then: [assignIf], else: [] }]
1009
- : [assignIf];
1010
- const sm = {
1011
- kind: "someMatch",
1012
- scrutinee: check.scrutinee, binderTy: check.innerTy,
1013
- binder: check.binderHint,
1014
- someBody,
1015
- noneBody: [],
1016
- };
1017
528
  return [
1018
529
  { kind: "let", name: s.name, ty: s.ty, mutable: true, init: s.init.else },
1019
- sm,
530
+ presentMatchStmts(check, [assignIf], []),
1020
531
  ];
1021
532
  }
1022
- /** Built-in collection methods that lower to pure Dafny expressions
1023
- * (`x in arr`, `x in m`, `x in s`, `|s|`, `s.Keys`, etc.) even though they
1024
- * carry `callKind: "method"` from resolve. Safe inside match arms. */
1025
- const PURE_BUILTIN_METHODS = new Set([
1026
- "includes", "has", "size", "length", "keys", "values",
1027
- ]);
1028
533
  /** Does this expression contain a method call that would be lifted to a
1029
534
  * var binding outside its containing expression by transform? Such calls
1030
535
  * are unsafe inside a match arm — the lifted binding would reference a
1031
- * name only valid in the arm. Built-in pure methods are exempt. */
536
+ * name only valid in the arm. Builtins whose registry entry is `pure`
537
+ * (they lower to pure Dafny expressions: `x in arr`, `x in m`, `s.Keys`,
538
+ * …) are exempt even though they carry `callKind: "method"`. */
1032
539
  function containsMethodCall(e) {
1033
540
  if (e.kind === "call" && e.callKind === "method" &&
1034
- !(e.fn.kind === "field" && PURE_BUILTIN_METHODS.has(e.fn.field))) {
541
+ !(e.builtinId !== undefined && builtinSpec(e.builtinId).pure)) {
1035
542
  return true;
1036
543
  }
1037
544
  switch (e.kind) {
1038
545
  case "var":
1039
546
  case "num":
547
+ case "bigint":
1040
548
  case "str":
1041
549
  case "bool":
1042
550
  case "havoc":
@@ -1064,19 +572,18 @@ function containsMethodCall(e) {
1064
572
  (e.fallthrough ? containsMethodCall(e.fallthrough) : false);
1065
573
  }
1066
574
  }
1067
- /** Rule (expression): `x !== undefined && rest ? a : b`.
575
+ /** Driver (expression): `x !== undefined && rest ? a : b` — presence fact in
576
+ * guarded-ternary position.
1068
577
  * → `someMatch x { Some(_x_val) => if rest then a else b, None => b }`.
1069
- * Walks the inner conditional back through narrow so chained checks
1070
- * (`a !== undefined && a.b !== undefined ? ... : ...`) become nested
1071
- * someMatches rather than leaving inner optional checks as raw conditionals.
1072
- * Does NOT fire if the guard `rest` contains method calls — transform lifts
1073
- * those out of the match arm, breaking the binder scope. The original
1074
- * transform's let-desugar (transformStmt let-case) handles those by lifting
1075
- * to a mutable var first. */
1076
- function ruleConditionalAndOptional(e) {
578
+ * Walks the inner conditional back through narrow so chained checks become
579
+ * nested someMatches. Does NOT fire if the guard `rest` contains method
580
+ * calls transform lifts those out of the match arm, breaking the binder
581
+ * scope. The original transform's let-desugar handles those by lifting to
582
+ * a mutable var first. */
583
+ function ruleConditionalAndOptional(e, ctx) {
1077
584
  if (e.kind !== "conditional")
1078
585
  return null;
1079
- const extracted = extractLeftmostOptionalCheck(e.cond);
586
+ const extracted = leadingPresent(e.cond);
1080
587
  if (!extracted)
1081
588
  return null;
1082
589
  const { check, restCond } = extracted;
@@ -1086,48 +593,92 @@ function ruleConditionalAndOptional(e) {
1086
593
  kind: "conditional",
1087
594
  cond: restCond, then: e.then, else: e.else, ty: e.ty,
1088
595
  };
1089
- const someExpr = walkExpr(innerCond);
1090
- const someBody = canBeFalsy(check)
1091
- ? { kind: "conditional", cond: bound(check), then: someExpr, else: e.else, ty: e.ty }
1092
- : someExpr;
596
+ return presentMatchExpr(check, walkExpr(innerCond, ctx), e.else, e.ty);
597
+ }
598
+ // ── Variant drivers (discriminants and synth array-unions) ──
599
+ /** Driver (expression): `Array.isArray(x) ==> B` or `!Array.isArray(x) ==> B` —
600
+ * isArray fact in implication position (spec premises).
601
+ * → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch).
602
+ * The other variant becomes a vacuous-true fallthrough. */
603
+ function ruleImplArrayIsArray(e, ctx) {
604
+ if (e.kind !== "binop" || e.op !== "==>")
605
+ return null;
606
+ const pos = isArrayFact(e.left, ctx);
607
+ const neg = e.left.kind === "unop" && e.left.op === "!"
608
+ ? isArrayFact(e.left.expr, ctx)
609
+ : null;
610
+ const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
611
+ if (!matched)
612
+ return null;
1093
613
  return {
1094
- kind: "someMatch",
1095
- scrutinee: check.scrutinee, binderTy: check.innerTy,
1096
- binder: check.binderHint,
1097
- someBody, noneBody: e.else, ty: e.ty,
614
+ kind: "tagMatch",
615
+ scrutinee: matched.scrutinee,
616
+ typeName: matched.typeName,
617
+ cases: [{ variant: matched.variant, body: walkExpr(e.right, ctx) }],
618
+ fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } },
619
+ ty: { kind: "bool" },
620
+ };
621
+ }
622
+ /** Driver (expression): `Array.isArray(x) ? a : b` — isArray fact in ternary
623
+ * position (also `typeof x === "string"`, which selects the NonArrayBranch).
624
+ * → `tagMatch x { <variant> => walkExpr(then-side) } fallthrough walkExpr(else-side)`.
625
+ * Inside the matched arm, bare references to `x` are rewritten to the
626
+ * variant's payload field by `transformExpr` when emitting the tagMatch. */
627
+ function ruleConditionalArrayIsArray(e, ctx) {
628
+ if (e.kind !== "conditional")
629
+ return null;
630
+ const pos = isArrayFact(e.cond, ctx);
631
+ // `typeof x === "string"` is a positive check like `Array.isArray`, but selects
632
+ // the NonArrayBranch — its then-branch is the matched-variant body.
633
+ const tof = pos ? null : typeofStringFact(e.cond, ctx);
634
+ const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
635
+ ? isArrayFact(e.cond.expr, ctx)
636
+ : null;
637
+ const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
638
+ if (!matched)
639
+ return null;
640
+ const positive = pos ?? tof;
641
+ const thenBody = positive ? e.then : e.else;
642
+ const elseBody = positive ? e.else : e.then;
643
+ return {
644
+ kind: "tagMatch",
645
+ scrutinee: matched.scrutinee,
646
+ typeName: matched.typeName,
647
+ cases: [{ variant: matched.variant, body: walkExpr(thenBody, ctx) }],
648
+ fallthrough: walkExpr(elseBody, ctx),
649
+ ty: e.ty,
1098
650
  };
1099
651
  }
1100
- /** Rule (statement): `if (<rest> && Array.isArray(path) && <more>) then [else]`
652
+ /** Driver: `if (<rest> && Array.isArray(path) && <more>) then [else]`
653
+ * isArray fact in guarded-if position.
1101
654
  * → `tagMatch path { ArrayBranch => if (<rest && more>) then [else] }`.
1102
655
  * The remaining conjuncts move inside the matched arm so any narrowing the
1103
- * `then` body relies on (typed `path` accesses) sees the unwrapped variant.
1104
- * Mirrors `ruleIfAndOptional` but for synth array-unions. */
1105
- function ruleIfAndArrayIsArray(s) {
656
+ * `then` body relies on (typed `path` accesses) sees the unwrapped variant. */
657
+ function ruleIfAndArrayIsArray(s, ctx) {
1106
658
  if (s.kind !== "if")
1107
659
  return null;
1108
- const extracted = extractLeftmostArrayIsArrayCheck(s.cond);
660
+ const extracted = leadingIsArray(s.cond, ctx);
1109
661
  if (!extracted)
1110
662
  return null;
1111
663
  const { check, restCond } = extracted;
1112
- // Inner if uses the remaining conjunction (or just the then-body if rest is
1113
- // a tautology — but in practice extractLeftmost leaves at least one other
1114
- // conjunct). Walk recursively so nested checks compose.
664
+ // Inner if uses the remaining conjunction. Walk recursively so nested
665
+ // checks compose.
1115
666
  const innerThen = [{ kind: "if", cond: restCond, then: s.then, else: s.else }];
1116
667
  return {
1117
668
  kind: "tagMatch",
1118
669
  scrutinee: check.scrutinee,
1119
670
  typeName: check.typeName,
1120
- cases: [{ variant: check.variant, body: innerThen.map(walkStmt) }],
671
+ cases: [{ variant: check.variant, body: innerThen.map(x => walkStmt(x, ctx)) }],
1121
672
  fallthrough: s.else,
1122
673
  };
1123
674
  }
1124
- /** Rule (expression): `(<rest> && Array.isArray(path)) ? a : b`
1125
- * `tagMatch path { ArrayBranch => (<rest>) ? a : b } fallthrough b`.
1126
- * Mirrors `ruleConditionalAndOptional`. */
1127
- function ruleConditionalAndArrayIsArray(e) {
675
+ /** Driver (expression): `(<rest> && Array.isArray(path)) ? a : b` — isArray
676
+ * fact in guarded-ternary position.
677
+ * `tagMatch path { ArrayBranch => (<rest>) ? a : b } fallthrough b`. */
678
+ function ruleConditionalAndArrayIsArray(e, ctx) {
1128
679
  if (e.kind !== "conditional")
1129
680
  return null;
1130
- const extracted = extractLeftmostArrayIsArrayCheck(e.cond);
681
+ const extracted = leadingIsArray(e.cond, ctx);
1131
682
  if (!extracted)
1132
683
  return null;
1133
684
  const { check, restCond } = extracted;
@@ -1139,31 +690,99 @@ function ruleConditionalAndArrayIsArray(e) {
1139
690
  kind: "tagMatch",
1140
691
  scrutinee: check.scrutinee,
1141
692
  typeName: check.typeName,
1142
- cases: [{ variant: check.variant, body: walkExpr(innerCond) }],
693
+ cases: [{ variant: check.variant, body: walkExpr(innerCond, ctx) }],
1143
694
  fallthrough: e.else,
1144
695
  ty: e.ty,
1145
696
  };
1146
697
  }
698
+ /** Driver (list-level): consecutive `if (x.kind === "v") ...` chain →
699
+ * tagMatch. Walks consecutive top-level ifs on the same discriminator var;
700
+ * the first one with an else-branch ends the chain (else becomes
701
+ * fallthrough; if-else-if flattens into more cases). Returns the tagMatch
702
+ * and how many stmts consumed. */
703
+ function ruleDiscriminantChain(stmts, ctx) {
704
+ if (stmts.length === 0 || stmts[0].kind !== "if")
705
+ return null;
706
+ const first = variantFact(stmts[0].cond, ctx);
707
+ if (!first)
708
+ return null;
709
+ const cases = [];
710
+ function collectElse(s) {
711
+ const p = variantFact(s.cond, ctx);
712
+ if (!p || p.scrutinee.name !== first.scrutinee.name)
713
+ return [s];
714
+ cases.push({ variant: p.variant, body: s.then });
715
+ if (s.else.length === 0)
716
+ return [];
717
+ if (s.else.length === 1 && s.else[0].kind === "if")
718
+ return collectElse(s.else[0]);
719
+ return s.else;
720
+ }
721
+ let consumed = 0;
722
+ for (let i = 0; i < stmts.length; i++) {
723
+ const s = stmts[i];
724
+ if (s.kind !== "if")
725
+ break;
726
+ const p = variantFact(s.cond, ctx);
727
+ if (!p || p.scrutinee.name !== first.scrutinee.name)
728
+ break;
729
+ cases.push({ variant: p.variant, body: s.then });
730
+ consumed = i + 1;
731
+ if (s.else.length > 0) {
732
+ const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
733
+ return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
734
+ cases, fallthrough: ft }, consumed };
735
+ }
736
+ }
737
+ if (cases.length === 0)
738
+ return null;
739
+ // If every case terminates, the trailing statements are the default arm
740
+ // (preserving the clean dispatch-as-expression shape). Otherwise the tail runs
741
+ // after the match for every variant, so leave it to the caller (empty default)
742
+ // rather than mis-routing it into the default arm only.
743
+ if (cases.every(c => isTerminating(c.body))) {
744
+ return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
745
+ cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
746
+ }
747
+ return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
748
+ cases, fallthrough: [] }, consumed };
749
+ }
750
+ /** Driver (list-level): `if (x.kind !== "v") terminate; rest` → tagMatch
751
+ * with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */
752
+ function ruleDiscriminantNegEarlyReturn(stmts, ctx) {
753
+ if (stmts.length < 2)
754
+ return null;
755
+ const first = stmts[0];
756
+ if (first.kind !== "if" || first.else.length > 0)
757
+ return null;
758
+ if (!isTerminating(first.then))
759
+ return null;
760
+ const cond = negVariantFact(first.cond, ctx);
761
+ if (!cond)
762
+ return null;
763
+ return { stmt: { kind: "tagMatch", scrutinee: cond.scrutinee, typeName: cond.typeName,
764
+ cases: [{ variant: cond.variant, body: stmts.slice(1) }], fallthrough: first.then },
765
+ consumed: stmts.length };
766
+ }
1147
767
  // ── Function / module entry ──────────────────────────────────
1148
- function narrowFunction(fn) {
768
+ function narrowFunction(fn, ctx) {
1149
769
  return {
1150
770
  ...fn,
1151
- requires: fn.requires.map(e => walkExpr(e)),
1152
- ensures: fn.ensures.map(e => walkExpr(e)),
1153
- decreases: fn.decreases ? walkExpr(fn.decreases) : null,
1154
- body: walkStmts(fn.body),
771
+ requires: fn.requires.map(e => walkExpr(e, ctx)),
772
+ ensures: fn.ensures.map(e => walkExpr(e, ctx)),
773
+ decreases: fn.decreases ? walkExpr(fn.decreases, ctx) : null,
774
+ body: walkStmts(fn.body, ctx),
1155
775
  };
1156
776
  }
1157
777
  export function narrowModule(mod) {
1158
- _ocCounter = 0;
1159
- _typeDecls = mod.typeDecls;
778
+ const ctx = { decls: mod.typeDecls, oc: { n: 0 } };
1160
779
  return {
1161
780
  ...mod,
1162
- constants: mod.constants.map(c => ({ ...c, value: walkExpr(c.value) })),
1163
- functions: mod.functions.map(narrowFunction),
781
+ constants: mod.constants.map(c => ({ ...c, value: walkExpr(c.value, ctx) })),
782
+ functions: mod.functions.map(fn => narrowFunction(fn, ctx)),
1164
783
  classes: mod.classes.map(cls => ({
1165
784
  ...cls,
1166
- methods: cls.methods.map(narrowFunction),
785
+ methods: cls.methods.map(m => narrowFunction(m, ctx)),
1167
786
  })),
1168
787
  };
1169
788
  }