lemmascript 0.3.3 → 0.5.0

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.
@@ -0,0 +1,932 @@
1
+ /**
2
+ * narrow — Structural-narrowing rewrite pass.
3
+ *
4
+ * Pipeline: resolve → narrow → transform → emit.
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)
19
+ *
20
+ * Following TS semantics, narrowing rules only fire for pure access paths
21
+ * (`var(x)` or `field(purePath, name)`). Complex scrutinees (call results,
22
+ * index ops) require bind-first: `const v = m.get(k); if (v !== undefined) ...`.
23
+ * The `optChain` rule is the exception: narrow constructs the someBody to use the
24
+ * binder directly, so any scrutinee shape is allowed.
25
+ *
26
+ * Transform lowers `someMatch` to IR `match` Some/None, substituting the
27
+ * binder for path-shaped scrutinees via `replacePathInTExpr` /
28
+ * `replacePathInTStmts`. Resolve runs before narrow and handles type narrowing
29
+ * only (env extension, `narrowedPaths`). Narrow doesn't substitute on raw IR
30
+ * — the body keeps its original expressions until transform.
31
+ *
32
+ * Walker shape: bottom-up over TExpr/TStmt. At each node, recurse children
33
+ * via the *Recurse* helpers, then try the rules in order. List-level rules
34
+ * (early-return, let-cond) run in `walkStmts` so they can consume the rest
35
+ * of the block.
36
+ */
37
+ // ── Optional-check detection ────────────────────────────────
38
+ /** Counter for naming optChain binders. Reset per module. */
39
+ let _ocCounter = 0;
40
+ /** Type declarations for this module. Set in narrowModule, used by the
41
+ * discriminant-narrowing rules to resolve `'key' in x` to a variant. */
42
+ let _typeDecls = [];
43
+ /** Detect optional checks: `e !== undefined`, `e === undefined`, or `!e` for a
44
+ * pure-access-path optional-typed e. `!e` is equivalent to `=== undefined`.
45
+ * Following TS, only pure access paths narrow; complex scrutinees return null. */
46
+ function parseOptionalCheck(cond) {
47
+ // `!e` where e is optional — same as `e === undefined` (negated: true).
48
+ if (cond.kind === "unop" && cond.op === "!" && cond.expr.ty.kind === "optional") {
49
+ const e = cond.expr;
50
+ const innerTy = cond.expr.ty.inner;
51
+ const hint = binderHintFor(e);
52
+ if (hint === null)
53
+ return null;
54
+ return { scrutinee: e, innerTy, negated: true, binderHint: hint };
55
+ }
56
+ if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
57
+ // Bare optional truthiness: `if (e)` where e: T | undefined — same as `e !== undefined`.
58
+ if (cond.ty.kind === "optional") {
59
+ const hint = binderHintFor(cond);
60
+ if (hint === null)
61
+ return null;
62
+ return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: hint };
63
+ }
64
+ return null;
65
+ }
66
+ let e = null;
67
+ if (cond.right.kind === "var" && cond.right.name === "undefined")
68
+ e = cond.left;
69
+ if (cond.left.kind === "var" && cond.left.name === "undefined")
70
+ e = cond.right;
71
+ if (!e || e.ty.kind !== "optional")
72
+ return null;
73
+ const hint = binderHintFor(e);
74
+ if (hint === null)
75
+ return null;
76
+ return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: hint };
77
+ }
78
+ function binderHintFor(e) {
79
+ // Pure access paths: var(x) or field(purePath, name).
80
+ // Walks down to the var root, collecting field names. Returns
81
+ // `_root_field1_field2_..._val` (or `_root_val` for a bare var).
82
+ const fields = [];
83
+ let cur = e;
84
+ while (cur.kind === "field") {
85
+ fields.unshift(cur.field);
86
+ cur = cur.obj;
87
+ }
88
+ if (cur.kind !== "var")
89
+ return null;
90
+ // \result is stored as the IR var name "\\result"; sanitize for a valid identifier.
91
+ const root = cur.name === "\\result" ? "result" : cur.name;
92
+ return fields.length === 0 ? `_${root}_val` : `_${root}_${fields.join("_")}_val`;
93
+ }
94
+ // Aliased for code that historically called the simpler check.
95
+ const parseSimpleOptionalCheck = parseOptionalCheck;
96
+ // ── Walkers ──────────────────────────────────────────────────
97
+ function walkExpr(e) {
98
+ const r = recurseExpr(e);
99
+ return ruleNullish(r) ?? ruleOptChain(r) ?? ruleImplOptional(r) ?? ruleImplArrayIsArray(r) ?? ruleConditionalArrayIsArray(r) ?? ruleConditionalAndArrayIsArray(r) ?? ruleConditionalAndOptional(r) ?? ruleConditionalOptionalSimple(r) ?? ruleConditionalInMap(r) ?? ruleConditionalOptionalTruthy(r) ?? r;
100
+ }
101
+ function recurseExpr(e) {
102
+ const re = walkExpr;
103
+ switch (e.kind) {
104
+ case "var":
105
+ case "num":
106
+ case "str":
107
+ case "bool":
108
+ case "havoc":
109
+ return e;
110
+ case "binop": return { ...e, left: re(e.left), right: re(e.right) };
111
+ case "unop": return { ...e, expr: re(e.expr) };
112
+ case "call": return { ...e, fn: re(e.fn), args: e.args.map(re) };
113
+ case "index": return { ...e, obj: re(e.obj), idx: re(e.idx) };
114
+ case "field": return { ...e, obj: re(e.obj) };
115
+ case "record": return { ...e, spread: e.spread ? re(e.spread) : null,
116
+ fields: e.fields.map(f => ({ ...f, value: re(f.value) })) };
117
+ case "arrayLiteral": return { ...e, elems: e.elems.map(re) };
118
+ case "lambda": return { ...e, body: walkStmts(e.body) };
119
+ case "conditional": return { ...e, cond: re(e.cond), then: re(e.then), else: re(e.else) };
120
+ case "optChain": return { ...e, obj: re(e.obj),
121
+ chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(re) }
122
+ : s.kind === "index" ? { ...s, idx: re(s.idx) }
123
+ : s) };
124
+ case "nullish": return { ...e, left: re(e.left), right: re(e.right) };
125
+ case "forall": return { ...e, body: re(e.body) };
126
+ case "exists": return { ...e, body: re(e.body) };
127
+ case "someMatch": return { ...e, someBody: re(e.someBody), noneBody: re(e.noneBody) };
128
+ case "tagMatch": return { ...e, scrutinee: re(e.scrutinee),
129
+ cases: e.cases.map(c => ({ ...c, body: re(c.body) })),
130
+ fallthrough: e.fallthrough ? re(e.fallthrough) : null };
131
+ }
132
+ }
133
+ function walkStmt(s) {
134
+ // Recurse into children first, then try rules at this node.
135
+ const r = recurseStmt(s);
136
+ // Optional narrowing fires before Array.isArray narrowing: in a chain like
137
+ // `next && Array.isArray(next.content)` the optional check must unwrap `next`
138
+ // *outside* the array match, since `next.content` is unreachable until then.
139
+ // (When the chain has no leading optional, ruleIfAndOptional no-ops and the
140
+ // array rule fires; independent narrows commute, so the order is harmless.)
141
+ // && rules fire before the simple rule because they produce nested ifs whose
142
+ // inner shape doesn't match the simple rule directly.
143
+ return ruleIfAndOptional(r) ?? ruleIfAndArrayIsArray(r) ?? ruleIfOptionalSimple(r) ?? r;
144
+ }
145
+ function walkStmts(stmts) {
146
+ const result = [];
147
+ for (let i = 0; i < stmts.length; i++) {
148
+ const s = stmts[i];
149
+ const rest = stmts.slice(i + 1);
150
+ // Discriminant rules consume a prefix of stmts; remaining is processed normally.
151
+ const tagged = ruleDiscriminantChain(stmts.slice(i)) ?? ruleDiscriminantNegEarlyReturn(stmts.slice(i));
152
+ if (tagged) {
153
+ result.push(walkStmt(tagged.stmt));
154
+ i += tagged.consumed - 1;
155
+ continue;
156
+ }
157
+ const consumed = ruleEarlyReturnOrChain(s, rest) ?? ruleEarlyReturnConsume(s, rest);
158
+ if (consumed) {
159
+ result.push(walkStmt(consumed));
160
+ return result;
161
+ }
162
+ // walkStmt first — narrow's expression rules may rewrite the let init from
163
+ // `conditional` to `someMatch`, in which case the let-cond desugar shouldn't fire.
164
+ const walked = walkStmt(s);
165
+ const expanded = ruleLetCondAndOptional(walked);
166
+ if (expanded) {
167
+ for (const x of expanded)
168
+ result.push(walkStmt(x));
169
+ continue;
170
+ }
171
+ result.push(walked);
172
+ }
173
+ return result;
174
+ }
175
+ function recurseStmt(s) {
176
+ const re = walkExpr;
177
+ const rs = walkStmts;
178
+ switch (s.kind) {
179
+ case "let": return { ...s, init: re(s.init) };
180
+ case "assign": return { ...s, value: re(s.value) };
181
+ case "return": return { ...s, value: re(s.value) };
182
+ case "break":
183
+ case "continue":
184
+ case "throw": return s;
185
+ case "expr": return { ...s, expr: re(s.expr) };
186
+ case "if": return { ...s, cond: re(s.cond), then: rs(s.then), else: rs(s.else) };
187
+ case "while": return { ...s, cond: re(s.cond),
188
+ invariants: s.invariants.map(re),
189
+ decreases: s.decreases ? re(s.decreases) : null,
190
+ doneWith: s.doneWith ? re(s.doneWith) : null,
191
+ body: rs(s.body) };
192
+ case "switch": return { ...s, expr: re(s.expr),
193
+ cases: s.cases.map(c => ({ ...c, body: rs(c.body) })),
194
+ defaultBody: rs(s.defaultBody) };
195
+ case "forof": return { ...s, iterable: re(s.iterable),
196
+ invariants: s.invariants.map(re),
197
+ doneWith: s.doneWith ? re(s.doneWith) : null,
198
+ body: rs(s.body) };
199
+ case "ghostLet": return { ...s, init: re(s.init) };
200
+ case "ghostAssign": return { ...s, value: re(s.value) };
201
+ case "assert": return { ...s, expr: re(s.expr) };
202
+ case "someMatch": return { ...s, someBody: rs(s.someBody), noneBody: rs(s.noneBody) };
203
+ case "tagMatch": return { ...s, scrutinee: re(s.scrutinee),
204
+ cases: s.cases.map(c => ({ ...c, body: rs(c.body) })),
205
+ fallthrough: rs(s.fallthrough) };
206
+ }
207
+ }
208
+ // ── Rules ───────────────────────────────────────────────────
209
+ /** Rule: `if (e !== undefined) then else` where e is a simple optional var or
210
+ * `obj.field` chain, and the Some branch is non-empty.
211
+ * → `someMatch e { Some(_e_val) => then, None => else }`. */
212
+ function ruleIfOptionalSimple(s) {
213
+ if (s.kind !== "if")
214
+ return null;
215
+ const check = parseSimpleOptionalCheck(s.cond);
216
+ if (!check)
217
+ return null;
218
+ const someBody = check.negated ? s.else : s.then;
219
+ const noneBody = check.negated ? s.then : s.else;
220
+ if (someBody.length === 0)
221
+ return null;
222
+ return {
223
+ kind: "someMatch",
224
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
225
+ binder: check.binderHint,
226
+ someBody, noneBody,
227
+ };
228
+ }
229
+ /** Rule: `if (e === undefined) terminate; rest` (early return / throw / break).
230
+ * → `someMatch e { Some(_e_val) => rest, None => terminate }`.
231
+ * Fires when the Some branch is empty AND there's a non-empty rest of the
232
+ * block — pulling the continuation into the narrowed scope. */
233
+ function ruleEarlyReturnConsume(s, rest) {
234
+ if (s.kind !== "if")
235
+ return null;
236
+ if (rest.length === 0)
237
+ return null;
238
+ const check = parseSimpleOptionalCheck(s.cond);
239
+ if (!check)
240
+ return null;
241
+ const someBranch = check.negated ? s.else : s.then;
242
+ const noneBranch = check.negated ? s.then : s.else;
243
+ if (someBranch.length !== 0)
244
+ return null;
245
+ return {
246
+ kind: "someMatch",
247
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
248
+ binder: check.binderHint,
249
+ someBody: rest,
250
+ noneBody: noneBranch,
251
+ };
252
+ }
253
+ /** Collect a `||` chain of negative optional checks (`x === undefined`).
254
+ * Returns the list of checks if every leaf is a negative optional check; null otherwise. */
255
+ function collectOrChainOfNegativeChecks(cond) {
256
+ if (cond.kind === "binop" && cond.op === "||") {
257
+ const left = collectOrChainOfNegativeChecks(cond.left);
258
+ const right = collectOrChainOfNegativeChecks(cond.right);
259
+ if (!left || !right)
260
+ return null;
261
+ return [...left, ...right];
262
+ }
263
+ const check = parseSimpleOptionalCheck(cond);
264
+ if (!check || !check.negated)
265
+ return null;
266
+ return [check];
267
+ }
268
+ /** Rule: `if (x === undefined || y === undefined || ...) terminate; rest`.
269
+ * → nested someMatches narrowing each var in turn, each None branch = terminate,
270
+ * deepest someBody = rest.
271
+ * Closes the resolve.ts:602 TODO ("|| narrowing"). */
272
+ function ruleEarlyReturnOrChain(s, rest) {
273
+ if (s.kind !== "if")
274
+ return null;
275
+ if (rest.length === 0)
276
+ return null;
277
+ if (s.then.length === 0 || s.else.length !== 0)
278
+ return null;
279
+ if (s.cond.kind !== "binop" || s.cond.op !== "||")
280
+ return null; // single check is the simpler rule
281
+ const checks = collectOrChainOfNegativeChecks(s.cond);
282
+ if (!checks || checks.length < 2)
283
+ return null;
284
+ // Build nested someMatch from innermost outward
285
+ let inner = rest;
286
+ for (let i = checks.length - 1; i >= 0; i--) {
287
+ const check = checks[i];
288
+ inner = [{
289
+ kind: "someMatch",
290
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
291
+ binder: check.binderHint,
292
+ someBody: inner,
293
+ noneBody: s.then,
294
+ }];
295
+ }
296
+ return inner[0];
297
+ }
298
+ /** Rule (expression): `e !== undefined ? a : b`. */
299
+ function ruleConditionalOptionalSimple(e) {
300
+ if (e.kind !== "conditional")
301
+ return null;
302
+ const check = parseOptionalCheck(e.cond);
303
+ if (!check)
304
+ return null;
305
+ const someBody = check.negated ? e.else : e.then;
306
+ const noneBody = check.negated ? e.then : e.else;
307
+ return {
308
+ kind: "someMatch",
309
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
310
+ binder: check.binderHint,
311
+ someBody, noneBody,
312
+ ty: e.ty,
313
+ };
314
+ }
315
+ /** Rule (expression): `Array.isArray(x) ==> B` or `!Array.isArray(x) ==> B` —
316
+ * premise narrowing for spec implications. Mirrors `ruleImplOptional` but for
317
+ * synth array-union discriminators.
318
+ * → `tagMatch x { ArrayBranch => walkExpr(B), _ => true }` (or NonArrayBranch).
319
+ * The other variant becomes a vacuous-true fallthrough (the implication is
320
+ * trivially satisfied when the premise is false). */
321
+ function ruleImplArrayIsArray(e) {
322
+ if (e.kind !== "binop" || e.op !== "==>")
323
+ return null;
324
+ const pos = parseArrayIsArrayCall(e.left);
325
+ const neg = e.left.kind === "unop" && e.left.op === "!"
326
+ ? parseArrayIsArrayCall(e.left.expr)
327
+ : null;
328
+ const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
329
+ if (!matched)
330
+ return null;
331
+ return {
332
+ kind: "tagMatch",
333
+ scrutinee: matched.scrutinee,
334
+ typeName: matched.typeName,
335
+ cases: [{ variant: matched.variant, body: walkExpr(e.right) }],
336
+ fallthrough: { kind: "bool", value: true, ty: { kind: "bool" } },
337
+ ty: { kind: "bool" },
338
+ };
339
+ }
340
+ /** Rule (expression): `Array.isArray(x) ? a : b` — ternary narrowing for
341
+ * synth array-unions. Mirrors `ruleImplArrayIsArray` but at the conditional
342
+ * position rather than the `==>` position.
343
+ * → `tagMatch x { ArrayBranch => walkExpr(a) } fallthrough walkExpr(b)`
344
+ * (or NonArrayBranch when the condition is negated).
345
+ * Inside the matched arm, bare references to `x` are rewritten to the
346
+ * variant's payload field (e.g. `x.arr`) by `transformExpr` when emitting
347
+ * the tagMatch — same mechanism `ruleImplArrayIsArray` already relies on. */
348
+ function ruleConditionalArrayIsArray(e) {
349
+ if (e.kind !== "conditional")
350
+ return null;
351
+ const pos = parseArrayIsArrayCall(e.cond);
352
+ const neg = e.cond.kind === "unop" && e.cond.op === "!"
353
+ ? parseArrayIsArrayCall(e.cond.expr)
354
+ : null;
355
+ const matched = pos ?? (neg ? { scrutinee: neg.scrutinee, typeName: neg.typeName, variant: "NonArrayBranch" } : null);
356
+ if (!matched)
357
+ return null;
358
+ const thenBody = pos ? e.then : e.else;
359
+ const elseBody = pos ? e.else : e.then;
360
+ return {
361
+ kind: "tagMatch",
362
+ scrutinee: matched.scrutinee,
363
+ typeName: matched.typeName,
364
+ cases: [{ variant: matched.variant, body: walkExpr(thenBody) }],
365
+ fallthrough: walkExpr(elseBody),
366
+ ty: e.ty,
367
+ };
368
+ }
369
+ /** Rule (expression): `(path !== undefined [&& rest]) ==> B` — premise narrowing
370
+ * for spec implications (ensures/requires). The premise's optional checks
371
+ * bind narrowed values that the conclusion can use.
372
+ * → `someMatch path { Some(_p_val) => (rest ==> B), None => true }`.
373
+ * Walks the inner ==> recursively so chained checks (`a !== undefined && a.b !== undefined ==> ...`) become nested someMatches. */
374
+ function ruleImplOptional(e) {
375
+ if (e.kind !== "binop" || e.op !== "==>")
376
+ return null;
377
+ let check;
378
+ let restCond = null;
379
+ const extracted = extractLeftmostOptionalCheck(e.left);
380
+ if (extracted) {
381
+ check = extracted.check;
382
+ restCond = extracted.restCond;
383
+ }
384
+ else {
385
+ const c = parseSimpleOptionalCheck(e.left);
386
+ if (!c || c.negated)
387
+ return null;
388
+ check = c;
389
+ }
390
+ const innerBody = restCond
391
+ ? { kind: "binop", op: "==>", left: restCond, right: e.right, ty: { kind: "bool" } }
392
+ : e.right;
393
+ return {
394
+ kind: "someMatch",
395
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
396
+ binder: check.binderHint,
397
+ someBody: walkExpr(innerBody),
398
+ noneBody: { kind: "bool", value: true, ty: { kind: "bool" } },
399
+ ty: { kind: "bool" },
400
+ };
401
+ }
402
+ /** Rule (expression): `left ?? right` — nullish coalescing.
403
+ * → `someMatch left { Some(_v) => _v, None => right }`.
404
+ * Single-evaluation: scrutinee may be any expression. */
405
+ function ruleNullish(e) {
406
+ if (e.kind !== "nullish")
407
+ return null;
408
+ if (e.left.ty.kind !== "optional")
409
+ return null;
410
+ const innerTy = e.left.ty.inner;
411
+ const binder = `_oc${_ocCounter++}_val`;
412
+ return {
413
+ kind: "someMatch",
414
+ scrutinee: e.left, binder, binderTy: innerTy,
415
+ someBody: { kind: "var", name: binder, ty: innerTy },
416
+ noneBody: e.right,
417
+ ty: e.ty,
418
+ };
419
+ }
420
+ /** Rule (expression): `obj?.<chain>` — single-eval optional chain.
421
+ * → `someMatch obj { Some(_oc{N}_val) => apply(chain, _oc{N}_val), None => undefined }`.
422
+ * The someBody applies the chain to the binder directly (field/call/index),
423
+ * so transform doesn't substitute. Scrutinee can be any expression. */
424
+ function ruleOptChain(e) {
425
+ if (e.kind !== "optChain")
426
+ return null;
427
+ if (e.obj.ty.kind !== "optional")
428
+ return null;
429
+ const innerTy = e.obj.ty.inner;
430
+ const binder = `_oc${_ocCounter++}_val`;
431
+ let body = { kind: "var", name: binder, ty: innerTy };
432
+ for (const step of e.chain) {
433
+ if (step.kind === "field") {
434
+ body = { kind: "field", obj: body, field: step.name, ty: step.ty };
435
+ }
436
+ else if (step.kind === "index") {
437
+ body = { kind: "index", obj: body, idx: step.idx, ty: step.ty };
438
+ }
439
+ else {
440
+ body = { kind: "call", fn: body, args: step.args, ty: step.ty, callKind: step.callKind };
441
+ }
442
+ }
443
+ const noneBody = { kind: "var", name: "undefined", ty: { kind: "void" } };
444
+ return {
445
+ kind: "someMatch",
446
+ scrutinee: e.obj, binder, binderTy: innerTy,
447
+ someBody: body, noneBody, ty: e.ty,
448
+ };
449
+ }
450
+ /** Minimal structural equality on the IR shapes we narrow against: var, field
451
+ * chain, and index (with pure key). Enough to recognize `m[k]` on both sides
452
+ * of a `k in m ? m[k] : default` ternary. */
453
+ function exprEqual(a, b) {
454
+ if (a.kind !== b.kind)
455
+ return false;
456
+ if (a.kind === "var" && b.kind === "var")
457
+ return a.name === b.name;
458
+ if (a.kind === "field" && b.kind === "field")
459
+ return a.field === b.field && exprEqual(a.obj, b.obj);
460
+ if (a.kind === "index" && b.kind === "index")
461
+ return exprEqual(a.obj, b.obj) && exprEqual(a.idx, b.idx);
462
+ return false;
463
+ }
464
+ /** Produce a reader-friendly binder hint for `m[k]` when both m and k are
465
+ * access-path shaped (var / field chain). Falls back to a generic counter
466
+ * name for computed keys. */
467
+ function binderHintForMapAccess(m, k) {
468
+ const mHint = binderHintFor(m);
469
+ const kHint = binderHintFor(k);
470
+ if (mHint && kHint) {
471
+ // mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`.
472
+ const mStem = mHint.replace(/_val$/, "");
473
+ const kStem = kHint.replace(/^_/, "").replace(/_val$/, "");
474
+ return `${mStem}_${kStem}_val`;
475
+ }
476
+ return `_oc${_ocCounter++}_val`;
477
+ }
478
+ /** Rule (expression): `k in m ? m[k] : default` where m is map-typed.
479
+ * The then-branch must be exactly `m[k]` (same obj, same key). This mirrors
480
+ * the discriminant-`in` path (line 438) but gated on `map` instead of `user`.
481
+ * → `someMatch m[k] { Some(_m_k_val) => _m_k_val, None => default }`.
482
+ * The existing Dafny peephole collapses the result to
483
+ * `if k in m then m[k] else default`. */
484
+ function ruleConditionalInMap(e) {
485
+ if (e.kind !== "conditional")
486
+ return null;
487
+ if (e.cond.kind !== "binop" || e.cond.op !== "in")
488
+ return null;
489
+ const m = e.cond.right;
490
+ const k = e.cond.left;
491
+ if (m.ty.kind !== "map")
492
+ return null;
493
+ // Then-branch must be exactly m[k].
494
+ if (e.then.kind !== "index")
495
+ return null;
496
+ if (!exprEqual(e.then.obj, m) || !exprEqual(e.then.idx, k))
497
+ return null;
498
+ // Only narrow when the else branch has a concrete non-optional type — otherwise
499
+ // the overall ternary could legitimately be Option<V> (e.g., `k in m ? m[k] : undefined`).
500
+ if (e.else.ty.kind === "optional" || e.else.ty.kind === "void")
501
+ return null;
502
+ // Dormant backup: when resolve's in-atom narrowing has already fired, e.then.ty is V,
503
+ // not Option<V>. The `someMatch` scrutinee would then have the wrong shape. Skip —
504
+ // the enclosing expression is already a plain if-then-else of the correct type.
505
+ if (e.then.ty.kind !== "optional")
506
+ return null;
507
+ const innerTy = m.ty.value;
508
+ const binder = binderHintForMapAccess(m, k);
509
+ return {
510
+ kind: "someMatch",
511
+ scrutinee: e.then, binder, binderTy: innerTy,
512
+ someBody: { kind: "var", name: binder, ty: innerTy },
513
+ noneBody: e.else, ty: innerTy,
514
+ };
515
+ }
516
+ /** Rule (expression): `opt ? a : b` (truthiness — cond itself is optional).
517
+ * Only fires for simple var or simple `obj.field` cond. */
518
+ function ruleConditionalOptionalTruthy(e) {
519
+ if (e.kind !== "conditional")
520
+ return null;
521
+ if (e.cond.ty.kind !== "optional")
522
+ return null;
523
+ const binder = binderHintFor(e.cond);
524
+ if (binder === null)
525
+ return null;
526
+ return {
527
+ kind: "someMatch",
528
+ scrutinee: e.cond, binderTy: e.cond.ty.inner,
529
+ binder,
530
+ someBody: e.then, noneBody: e.else, ty: e.ty,
531
+ };
532
+ }
533
+ /** Extract an optional check from any position in an `&&` chain.
534
+ * `(x !== undefined && b) && c` → { check, restCond: b && c }.
535
+ * `a && (x !== undefined)` → { check, restCond: a }.
536
+ * Conjunct order doesn't carry semantic weight, so either side is fine. */
537
+ function extractLeftmostOptionalCheck(cond) {
538
+ if (cond.kind !== "binop" || cond.op !== "&&")
539
+ return null;
540
+ const leftCheck = parseSimpleOptionalCheck(cond.left);
541
+ if (leftCheck && !leftCheck.negated)
542
+ return { check: leftCheck, restCond: cond.right };
543
+ const rightCheck = parseSimpleOptionalCheck(cond.right);
544
+ if (rightCheck && !rightCheck.negated)
545
+ return { check: rightCheck, restCond: cond.left };
546
+ if (cond.left.kind === "binop" && cond.left.op === "&&") {
547
+ const inner = extractLeftmostOptionalCheck(cond.left);
548
+ if (inner)
549
+ return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
550
+ }
551
+ if (cond.right.kind === "binop" && cond.right.op === "&&") {
552
+ const inner = extractLeftmostOptionalCheck(cond.right);
553
+ if (inner)
554
+ return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
555
+ }
556
+ return null;
557
+ }
558
+ /** Rule: `if (x !== undefined && rest) then` (no else) where x is a pure
559
+ * access path.
560
+ * → `someMatch x { Some(_x_val) => if rest then then; , None => {} }`.
561
+ * Walks the inner if back through narrow so that nested optional checks in rest
562
+ * (`a !== undefined && a.b !== undefined && ...`) also become someMatches. */
563
+ function ruleIfAndOptional(s) {
564
+ if (s.kind !== "if")
565
+ return null;
566
+ if (s.else.length !== 0)
567
+ return null;
568
+ const extracted = extractLeftmostOptionalCheck(s.cond);
569
+ if (!extracted)
570
+ return null;
571
+ const { check, restCond } = extracted;
572
+ const innerIf = { kind: "if", cond: restCond, then: s.then, else: [] };
573
+ return {
574
+ kind: "someMatch",
575
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
576
+ binder: check.binderHint,
577
+ someBody: [walkStmt(innerIf)],
578
+ noneBody: [],
579
+ };
580
+ }
581
+ // ── Discriminant narrowing ──────────────────────────────────
582
+ /** Detect `Array.isArray(<path>)` where `<path>` is a var or a chain of
583
+ * field accesses rooted at a var, and the path's type is a synthesized
584
+ * array-union (discriminant `"__isArray__"`). Returns the variant name to
585
+ * narrow to. The scrutinee is whatever path the user wrote — downstream
586
+ * transforms substitute it inside the matched arm. */
587
+ function parseArrayIsArrayCall(call) {
588
+ if (call.kind !== "call")
589
+ return null;
590
+ if (call.fn.kind !== "field" || call.fn.field !== "isArray")
591
+ return null;
592
+ if (call.fn.obj.kind !== "var" || call.fn.obj.name !== "Array")
593
+ return null;
594
+ if (call.args.length !== 1)
595
+ return null;
596
+ const arg = call.args[0];
597
+ if (!isNarrowablePath(arg) || arg.ty.kind !== "user")
598
+ return null;
599
+ const baseTyName = arg.ty.name.includes("<") ? arg.ty.name.slice(0, arg.ty.name.indexOf("<")) : arg.ty.name;
600
+ const decl = _typeDecls.find(d => d.name === baseTyName);
601
+ if (decl?.kind !== "discriminated-union" || decl.discriminant !== "__isArray__")
602
+ return null;
603
+ return { scrutinee: arg, typeName: arg.ty.name, variant: "ArrayBranch" };
604
+ }
605
+ /** A "narrowable path" is a var or a chain of field accesses rooted at a var
606
+ * — i.e., pure and structurally addressable, so transforms can substitute
607
+ * occurrences inside a matched arm without worrying about re-evaluation. */
608
+ function isNarrowablePath(e) {
609
+ if (e.kind === "var")
610
+ return true;
611
+ if (e.kind === "field")
612
+ return isNarrowablePath(e.obj);
613
+ return false;
614
+ }
615
+ /** Mirror of `extractLeftmostOptionalCheck` for synth-array-union checks:
616
+ * finds `Array.isArray(path)` somewhere in a `&&` chain, returns it plus
617
+ * the remaining conjunction. The check must be the positive form (negated
618
+ * `!Array.isArray(...)` would narrow to the wrong variant for then-body
619
+ * consumers, so we leave those to the existing untouched-conditional path). */
620
+ function extractLeftmostArrayIsArrayCheck(cond) {
621
+ if (cond.kind !== "binop" || cond.op !== "&&")
622
+ return null;
623
+ const leftCheck = parseArrayIsArrayCall(cond.left);
624
+ if (leftCheck)
625
+ return { check: leftCheck, restCond: cond.right };
626
+ const rightCheck = parseArrayIsArrayCall(cond.right);
627
+ if (rightCheck)
628
+ return { check: rightCheck, restCond: cond.left };
629
+ if (cond.left.kind === "binop" && cond.left.op === "&&") {
630
+ const inner = extractLeftmostArrayIsArrayCheck(cond.left);
631
+ if (inner)
632
+ return { check: inner.check, restCond: { ...cond, left: inner.restCond } };
633
+ }
634
+ if (cond.right.kind === "binop" && cond.right.op === "&&") {
635
+ const inner = extractLeftmostArrayIsArrayCheck(cond.right);
636
+ if (inner)
637
+ return { check: inner.check, restCond: { ...cond, right: inner.restCond } };
638
+ }
639
+ return null;
640
+ }
641
+ /** Detect `x.kind === "variant"`, `'key' in x`, or `Array.isArray(x)` (synth
642
+ * array-union) as a positive discriminant check. Returns the scrutinee var
643
+ * (with its type), type name, and variant. */
644
+ function parseDiscriminantCond(cond) {
645
+ // Pattern: x.discriminant === "variant"
646
+ if (cond.kind === "binop" && cond.op === "===" && cond.right.kind === "str" &&
647
+ cond.left.kind === "field" && cond.left.isDiscriminant &&
648
+ cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
649
+ return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
650
+ }
651
+ // Pattern: 'key' in x — narrows x to the unique variant containing `key`.
652
+ if (cond.kind === "binop" && cond.op === "in" &&
653
+ cond.left.kind === "str" && cond.right.kind === "var" &&
654
+ cond.right.ty.kind === "user") {
655
+ const key = cond.left.value;
656
+ const typeName = cond.right.ty.name;
657
+ const baseTyName = typeName.includes("<") ? typeName.slice(0, typeName.indexOf("<")) : typeName;
658
+ const decl = _typeDecls.find(d => d.name === baseTyName);
659
+ if (decl?.kind === "discriminated-union" && decl.variants) {
660
+ const matches = decl.variants.filter(v => v.fields.some(f => f.name === key));
661
+ if (matches.length === 1) {
662
+ return { scrutinee: cond.right, typeName, variant: matches[0].name };
663
+ }
664
+ }
665
+ }
666
+ // Pattern: Array.isArray(x) — narrows x to the ArrayBranch variant of a
667
+ // synthesized array-union (discriminant "__isArray__"). Statement-level
668
+ // discriminant chains (`if (Array.isArray(x)) {...} else if (...)`) still
669
+ // require a bare-var scrutinee since the existing var-name-keyed
670
+ // replacement machinery in transform.ts only handles that shape; path
671
+ // scrutinees (e.g. `m.content`) are handled exclusively by
672
+ // `ruleConditionalArrayIsArray` and the expression-form tagMatch path.
673
+ const arrCheck = parseArrayIsArrayCall(cond);
674
+ if (arrCheck && arrCheck.scrutinee.kind === "var") {
675
+ return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: arrCheck.variant };
676
+ }
677
+ return null;
678
+ }
679
+ /** Detect `x.kind !== "variant"` (negative discriminant check) or
680
+ * `!Array.isArray(x)` (synth array-union, narrows to NonArrayBranch). */
681
+ function parseNegativeDiscriminantCond(cond) {
682
+ if (cond.kind === "binop" && cond.op === "!==" && cond.right.kind === "str" &&
683
+ cond.left.kind === "field" && cond.left.isDiscriminant &&
684
+ cond.left.obj.kind === "var" && cond.left.obj.ty.kind === "user") {
685
+ return { scrutinee: cond.left.obj, typeName: cond.left.obj.ty.name, variant: cond.right.value };
686
+ }
687
+ // Pattern: !Array.isArray(x) — narrows x to the NonArrayBranch variant.
688
+ // Same var-scrutinee restriction as parseDiscriminantCond.
689
+ if (cond.kind === "unop" && cond.op === "!") {
690
+ const arrCheck = parseArrayIsArrayCall(cond.expr);
691
+ if (arrCheck && arrCheck.scrutinee.kind === "var") {
692
+ return { scrutinee: arrCheck.scrutinee, typeName: arrCheck.typeName, variant: "NonArrayBranch" };
693
+ }
694
+ }
695
+ return null;
696
+ }
697
+ function isTerminating(stmts) {
698
+ if (stmts.length === 0)
699
+ return false;
700
+ const last = stmts[stmts.length - 1];
701
+ return last.kind === "return" || last.kind === "throw" || last.kind === "break" || last.kind === "continue";
702
+ }
703
+ /** Rule (list-level): consecutive `if (x.kind === "v") ...` chain → tagMatch.
704
+ * Walks consecutive top-level ifs on the same discriminator var; the first
705
+ * one with an else-branch ends the chain (else becomes fallthrough; if-else-if
706
+ * flattens into more cases). Returns the tagMatch and how many stmts consumed. */
707
+ function ruleDiscriminantChain(stmts) {
708
+ if (stmts.length === 0 || stmts[0].kind !== "if")
709
+ return null;
710
+ const first = parseDiscriminantCond(stmts[0].cond);
711
+ if (!first)
712
+ return null;
713
+ const cases = [];
714
+ function collectElse(s) {
715
+ const p = parseDiscriminantCond(s.cond);
716
+ if (!p || p.scrutinee.name !== first.scrutinee.name)
717
+ return [s];
718
+ cases.push({ variant: p.variant, body: s.then });
719
+ if (s.else.length === 0)
720
+ return [];
721
+ if (s.else.length === 1 && s.else[0].kind === "if")
722
+ return collectElse(s.else[0]);
723
+ return s.else;
724
+ }
725
+ let consumed = 0;
726
+ for (let i = 0; i < stmts.length; i++) {
727
+ const s = stmts[i];
728
+ if (s.kind !== "if")
729
+ break;
730
+ const p = parseDiscriminantCond(s.cond);
731
+ if (!p || p.scrutinee.name !== first.scrutinee.name)
732
+ break;
733
+ cases.push({ variant: p.variant, body: s.then });
734
+ consumed = i + 1;
735
+ if (s.else.length > 0) {
736
+ const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
737
+ return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
738
+ cases, fallthrough: ft }, consumed };
739
+ }
740
+ }
741
+ if (cases.length === 0)
742
+ return null;
743
+ return { stmt: { kind: "tagMatch", scrutinee: first.scrutinee, typeName: first.typeName,
744
+ cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
745
+ }
746
+ /** Rule (list-level): `if (x.kind !== "v") terminate; rest` → tagMatch
747
+ * with cases = [{ variant: v, body: rest }] and fallthrough = terminate. */
748
+ function ruleDiscriminantNegEarlyReturn(stmts) {
749
+ if (stmts.length < 2)
750
+ return null;
751
+ const first = stmts[0];
752
+ if (first.kind !== "if" || first.else.length > 0)
753
+ return null;
754
+ if (!isTerminating(first.then))
755
+ return null;
756
+ const cond = parseNegativeDiscriminantCond(first.cond);
757
+ if (!cond)
758
+ return null;
759
+ return { stmt: { kind: "tagMatch", scrutinee: cond.scrutinee, typeName: cond.typeName,
760
+ cases: [{ variant: cond.variant, body: stmts.slice(1) }], fallthrough: first.then },
761
+ consumed: stmts.length };
762
+ }
763
+ /** Rule (statement): `let x = (e_opt && rest) ? a : b` where rest may contain
764
+ * method calls. → `var x: T := b; someMatch e_opt { Some(_v) => { if rest { x := a } } }`.
765
+ * Statement-level form is needed because Dafny doesn't allow method calls
766
+ * inside match expression arms. */
767
+ function ruleLetCondAndOptional(s) {
768
+ if (s.kind !== "let" || s.mutable)
769
+ return null;
770
+ if (s.init.kind !== "conditional")
771
+ return null;
772
+ const extracted = extractLeftmostOptionalCheck(s.init.cond);
773
+ if (!extracted)
774
+ return null;
775
+ const { check, restCond } = extracted;
776
+ const sm = {
777
+ kind: "someMatch",
778
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
779
+ binder: check.binderHint,
780
+ someBody: [{ kind: "if", cond: restCond,
781
+ then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] }],
782
+ noneBody: [],
783
+ };
784
+ return [
785
+ { kind: "let", name: s.name, ty: s.ty, mutable: true, init: s.init.else },
786
+ sm,
787
+ ];
788
+ }
789
+ /** Built-in collection methods that lower to pure Dafny expressions
790
+ * (`x in arr`, `x in m`, `x in s`, `|s|`, `s.Keys`, etc.) even though they
791
+ * carry `callKind: "method"` from resolve. Safe inside match arms. */
792
+ const PURE_BUILTIN_METHODS = new Set([
793
+ "includes", "has", "size", "length", "keys", "values",
794
+ ]);
795
+ /** Does this expression contain a method call that would be lifted to a
796
+ * var binding outside its containing expression by transform? Such calls
797
+ * are unsafe inside a match arm — the lifted binding would reference a
798
+ * name only valid in the arm. Built-in pure methods are exempt. */
799
+ function containsMethodCall(e) {
800
+ if (e.kind === "call" && e.callKind === "method" &&
801
+ !(e.fn.kind === "field" && PURE_BUILTIN_METHODS.has(e.fn.field))) {
802
+ return true;
803
+ }
804
+ switch (e.kind) {
805
+ case "var":
806
+ case "num":
807
+ case "str":
808
+ case "bool":
809
+ case "havoc":
810
+ return false;
811
+ case "binop": return containsMethodCall(e.left) || containsMethodCall(e.right);
812
+ case "unop": return containsMethodCall(e.expr);
813
+ case "call": return containsMethodCall(e.fn) || e.args.some(containsMethodCall);
814
+ case "index": return containsMethodCall(e.obj) || containsMethodCall(e.idx);
815
+ case "field": return containsMethodCall(e.obj);
816
+ case "record":
817
+ return (e.spread ? containsMethodCall(e.spread) : false) ||
818
+ e.fields.some(f => containsMethodCall(f.value));
819
+ case "arrayLiteral": return e.elems.some(containsMethodCall);
820
+ case "lambda": return false; // body is its own scope
821
+ case "conditional":
822
+ return containsMethodCall(e.cond) || containsMethodCall(e.then) || containsMethodCall(e.else);
823
+ case "optChain": return containsMethodCall(e.obj);
824
+ case "nullish": return containsMethodCall(e.left) || containsMethodCall(e.right);
825
+ case "forall":
826
+ case "exists": return containsMethodCall(e.body);
827
+ case "someMatch": return containsMethodCall(e.scrutinee) ||
828
+ containsMethodCall(e.someBody) || containsMethodCall(e.noneBody);
829
+ case "tagMatch": return containsMethodCall(e.scrutinee) ||
830
+ e.cases.some(c => containsMethodCall(c.body)) ||
831
+ (e.fallthrough ? containsMethodCall(e.fallthrough) : false);
832
+ }
833
+ }
834
+ /** Rule (expression): `x !== undefined && rest ? a : b`.
835
+ * → `someMatch x { Some(_x_val) => if rest then a else b, None => b }`.
836
+ * Walks the inner conditional back through narrow so chained checks
837
+ * (`a !== undefined && a.b !== undefined ? ... : ...`) become nested
838
+ * someMatches rather than leaving inner optional checks as raw conditionals.
839
+ * Does NOT fire if the guard `rest` contains method calls — transform lifts
840
+ * those out of the match arm, breaking the binder scope. The original
841
+ * transform's let-desugar (transformStmt let-case) handles those by lifting
842
+ * to a mutable var first. */
843
+ function ruleConditionalAndOptional(e) {
844
+ if (e.kind !== "conditional")
845
+ return null;
846
+ const extracted = extractLeftmostOptionalCheck(e.cond);
847
+ if (!extracted)
848
+ return null;
849
+ const { check, restCond } = extracted;
850
+ if (containsMethodCall(restCond))
851
+ return null;
852
+ const innerCond = {
853
+ kind: "conditional",
854
+ cond: restCond, then: e.then, else: e.else, ty: e.ty,
855
+ };
856
+ return {
857
+ kind: "someMatch",
858
+ scrutinee: check.scrutinee, binderTy: check.innerTy,
859
+ binder: check.binderHint,
860
+ someBody: walkExpr(innerCond), noneBody: e.else, ty: e.ty,
861
+ };
862
+ }
863
+ /** Rule (statement): `if (<rest> && Array.isArray(path) && <more>) then [else]`
864
+ * → `tagMatch path { ArrayBranch => if (<rest && more>) then [else] }`.
865
+ * The remaining conjuncts move inside the matched arm so any narrowing the
866
+ * `then` body relies on (typed `path` accesses) sees the unwrapped variant.
867
+ * Mirrors `ruleIfAndOptional` but for synth array-unions. */
868
+ function ruleIfAndArrayIsArray(s) {
869
+ if (s.kind !== "if")
870
+ return null;
871
+ const extracted = extractLeftmostArrayIsArrayCheck(s.cond);
872
+ if (!extracted)
873
+ return null;
874
+ const { check, restCond } = extracted;
875
+ // Inner if uses the remaining conjunction (or just the then-body if rest is
876
+ // a tautology — but in practice extractLeftmost leaves at least one other
877
+ // conjunct). Walk recursively so nested checks compose.
878
+ const innerThen = [{ kind: "if", cond: restCond, then: s.then, else: s.else }];
879
+ return {
880
+ kind: "tagMatch",
881
+ scrutinee: check.scrutinee,
882
+ typeName: check.typeName,
883
+ cases: [{ variant: check.variant, body: innerThen.map(walkStmt) }],
884
+ fallthrough: s.else,
885
+ };
886
+ }
887
+ /** Rule (expression): `(<rest> && Array.isArray(path)) ? a : b`
888
+ * → `tagMatch path { ArrayBranch => (<rest>) ? a : b } fallthrough b`.
889
+ * Mirrors `ruleConditionalAndOptional`. */
890
+ function ruleConditionalAndArrayIsArray(e) {
891
+ if (e.kind !== "conditional")
892
+ return null;
893
+ const extracted = extractLeftmostArrayIsArrayCheck(e.cond);
894
+ if (!extracted)
895
+ return null;
896
+ const { check, restCond } = extracted;
897
+ const innerCond = {
898
+ kind: "conditional",
899
+ cond: restCond, then: e.then, else: e.else, ty: e.ty,
900
+ };
901
+ return {
902
+ kind: "tagMatch",
903
+ scrutinee: check.scrutinee,
904
+ typeName: check.typeName,
905
+ cases: [{ variant: check.variant, body: walkExpr(innerCond) }],
906
+ fallthrough: e.else,
907
+ ty: e.ty,
908
+ };
909
+ }
910
+ // ── Function / module entry ──────────────────────────────────
911
+ function narrowFunction(fn) {
912
+ return {
913
+ ...fn,
914
+ requires: fn.requires.map(e => walkExpr(e)),
915
+ ensures: fn.ensures.map(e => walkExpr(e)),
916
+ decreases: fn.decreases ? walkExpr(fn.decreases) : null,
917
+ body: walkStmts(fn.body),
918
+ };
919
+ }
920
+ export function narrowModule(mod) {
921
+ _ocCounter = 0;
922
+ _typeDecls = mod.typeDecls;
923
+ return {
924
+ ...mod,
925
+ constants: mod.constants.map(c => ({ ...c, value: walkExpr(c.value) })),
926
+ functions: mod.functions.map(narrowFunction),
927
+ classes: mod.classes.map(cls => ({
928
+ ...cls,
929
+ methods: cls.methods.map(narrowFunction),
930
+ })),
931
+ };
932
+ }