lemmascript 0.5.18 → 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,83 +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
  */
37
36
  import { isTerminatorKind } from "./typedir.js";
38
37
  import { freshName } from "./names.js";
39
- // ── Optional-check detection ────────────────────────────────
40
- /** Counter for naming optChain binders. Reset per module. */
41
- let _ocCounter = 0;
42
- /** Type declarations for this module. Set in narrowModule, used by the
43
- * discriminant-narrowing rules to resolve `'key' in x` to a variant. */
44
- let _typeDecls = [];
45
- /** Detect optional checks: `e !== undefined`, `e === undefined`, or `!e` for a
46
- * pure-access-path optional-typed e. `!e` is equivalent to `=== undefined`.
47
- * Following TS, only pure access paths narrow; complex scrutinees return null. */
48
- function parseOptionalCheck(cond) {
49
- // `!e` where e is optional — a truthiness form: false iff e is absent OR its
50
- // inner value is itself falsy (so `Some(0)`/`Some("")` count as falsy too).
51
- if (cond.kind === "unop" && cond.op === "!" && cond.expr.ty.kind === "optional") {
52
- const e = cond.expr;
53
- const innerTy = cond.expr.ty.inner;
54
- const hint = binderHintFor(e);
55
- if (hint === null)
56
- return null;
57
- return { scrutinee: e, innerTy, negated: true, binderHint: freshName(hint), truthiness: true };
58
- }
59
- if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
60
- // Bare optional truthiness: `if (e)` where e: T | undefined — true iff e is
61
- // present AND its inner value is truthy.
62
- if (cond.ty.kind === "optional") {
63
- const hint = binderHintFor(cond);
64
- if (hint === null)
65
- return null;
66
- return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: freshName(hint), truthiness: true };
67
- }
68
- return null;
69
- }
70
- // Explicit `e === undefined` / `e !== undefined` — a pure presence check,
71
- // independent of the inner value (so NOT a truthiness form).
72
- let e = null;
73
- if (cond.right.kind === "var" && cond.right.name === "undefined")
74
- e = cond.left;
75
- if (cond.left.kind === "var" && cond.left.name === "undefined")
76
- e = cond.right;
77
- if (!e || e.ty.kind !== "optional")
78
- return null;
79
- const hint = binderHintFor(e);
80
- if (hint === null)
81
- return null;
82
- return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: freshName(hint), truthiness: false };
83
- }
84
- function binderHintFor(e) {
85
- // Pure access paths: var(x) or field(purePath, name).
86
- // Walks down to the var root, collecting field names. Returns
87
- // `_root_field1_field2_..._val` (or `_root_val` for a bare var).
88
- const fields = [];
89
- let cur = e;
90
- while (cur.kind === "field") {
91
- fields.unshift(cur.field);
92
- cur = cur.obj;
93
- }
94
- if (cur.kind !== "var")
95
- return null;
96
- // \result is stored as the IR var name "\\result"; sanitize for a valid identifier.
97
- const root = cur.name === "\\result" ? "result" : cur.name;
98
- return fields.length === 0 ? `_${root}_val` : `_${root}_${fields.join("_")}_val`;
99
- }
100
- // Aliased for code that historically called the simpler check.
101
- 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";
102
40
  // ── Walkers ──────────────────────────────────────────────────
103
- function walkExpr(e) {
104
- const r = recurseExpr(e);
105
- 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;
106
44
  }
107
- function recurseExpr(e) {
108
- const re = walkExpr;
45
+ function recurseExpr(e, ctx) {
46
+ const re = (x) => walkExpr(x, ctx);
109
47
  switch (e.kind) {
110
48
  case "var":
111
49
  case "num":
50
+ case "bigint":
112
51
  case "str":
113
52
  case "bool":
114
53
  case "havoc":
@@ -121,7 +60,7 @@ function recurseExpr(e) {
121
60
  case "record": return { ...e, spread: e.spread ? re(e.spread) : null,
122
61
  fields: e.fields.map(f => ({ ...f, value: re(f.value) })) };
123
62
  case "arrayLiteral": return { ...e, elems: e.elems.map(re) };
124
- case "lambda": return { ...e, body: walkStmts(e.body) };
63
+ case "lambda": return { ...e, body: walkStmts(e.body, ctx) };
125
64
  case "conditional": return { ...e, cond: re(e.cond), then: re(e.then), else: re(e.else) };
126
65
  case "optChain": return { ...e, obj: re(e.obj),
127
66
  chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(re) }
@@ -136,9 +75,9 @@ function recurseExpr(e) {
136
75
  fallthrough: e.fallthrough ? re(e.fallthrough) : null };
137
76
  }
138
77
  }
139
- function walkStmt(s) {
78
+ function walkStmt(s, ctx) {
140
79
  // Recurse into children first, then try rules at this node.
141
- const r = recurseStmt(s);
80
+ const r = recurseStmt(s, ctx);
142
81
  // Optional narrowing fires before Array.isArray narrowing: in a chain like
143
82
  // `next && Array.isArray(next.content)` the optional check must unwrap `next`
144
83
  // *outside* the array match, since `next.content` is unreachable until then.
@@ -146,41 +85,41 @@ function walkStmt(s) {
146
85
  // array rule fires; independent narrows commute, so the order is harmless.)
147
86
  // && rules fire before the simple rule because they produce nested ifs whose
148
87
  // inner shape doesn't match the simple rule directly.
149
- 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;
150
89
  }
151
- function walkStmts(stmts) {
90
+ function walkStmts(stmts, ctx) {
152
91
  const result = [];
153
92
  for (let i = 0; i < stmts.length; i++) {
154
93
  const s = stmts[i];
155
94
  const rest = stmts.slice(i + 1);
156
95
  // Discriminant rules consume a prefix of stmts; remaining is processed normally.
157
- const tagged = ruleDiscriminantChain(stmts.slice(i)) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i));
96
+ const tagged = ruleDiscriminantChain(stmts.slice(i), ctx) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i), ctx);
158
97
  if (tagged) {
159
- result.push(walkStmt(tagged.stmt));
98
+ result.push(walkStmt(tagged.stmt, ctx));
160
99
  i += tagged.consumed - 1;
161
100
  continue;
162
101
  }
163
- 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);
164
103
  if (consumed) {
165
- result.push(walkStmt(consumed));
104
+ result.push(walkStmt(consumed, ctx));
166
105
  return result;
167
106
  }
168
107
  // walkStmt first — narrow's expression rules may rewrite the let init from
169
108
  // `conditional` to `someMatch`, in which case the let-cond desugar shouldn't fire.
170
- const walked = walkStmt(s);
109
+ const walked = walkStmt(s, ctx);
171
110
  const expanded = ruleLetCondAndOptional(walked);
172
111
  if (expanded) {
173
112
  for (const x of expanded)
174
- result.push(walkStmt(x));
113
+ result.push(walkStmt(x, ctx));
175
114
  continue;
176
115
  }
177
116
  result.push(walked);
178
117
  }
179
118
  return result;
180
119
  }
181
- function recurseStmt(s) {
182
- const re = walkExpr;
183
- const rs = walkStmts;
120
+ function recurseStmt(s, ctx) {
121
+ const re = (x) => walkExpr(x, ctx);
122
+ const rs = (x) => walkStmts(x, ctx);
184
123
  switch (s.kind) {
185
124
  case "let": return { ...s, init: re(s.init) };
186
125
  case "assign": return { ...s, value: re(s.value) };
@@ -211,40 +150,36 @@ function recurseStmt(s) {
211
150
  fallthrough: rs(s.fallthrough) };
212
151
  }
213
152
  }
214
- const canBeFalsy = (c) => c.truthiness && ["int", "nat", "string", "bool"].includes(c.innerTy.kind);
215
- const bound = (c) => ({ kind: "var", name: c.binderHint, ty: c.innerTy });
216
- // ── Rules ───────────────────────────────────────────────────
217
- /** Rule: `if (e !== undefined) then else` where e is a simple optional var or
218
- * `obj.field` chain, and the Some branch is non-empty.
219
- * → `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. */
220
162
  function ruleIfOptionalSimple(s) {
221
163
  if (s.kind !== "if")
222
164
  return null;
223
- const check = parseSimpleOptionalCheck(s.cond);
165
+ const check = presentFact(s.cond);
224
166
  if (!check)
225
167
  return null;
226
168
  const someBody = check.negated ? s.else : s.then;
227
169
  const noneBody = check.negated ? s.then : s.else;
228
170
  if (someBody.length === 0)
229
171
  return null;
230
- return {
231
- kind: "someMatch",
232
- scrutinee: check.scrutinee, binderTy: check.innerTy,
233
- binder: check.binderHint,
234
- someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: someBody, else: noneBody }] : someBody,
235
- noneBody,
236
- };
172
+ return presentMatchStmts(check, someBody, noneBody);
237
173
  }
238
- /** Rule: `if (e === undefined) terminate; rest` (early return / throw / break).
239
- * `someMatch e { Some(_e_val) => rest, None => terminate }`.
240
- * Fires when the Some branch is empty AND there's a non-empty rest of the
241
- * 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. */
242
177
  function ruleEarlyReturnConsume(s, rest) {
243
178
  if (s.kind !== "if")
244
179
  return null;
245
180
  if (rest.length === 0)
246
181
  return null;
247
- const check = parseSimpleOptionalCheck(s.cond);
182
+ const check = presentFact(s.cond);
248
183
  if (!check)
249
184
  return null;
250
185
  const someBranch = check.negated ? s.else : s.then;
@@ -253,58 +188,15 @@ function ruleEarlyReturnConsume(s, rest) {
253
188
  return null;
254
189
  if (!isTerminating(noneBranch))
255
190
  return null;
256
- return {
257
- kind: "someMatch",
258
- scrutinee: check.scrutinee, binderTy: check.innerTy,
259
- binder: check.binderHint,
260
- someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: rest, else: noneBranch }] : rest,
261
- noneBody: noneBranch,
262
- };
263
- }
264
- /** Flatten a nested `||` chain into its leaf conditions. */
265
- function flattenOr(e) {
266
- if (e.kind === "binop" && e.op === "||")
267
- return [...flattenOr(e.left), ...flattenOr(e.right)];
268
- return [e];
191
+ return presentMatchStmts(check, rest, noneBranch);
269
192
  }
270
- function classifyDisjunct(leaf) {
271
- // `x?.chain !== lit` `undefined !== lit` is true when x is None.
272
- if (leaf.kind === "binop" && leaf.op === "!==") {
273
- const oc = leaf.left.kind === "optChain" ? leaf.left : leaf.right.kind === "optChain" ? leaf.right : null;
274
- if (oc && oc.kind === "optChain" && oc.obj.ty.kind === "optional") {
275
- const hint = binderHintFor(oc.obj);
276
- if (hint === null)
277
- return null;
278
- const binder = freshName(hint);
279
- const unwrapped = applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain);
280
- if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
281
- const base = unwrapped.obj.ty.name.replace(/<.*/, "");
282
- const decl = _typeDecls.find(d => d.name === base);
283
- if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field)
284
- unwrapped.isDiscriminant = true;
285
- }
286
- const lit = leaf.left === oc ? leaf.right : leaf.left;
287
- return { scrutinee: oc.obj, innerTy: oc.obj.ty.inner, binder, residual: { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } } };
288
- }
289
- }
290
- // `!x` / `x === undefined`.
291
- const chk = parseOptionalCheck(leaf);
292
- if (chk && chk.negated) {
293
- const residual = canBeFalsy(chk)
294
- ? { kind: "unop", op: "!", expr: { kind: "var", name: chk.binderHint, ty: chk.innerTy }, ty: { kind: "bool" } }
295
- : null;
296
- return { scrutinee: chk.scrutinee, innerTy: chk.innerTy, binder: chk.binderHint, residual };
297
- }
298
- return null;
299
- }
300
- /** Rule: `if (D1 || … || Dn) terminate; rest`. Each `Di` that detects some optional
301
- * `x` is None (`!x`, `x === undefined`, `x?.chain !== lit`) narrows that `x` to Some
302
- * across `rest`; the rest — value guards reading a narrowed `x` directly, plus the
303
- * detectors' Some-case residuals — become a trailing early-return. Sound: reaching
304
- * `rest` means every disjunct was false, so every detected optional is present.
305
- * Covers `if (!x || x.f !== v) continue` / `if (x?.t !== 'm' || x.g) break`.
306
- * Closes the resolve.ts:602 TODO ("|| narrowing"). */
307
- 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) {
308
200
  if (s.kind !== "if")
309
201
  return null;
310
202
  if (rest.length === 0)
@@ -320,7 +212,7 @@ function ruleEarlyReturnOrChain(s, rest) {
320
212
  const residualLeaves = [];
321
213
  const seen = new Set();
322
214
  for (const leaf of leaves) {
323
- const d = classifyDisjunct(leaf);
215
+ const d = noneDetector(leaf, ctx);
324
216
  if (!d) {
325
217
  residualLeaves.push(leaf);
326
218
  continue;
@@ -344,17 +236,15 @@ function ruleEarlyReturnOrChain(s, rest) {
344
236
  }
345
237
  return inner[0];
346
238
  }
347
- /** Rule: `if (opt?.chain !== lit) terminate; rest` where `opt` is optional.
348
- * `opt?.chain` is `undefined` when `opt` is None, and `undefined !== lit` is
349
- * true, so the None case takes the terminating branch — falling through to
350
- * `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
351
243
  * someMatch opt { Some(v) => [if (v.chain !== lit) terminate; rest]; None => terminate }
352
- * narrowing `opt` to `v` across `rest` (transform substitutes the scrutinee) and
353
- * handing the now-non-optional inner guard to the ordinary rules (e.g.
354
- * discriminant narrowing). Bound-optional companion to ruleEarlyReturnConsume,
355
- * 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
356
246
  * `!==` so the None case is guaranteed to terminate. */
357
- function ruleEarlyReturnOptChainCompare(s, rest) {
247
+ function ruleEarlyReturnOptChainCompare(s, rest, ctx) {
358
248
  if (s.kind !== "if")
359
249
  return null;
360
250
  if (rest.length === 0)
@@ -375,16 +265,7 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
375
265
  const binder = freshName(hint);
376
266
  const binderVar = { kind: "var", name: binder, ty: innerTy };
377
267
  const unwrapped = applyChain(binderVar, oc.chain);
378
- // applyChain rebuilds the field without the `isDiscriminant` flag resolve sets
379
- // on a direct `x.disc`; restore it when the unwrapped access is the binder
380
- // union's discriminant, so the inner guard feeds discriminant narrowing.
381
- if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
382
- const base = unwrapped.obj.ty.name.replace(/<.*/, "");
383
- const decl = _typeDecls.find(d => d.name === base);
384
- if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field) {
385
- unwrapped.isDiscriminant = true;
386
- }
387
- }
268
+ restoreDiscriminantFlag(unwrapped, ctx.decls);
388
269
  const innerGuard = { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } };
389
270
  // Keep `rest` as trailing statements (not an else branch) — `s.then` terminates,
390
271
  // so `if (g) terminate; rest` ≡ `if (g) terminate else rest`, and the trailing
@@ -393,99 +274,37 @@ function ruleEarlyReturnOptChainCompare(s, rest) {
393
274
  const someBody = [{ kind: "if", cond: innerGuard, then: s.then, else: [] }, ...rest];
394
275
  return { kind: "someMatch", scrutinee: oc.obj, binder, binderTy: innerTy, someBody, noneBody: s.then };
395
276
  }
396
- /** Rule (expression): `e !== undefined ? a : b`. */
277
+ /** Driver (expression): `e !== undefined ? a : b` — presence fact in
278
+ * ternary position. */
397
279
  function ruleConditionalOptionalSimple(e) {
398
280
  if (e.kind !== "conditional")
399
281
  return null;
400
- const check = parseOptionalCheck(e.cond);
282
+ const check = presentFact(e.cond);
401
283
  if (!check)
402
284
  return null;
403
285
  const someBody = check.negated ? e.else : e.then;
404
286
  const noneBody = check.negated ? e.then : e.else;
405
- return {
406
- kind: "someMatch",
407
- scrutinee: check.scrutinee, binderTy: check.innerTy,
408
- binder: check.binderHint,
409
- someBody: canBeFalsy(check) ? { kind: "conditional", cond: bound(check), then: someBody, else: noneBody, ty: e.ty } : someBody,
410
- noneBody,
411
- ty: e.ty,
412
- };
413
- }
414
- /** Rule (expression): `Array.isArray(x) ==> B` or `!Array.isArray(x) ==> B` —
415
- * premise narrowing for spec implications. Mirrors `ruleImplOptional` but for
416
- * synth array-union discriminators.
417
- * → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch).
418
- * The other variant becomes a vacuous-true fallthrough (the implication is
419
- * trivially satisfied when the premise is false). */
420
- function ruleImplArrayIsArray(e) {
421
- if (e.kind !== "binop" || e.op !== "==>")
422
- return null;
423
- const pos = parseArrayIsArrayCall(e.left);
424
- const neg = e.left.kind === "unop" && e.left.op === "!"
425
- ? parseArrayIsArrayCall(e.left.expr)
426
- : null;
427
- const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
428
- if (!matched)
429
- return null;
430
- return {
431
- kind: "tagMatch",
432
- scrutinee: matched.scrutinee,
433
- typeName: matched.typeName,
434
- cases: [{ variant: matched.variant, body: walkExpr(e.right) }],
435
- fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } },
436
- ty: { kind: "bool" },
437
- };
287
+ return presentMatchExpr(check, someBody, noneBody, e.ty);
438
288
  }
439
- /** Rule (expression): `Array.isArray(x) ? a : b` — ternary narrowing for
440
- * synth array-unions. Mirrors `ruleImplArrayIsArray` but at the conditional
441
- * position rather than the `==>` position.
442
- * → `tagMatch x { ArrayBranch => walkExpr(a) } fallthrough walkExpr(b)`
443
- * (or NonArrayBranch when the condition is negated).
444
- * Inside the matched arm, bare references to `x` are rewritten to the
445
- * variant's payload field (e.g. `x.arr`) by `transformExpr` when emitting
446
- * the tagMatch — same mechanism `ruleImplArrayIsArray` already relies on. */
447
- function ruleConditionalArrayIsArray(e) {
448
- if (e.kind !== "conditional")
449
- return null;
450
- const pos = parseArrayIsArrayCall(e.cond);
451
- // `typeof x === "string"` is a positive check like `Array.isArray`, but selects
452
- // the NonArrayBranch — its then-branch is the matched-variant body.
453
- const tof = pos ? null : parseTypeofStringCheck(e.cond);
454
- const neg = !pos && !tof && e.cond.kind === "unop" && e.cond.op === "!"
455
- ? parseArrayIsArrayCall(e.cond.expr)
456
- : null;
457
- const matched = pos ?? tof ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
458
- if (!matched)
459
- return null;
460
- const positive = pos ?? tof;
461
- const thenBody = positive ? e.then : e.else;
462
- const elseBody = positive ? e.else : e.then;
463
- return {
464
- kind: "tagMatch",
465
- scrutinee: matched.scrutinee,
466
- typeName: matched.typeName,
467
- cases: [{ variant: matched.variant, body: walkExpr(thenBody) }],
468
- fallthrough: walkExpr(elseBody),
469
- ty: e.ty,
470
- };
471
- }
472
- /** Rule (expression): `(path !== undefined [&& rest]) ==> B` — premise narrowing
473
- * for spec implications (ensures/requires). The premise's optional checks
474
- * 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.
475
292
  * → `someMatch path { Some(_p_val) => (rest ==> B), None => true }`.
476
- * Walks the inner ==> recursively so chained checks (`a !== undefined && a.b !== undefined ==> ...`) become nested someMatches. */
477
- 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) {
478
297
  if (e.kind !== "binop" || e.op !== "==>")
479
298
  return null;
480
299
  let check;
481
300
  let restCond = null;
482
- const extracted = extractLeftmostOptionalCheck(e.left);
301
+ const extracted = leadingPresent(e.left);
483
302
  if (extracted) {
484
303
  check = extracted.check;
485
304
  restCond = extracted.restCond;
486
305
  }
487
306
  else {
488
- const c = parseSimpleOptionalCheck(e.left);
307
+ const c = presentFact(e.left);
489
308
  if (!c || c.negated)
490
309
  return null;
491
310
  check = c;
@@ -496,42 +315,23 @@ function ruleImplOptional(e) {
496
315
  return {
497
316
  kind: "someMatch",
498
317
  scrutinee: check.scrutinee, binderTy: check.innerTy,
499
- binder: check.binderHint,
500
- someBody: walkExpr(innerBody),
318
+ binder: check.binder,
319
+ someBody: walkExpr(innerBody, ctx),
501
320
  noneBody: { kind: "bool", value: true, ty: { kind: "bool" } },
502
321
  ty: { kind: "bool" },
503
322
  };
504
323
  }
505
- /** Apply an optional chain's steps (field / index / call) to a base expr —
506
- * shared by `ruleOptChain` (base = binder) and `ruleOptChainIndex` (base = arr[i]). */
507
- function applyChain(body, chain) {
508
- for (const step of chain) {
509
- if (step.kind === "field")
510
- body = { kind: "field", obj: body, field: step.name, ty: step.ty };
511
- else if (step.kind === "index")
512
- body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
513
- else
514
- body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
515
- }
516
- return body;
517
- }
518
- /** `0 <= idx && idx < arr.length` — the in-bounds guard for an array index. */
519
- function arrayBoundsCond(arr, idx) {
520
- const len = { kind: "field", obj: arr, field: "length", ty: { kind: "int" } };
521
- const lo = { kind: "binop", op: "<=", left: { kind: "num", value: 0, ty: { kind: "int" } }, right: idx, ty: { kind: "bool" } };
522
- const hi = { kind: "binop", op: "<", left: idx, right: len, ty: { kind: "bool" } };
523
- return { kind: "binop", op: "&&", left: lo, right: hi, ty: { kind: "bool" } };
524
- }
525
- /** Rule (expression): `left ?? right` — nullish coalescing.
324
+ /** Driver (expression): `left ?? right` nullish coalescing.
526
325
  * → `someMatch left { Some(_v) => _v, None => right }`.
527
- * Single-evaluation: scrutinee may be any expression. */
528
- 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) {
529
329
  if (e.kind !== "nullish")
530
330
  return null;
531
331
  if (e.left.ty.kind !== "optional")
532
332
  return null;
533
333
  const innerTy = e.left.ty.inner;
534
- const binder = freshName(`_oc${_ocCounter++}_val`);
334
+ const binder = freshOcBinder(ctx);
535
335
  return {
536
336
  kind: "someMatch",
537
337
  scrutinee: e.left, binder, binderTy: innerTy,
@@ -540,12 +340,12 @@ function ruleNullish(e) {
540
340
  ty: e.ty,
541
341
  };
542
342
  }
543
- /** Rule (expression): `arr[i] ?? right` — nullish coalescing on an array index.
544
- * Under noUncheckedIndexedAccess `arr[i]` is `T | undefined`, undefined exactly
545
- * when out of bounds, so → `(0 <= i && i < arr.length) ? arr[i] : right`. The
546
- * guarded `then` keeps the seq index in bounds for the backend. (Map index is
547
- * already optional-typed and handled by ruleNullish above; this is the array
548
- * 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.) */
549
349
  function ruleNullishIndex(e) {
550
350
  if (e.kind !== "nullish")
551
351
  return null;
@@ -556,14 +356,12 @@ function ruleNullishIndex(e) {
556
356
  const cond = arrayBoundsCond(e.left.obj, e.left.idx);
557
357
  return { kind: "conditional", cond, then: e.left, else: e.right, ty: e.ty };
558
358
  }
559
- /** Rule (expression): `arr[i]?.<chain>` — optional chaining on an array index,
560
- * the optChain sibling of ruleNullishIndex. `arr[i]` is `T | undefined`,
561
- * undefined exactly out of bounds, so → `(0 <= i && i < arr.length) ? <chain on
562
- * arr[i]> : undefined`. The conditional's optional type makes transform wrap the
563
- * in-bounds chain result in Some and the OOB branch in None the same Option<…>
564
- * a directly-optional scrutinee yields via ruleOptChain, just bounds-guarded.
565
- * (ruleOptChain itself bails here: an array index is typed as the non-optional
566
- * 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.) */
567
365
  function ruleOptChainIndex(e) {
568
366
  if (e.kind !== "optChain")
569
367
  return null;
@@ -576,15 +374,13 @@ function ruleOptChainIndex(e) {
576
374
  const undef = { kind: "var", name: "undefined", ty: { kind: "void" } };
577
375
  return { kind: "conditional", cond, then: body, else: undef, ty: e.ty };
578
376
  }
579
- /** Rule (statement): reconcile a `const e = arr[i]` whose binding is optional
580
- * (`e: T | undefined`) but whose array-index initializer is total (`T`). Model
581
- * the index as its JS semantics `e := (0 <= i && i < arr.length) ? arr[i] :
582
- * undefined`so `e` is a real `Option<T>` and a later `e?.f` someMatch is
583
- * well-typed; an in-bounds proof makes the None branch dead, so safely-indexed
584
- * code verifies as if total. Bound-form sibling of ruleOptChainIndex /
585
- * ruleNullishIndex. Fires purely on the optional-binding/total-index shape (the
586
- * usual source is `noUncheckedIndexedAccess`, but the flag itself is never
587
- * 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. */
588
384
  function ruleOptionalIndexBinding(s) {
589
385
  if (s.kind !== "let")
590
386
  return null;
@@ -602,17 +398,17 @@ function ruleOptionalIndexBinding(s) {
602
398
  const guarded = { kind: "conditional", cond, then: init, else: undef, ty: s.ty };
603
399
  return { ...s, init: guarded };
604
400
  }
605
- /** Rule (expression): `obj?.<chain>` — single-eval optional chain.
401
+ /** Driver (expression): `obj?.<chain>` — single-eval optional chain.
606
402
  * → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`.
607
403
  * The someBody applies the chain to the binder directly (field/call/index),
608
404
  * so transform doesn't substitute. Scrutinee can be any expression. */
609
- function ruleOptChain(e) {
405
+ function ruleOptChain(e, ctx) {
610
406
  if (e.kind !== "optChain")
611
407
  return null;
612
408
  if (e.obj.ty.kind !== "optional")
613
409
  return null;
614
410
  const innerTy = e.obj.ty.inner;
615
- const binder = freshName(`_oc${_ocCounter++}_val`);
411
+ const binder = freshOcBinder(ctx);
616
412
  const body = applyChain({ kind: "var", name: binder, ty: innerTy }, e.chain);
617
413
  const noneBody = { kind: "var", name: "undefined", ty: { kind: "void" } };
618
414
  return {
@@ -621,41 +417,12 @@ function ruleOptChain(e) {
621
417
  someBody: body, noneBody, ty: e.ty,
622
418
  };
623
419
  }
624
- /** Minimal structural equality on the IR shapes we narrow against: var, field
625
- * chain, and index (with pure key). Enough to recognize `m[k]` on both sides
626
- * of a `k in m ? m[k] : default` ternary. */
627
- function exprEqual(a, b) {
628
- if (a.kind !== b.kind)
629
- return false;
630
- if (a.kind === "var" && b.kind === "var")
631
- return a.name === b.name;
632
- if (a.kind === "field" && b.kind === "field")
633
- return a.field === b.field && exprEqual(a.obj, b.obj);
634
- if (a.kind === "index" && b.kind === "index")
635
- return exprEqual(a.obj, b.obj) && exprEqual(a.idx, b.idx);
636
- return false;
637
- }
638
- /** Produce a reader-friendly binder hint for `m[k]` when both m and k are
639
- * access-path shaped (var / field chain). Falls back to a generic counter
640
- * name for computed keys. */
641
- function binderHintForMapAccess(m, k) {
642
- const mHint = binderHintFor(m);
643
- const kHint = binderHintFor(k);
644
- if (mHint && kHint) {
645
- // mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`.
646
- const mStem = mHint.replace(/_val$/, "");
647
- const kStem = kHint.replace(/^_/, "").replace(/_val$/, "");
648
- return freshName(`${mStem}_${kStem}_val`);
649
- }
650
- return freshName(`_oc${_ocCounter++}_val`);
651
- }
652
- /** Rule (expression): `k in m ? m[k] : default` where m is map-typed.
653
- * The then-branch must be exactly `m[k]` (same obj, same key). This mirrors
654
- * 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]`.
655
422
  * → `someMatch m[k] { Some(_m_k_val) => _m_k_val, None => default }`.
656
423
  * The existing Dafny peephole collapses the result to
657
424
  * `if k in m then m[k] else default`. */
658
- function ruleConditionalInMap(e) {
425
+ function ruleConditionalInMap(e, ctx) {
659
426
  if (e.kind !== "conditional")
660
427
  return null;
661
428
  if (e.cond.kind !== "binop" || e.cond.op !== "in")
@@ -679,7 +446,7 @@ function ruleConditionalInMap(e) {
679
446
  if (e.then.ty.kind !== "optional")
680
447
  return null;
681
448
  const innerTy = m.ty.value;
682
- const binder = binderHintForMapAccess(m, k);
449
+ const binder = binderHintForMapAccess(m, k, ctx);
683
450
  return {
684
451
  kind: "someMatch",
685
452
  scrutinee: e.then, binder, binderTy: innerTy,
@@ -687,8 +454,9 @@ function ruleConditionalInMap(e) {
687
454
  noneBody: e.else, ty: innerTy,
688
455
  };
689
456
  }
690
- /** Rule (expression): `opt ? a : b` (truthiness — cond itself is optional).
691
- * 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). */
692
460
  function ruleConditionalOptionalTruthy(e) {
693
461
  if (e.kind !== "conditional")
694
462
  return null;
@@ -705,292 +473,45 @@ function ruleConditionalOptionalTruthy(e) {
705
473
  someBody: e.then, noneBody: e.else, ty: e.ty,
706
474
  };
707
475
  }
708
- /** Find the leftmost `parse`-matching conjunct anywhere in an `&&` chain,
709
- * returning it plus the remaining conjunction. Conjunct order doesn't carry
710
- * semantic weight, so either side is fine. Shared by the optional and
711
- * Array.isArray chain extractors — they differ only in `parse`.
712
- * `(x !== undefined && b) && c` → { check, restCond: b && c }. */
713
- function extractLeftmostCheck(cond, parse) {
714
- if (cond.kind !== "binop" || cond.op !== "&&")
715
- return null;
716
- const left = parse(cond.left);
717
- if (left)
718
- return { check: left, restCond: cond.right };
719
- const right = parse(cond.right);
720
- if (right)
721
- return { check: right, restCond: cond.left };
722
- if (cond.left.kind === "binop" && cond.left.op === "&&") {
723
- const inner = extractLeftmostCheck(cond.left, parse);
724
- if (inner)
725
- return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
726
- }
727
- if (cond.right.kind === "binop" && cond.right.op === "&&") {
728
- const inner = extractLeftmostCheck(cond.right, parse);
729
- if (inner)
730
- return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
731
- }
732
- return null;
733
- }
734
- /** `&&`-chain extractor for a positive optional check. */
735
- function extractLeftmostOptionalCheck(cond) {
736
- return extractLeftmostCheck(cond, e => {
737
- const c = parseSimpleOptionalCheck(e);
738
- return c && !c.negated ? c : null;
739
- });
740
- }
741
- /** Rule: `if (x !== undefined && rest) then` (no else) where x is a pure
742
- * access path.
476
+ /** Driver: `if (x !== undefined && rest) then` (no else) presence fact in
477
+ * guarded-if position.
743
478
  * → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`.
744
- * Walks the inner if back through narrow so that nested optional checks in rest
745
- * (`a !== undefined && a.b !== undefined && ...`) also become someMatches. */
746
- 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) {
747
482
  if (s.kind !== "if")
748
483
  return null;
749
484
  if (s.else.length !== 0)
750
485
  return null;
751
- const extracted = extractLeftmostOptionalCheck(s.cond);
486
+ const extracted = leadingPresent(s.cond);
752
487
  if (!extracted)
753
488
  return null;
754
489
  const { check, restCond } = extracted;
755
490
  const innerIf = { kind: "if", cond: restCond, then: s.then, else: [] };
756
- const someBody = canBeFalsy(check)
757
- ? [{ kind: "if", cond: bound(check), then: [walkStmt(innerIf)], else: [] }]
758
- : [walkStmt(innerIf)];
759
- return {
760
- kind: "someMatch",
761
- scrutinee: check.scrutinee, binderTy: check.innerTy,
762
- binder: check.binderHint,
763
- someBody,
764
- noneBody: [],
765
- };
491
+ return presentMatchStmts(check, [walkStmt(innerIf, ctx)], []);
766
492
  }
767
- /** Rule: a bare expression statement `x !== undefined && rest` (the `if`-less
768
- * guard idiom, TS-equivalent to `if (x !== undefined) rest;`) where `x` is a
769
- * 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;`).
770
495
  * → `someMatch x { Some(_x_val) => rest;, None => {} }`.
771
- * Runs `rest` for effect inside the narrowed scope. Wrapping `rest` as an
772
- * expr-statement and walking it lets chained checks
773
- * (`a !== undefined && a.b !== undefined && a.b.f()`) nest into someMatches.
774
- * Unlike the ternary rule (`ruleConditionalAndOptional`), a method call in
775
- * `rest` is fine here: a statement-level someMatch arm keeps it in statement
776
- * position, so transform never ANF-lifts it out of the arm (which would drop
777
- * the guard and reference the un-narrowed optional). */
778
- 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) {
779
501
  if (s.kind !== "expr")
780
502
  return null;
781
503
  if (s.expr.kind !== "binop" || s.expr.op !== "&&")
782
504
  return null;
783
- const extracted = extractLeftmostOptionalCheck(s.expr);
505
+ const extracted = leadingPresent(s.expr);
784
506
  if (!extracted)
785
507
  return null;
786
508
  const { check, restCond } = extracted;
787
509
  const innerStmt = { kind: "expr", expr: restCond };
788
- const someBody = canBeFalsy(check)
789
- ? [{ kind: "if", cond: bound(check), then: [walkStmt(innerStmt)], else: [] }]
790
- : [walkStmt(innerStmt)];
791
- return {
792
- kind: "someMatch",
793
- scrutinee: check.scrutinee, binderTy: check.innerTy,
794
- binder: check.binderHint,
795
- someBody,
796
- noneBody: [],
797
- };
798
- }
799
- // ── Discriminant narrowing ──────────────────────────────────
800
- /** Detect `Array.isArray(<path>)` where `<path>` is a var or a chain of
801
- * field accesses rooted at a var, and the path's type is a synthesized
802
- * array-union (discriminant `"__isArray__"`). Returns the variant name to
803
- * narrow to. The scrutinee is whatever path the user wrote — downstream
804
- * transforms substitute it inside the matched arm. */
805
- function parseArrayIsArrayCall(call) {
806
- if (call.kind !== "call")
807
- return null;
808
- if (call.fn.kind !== "field" || call.fn.field !== "isArray")
809
- return null;
810
- if (call.fn.obj.kind !== "var" || call.fn.obj.name !== "Array")
811
- return null;
812
- if (call.args.length !== 1)
813
- return null;
814
- const arg = call.args[0];
815
- if (!isNarrowablePath(arg) || arg.ty.kind !== "user")
816
- return null;
817
- const baseTyName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
818
- const decl = _typeDecls.find(d => d.name === baseTyName);
819
- if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
820
- return null;
821
- return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
822
- }
823
- /** Detect `typeof <path> === "string"` where `<path>`'s type is a synth array-
824
- * union (`U | T[]`) AND its `NonArrayBranch` payload `U` is itself `string`.
825
- * The runtime `=== "string"` test matches that branch only when `U` is string —
826
- * for any other non-array payload (`number | T[]`, …) it never holds, so we must
827
- * NOT narrow. Returns the `NonArrayBranch` variant; the dual of `Array.isArray`. */
828
- function parseTypeofStringCheck(e) {
829
- if (e.kind !== "binop" || e.op !== "===")
830
- return null;
831
- const tof = e.left.kind === "unop" && e.left.op === "typeof" ? e.left.expr
832
- : e.right.kind === "unop" && e.right.op === "typeof" ? e.right.expr : null;
833
- const lit = e.left.kind === "str" ? e.left.value : e.right.kind === "str" ? e.right.value : null;
834
- if (!tof || lit !== "string")
835
- return null;
836
- if (!isNarrowablePath(tof) || tof.ty.kind !== "user")
837
- return null;
838
- const baseTyName = tof.ty.name.includes("<") ? tof.ty.name.slice(0, tof.ty.name.indexOf("<")) : tof.ty.name;
839
- const decl = _typeDecls.find(d => d.name === baseTyName);
840
- if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
841
- return null;
842
- const valTy = decl.variants?.find(v => v.name === "NonArrayBranch")?.fields.find(f => f.name === "val")?.type;
843
- if (valTy?.kind !== "string")
844
- return null; // guard: the non-array branch must actually be `string`
845
- return { scrutinee: tof, typeName: tof.ty.name, variant: "NonArrayBranch" };
846
- }
847
- /** A "narrowable path" is a var or a chain of field accesses rooted at a var
848
- * — i.e., pure and structurally addressable, so transforms can substitute
849
- * occurrences inside a matched arm without worrying about re-evaluation. */
850
- function isNarrowablePath(e) {
851
- if (e.kind === "var")
852
- return true;
853
- if (e.kind === "field")
854
- return isNarrowablePath(e.obj);
855
- return false;
856
- }
857
- /** `&&`-chain extractor for `Array.isArray(path)` (positive form only — a negated
858
- * `!Array.isArray(...)` would narrow to the wrong variant for then-body consumers,
859
- * so those are left to the untouched-conditional path). */
860
- function extractLeftmostArrayIsArrayCheck(cond) {
861
- return extractLeftmostCheck(cond, parseArrayIsArrayCall);
862
- }
863
- /** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth
864
- * array-union) as a positive discriminant check. Returns the scrutinee var
865
- * (with its type), type name, and variant. */
866
- function parseDiscriminantCond(cond) {
867
- // Pattern: x.discriminant === "variant"
868
- if (cond.kind === "binop" && cond.op === "===" && cond.right.kind === "str" &&
869
- cond.left.kind === "field" && cond.left.isDiscriminant &&
870
- cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
871
- return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
872
- }
873
- // Pattern: 'key' in x — narrows x to the unique variant containing `key`.
874
- if (cond.kind === "binop" && cond.op === "in" &&
875
- cond.left.kind === "str" && cond.right.kind === "var" &&
876
- cond.right.ty.kind === "user") {
877
- const key = cond.left.value;
878
- const typeName = cond.right.ty.name;
879
- const baseTyName = typeName.includes("<") ? typeName.slice(0, typeName.indexOf("<")) : typeName;
880
- const decl = _typeDecls.find(d => d.name === baseTyName);
881
- if (decl?.kind === "discriminated-union" && decl.variants) {
882
- const matches = decl.variants.filter(v => v.fields.some(f => f.name === key));
883
- if (matches.length === 1) {
884
- return { scrutinee: cond.right, typeName, variant: matches[0].name };
885
- }
886
- }
887
- }
888
- // Pattern: Array.isArray(x) — narrows x to the ArrayBranch variant of a
889
- // synthesized array-union (discriminant "__isArray__"). Statement-level
890
- // discriminant chains (`if (Array.isArray(x)) {...} else if (...)`) still
891
- // require a bare-var scrutinee since the existing var-name-keyed
892
- // replacement machinery in transform.ts only handles that shape; path
893
- // scrutinees (e.g. `m.content`) are handled exclusively by
894
- // `ruleConditionalArrayIsArray` and the expression-form tagMatch path.
895
- const arrCheck = parseArrayIsArrayCall(cond);
896
- if (arrCheck && arrCheck.scrutinee.kind === "var") {
897
- return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: arrCheck.variant };
898
- }
899
- return null;
900
- }
901
- /** Detect `x.kind !== "variant"` (negative discriminant check) or
902
- * `!Array.isArray(x)` (synth array-union, narrows to NonArrayBranch). */
903
- function parseNegativeDiscriminantCond(cond) {
904
- if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" &&
905
- cond.left.kind === "field" && cond.left.isDiscriminant &&
906
- cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
907
- return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
908
- }
909
- // Pattern: !Array.isArray(x) — narrows x to the NonArrayBranch variant.
910
- // Same var-scrutinee restriction as parseDiscriminantCond.
911
- if (cond.kind === "unop" && cond.op === "!") {
912
- const arrCheck = parseArrayIsArrayCall(cond.expr);
913
- if (arrCheck && arrCheck.scrutinee.kind === "var") {
914
- return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: "NonArrayBranch" };
915
- }
916
- }
917
- return null;
918
- }
919
- function isTerminating(stmts) {
920
- if (stmts.length === 0)
921
- return false;
922
- return isTerminatorKind(stmts[stmts.length - 1].kind);
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 };
974
- }
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 };
510
+ return presentMatchStmts(check, [walkStmt(innerStmt, ctx)], []);
991
511
  }
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" },
1098
620
  };
1099
621
  }
1100
- /** Rule (statement): `if (<rest> && Array.isArray(path) && <more>) then [else]`
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,
650
+ };
651
+ }
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
  }