lemmascript 0.3.3 → 0.4.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.
@@ -21,10 +21,10 @@ function mapExpr(e, f) {
21
21
  case "num":
22
22
  case "bool":
23
23
  case "str":
24
- case "constructor":
25
24
  case "emptyMap":
26
25
  case "emptySet":
27
26
  case "havoc": return e;
27
+ case "constructor": return e.args ? { ...e, args: e.args.map(r) } : e;
28
28
  case "binop": return { ...e, left: r(e.left), right: r(e.right) };
29
29
  case "unop": return { ...e, expr: r(e.expr) };
30
30
  case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) };
@@ -90,6 +90,15 @@ function mapTExpr(e, f) {
90
90
  case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
91
91
  case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
92
92
  case "conditional": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
93
+ case "optChain": return { ...e, obj: r(e.obj),
94
+ chain: e.chain.map(s => s.kind === "call" ? { ...s, args: s.args.map(r) }
95
+ : s.kind === "index" ? { ...s, idx: r(s.idx) }
96
+ : s) };
97
+ case "nullish": return { ...e, left: r(e.left), right: r(e.right) };
98
+ case "someMatch": return { ...e, scrutinee: r(e.scrutinee), someBody: r(e.someBody), noneBody: r(e.noneBody) };
99
+ case "tagMatch": return { ...e, scrutinee: r(e.scrutinee),
100
+ cases: e.cases.map(c => ({ ...c, body: r(c.body) })),
101
+ fallthrough: e.fallthrough ? r(e.fallthrough) : null };
93
102
  case "forall": return { ...e, body: r(e.body) };
94
103
  case "exists": return { ...e, body: r(e.body) };
95
104
  case "lambda": return e;
@@ -113,6 +122,10 @@ function mapTStmt(s, f) {
113
122
  case "ghostLet": return { ...s, init: r(s.init) };
114
123
  case "ghostAssign": return { ...s, value: r(s.value) };
115
124
  case "assert": return { ...s, expr: r(s.expr) };
125
+ case "someMatch": return { ...s, scrutinee: r(s.scrutinee), someBody: s.someBody.map(t => mapTStmt(t, f)), noneBody: s.noneBody.map(t => mapTStmt(t, f)) };
126
+ case "tagMatch": return { ...s, scrutinee: r(s.scrutinee),
127
+ cases: s.cases.map(c => ({ ...c, body: c.body.map(t => mapTStmt(t, f)) })),
128
+ fallthrough: s.fallthrough.map(t => mapTStmt(t, f)) };
116
129
  }
117
130
  }
118
131
  export const LEAN_OPTIONS = {
@@ -186,18 +199,26 @@ function transformExpr(e) { return lowerExpr(e, null); }
186
199
  /** Wrap an expression in Some/None for optional-typed conditionals.
187
200
  * If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
188
201
  function wrapOptionalBranch(expr, raw) {
189
- return (raw.kind === "var" && raw.name === "undefined")
190
- ? { kind: "constructor", name: ".none" }
191
- : { kind: "app", fn: "Some", args: [expr] };
202
+ // Set type: "Option" so Lean emits `Option.some`/`Option.none` (qualified).
203
+ // The dotted form `.some`/`.none` would be ambiguous in expression positions
204
+ // like the scrutinee of an outer match. Dafny treats `Option.Some` and bare
205
+ // `Some` equivalently — the qualification is harmless there.
206
+ if (raw.kind === "var" && raw.name === "undefined")
207
+ return { kind: "constructor", name: "none", type: "Option" };
208
+ if (raw.ty.kind === "optional")
209
+ return expr; // already Option<T>, don't double-wrap
210
+ return { kind: "constructor", name: "some", type: "Option", args: [expr] };
192
211
  }
193
212
  function lowerExpr(e, binds) {
194
- // Monadic lifting: extract embedded method calls to let-binds
195
- // Pass binds through to args so nested method calls are also lifted
196
- if (binds && e.kind === "call" && e.callKind === "method") {
213
+ // Monadic lifting: extract embedded method calls to let-binds.
214
+ // `callKind: "method"` means a global var-fn call (classifyCall returns
215
+ // "method" only for `fn.kind === "var"`). Receiver method calls have
216
+ // callKind "unknown" and fall through to the regular case below where
217
+ // they become `methodCall`.
218
+ if (binds && e.kind === "call" && e.callKind === "method" && e.fn.kind === "var") {
197
219
  const name = `_t${_liftCounter++}`;
198
- const fn = e.fn.kind === "var" ? e.fn.name : `${lowerExpr(e.fn, binds)}`;
199
220
  const args = e.args.map(a => lowerExpr(a, binds));
200
- binds.push({ kind: "let-bind", name, value: { kind: "app", fn, args } });
221
+ binds.push({ kind: "let-bind", name, value: { kind: "app", fn: e.fn.name, args } });
201
222
  return { kind: "var", name };
202
223
  }
203
224
  switch (e.kind) {
@@ -366,7 +387,11 @@ function lowerExpr(e, binds) {
366
387
  case "index": {
367
388
  const idx = transformExpr(e.idx);
368
389
  if (e.obj.ty.kind === "map") {
369
- return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method: "get", args: [idx], monadic: false };
390
+ // Mirrors the .get() .getDirect switch at line ~453: when resolve has
391
+ // narrowed the index type to non-optional (via `k in m` atoms in scope),
392
+ // emit direct access; otherwise keep the Option-producing `get`.
393
+ const method = e.ty.kind !== "optional" ? "getDirect" : "get";
394
+ return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method, args: [idx], monadic: false };
370
395
  }
371
396
  const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
372
397
  return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
@@ -408,6 +433,10 @@ function lowerExpr(e, binds) {
408
433
  return { kind: "toNat", expr: lowered };
409
434
  return lowered;
410
435
  });
436
+ // arr.concat(otherArr): array argument → real concatenation, not push
437
+ if (method === "concat" && e.fn.obj.ty.kind === "array" && e.args.length === 1 && e.args[0].ty.kind === "array") {
438
+ return { kind: "binop", op: "arrayConcat", left: recv, right: args[0] };
439
+ }
411
440
  // Spec-context map get: result type is non-optional → direct access
412
441
  if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
413
442
  method = "getDirect";
@@ -525,65 +554,6 @@ function lowerExpr(e, binds) {
525
554
  case "exists":
526
555
  return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
527
556
  case "conditional": {
528
- // Phase 0: Complex expression check (call results, etc.) — resolve substituted
529
- // and set narrowedVar + narrowedExpr because transform can't detect these.
530
- if (e.narrowedVar && e.narrowedExpr) {
531
- const scrutinee = lowerExpr(e.narrowedExpr, binds);
532
- const bound = matchBinder(e.narrowedVar);
533
- let thenExpr = lowerExpr(e.then, binds);
534
- let elseExpr = lowerExpr(e.else, binds);
535
- if (bound !== e.narrowedVar) {
536
- thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
537
- }
538
- if (e.ty.kind === "optional") {
539
- thenExpr = wrapOptionalBranch(thenExpr, e.then);
540
- elseExpr = wrapOptionalBranch(elseExpr, e.else);
541
- }
542
- return {
543
- kind: "match", scrutinee,
544
- arms: [
545
- { pattern: `.some ${bound}`, body: thenExpr },
546
- { pattern: ".none", body: elseExpr },
547
- ],
548
- };
549
- }
550
- // Phase 1: Truthiness — cond itself is optional (e.g. opt ? X : Y)
551
- // Uses narrowedVar set by resolve's Phase 1 (unchanged).
552
- if (e.narrowedVar && e.cond.ty.kind === "optional") {
553
- const cond = lowerExpr(e.cond, binds);
554
- let thenExpr = lowerExpr(e.then, binds);
555
- let elseExpr = lowerExpr(e.else, binds);
556
- const bound = matchBinder(e.narrowedVar);
557
- if (bound !== e.narrowedVar) {
558
- thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
559
- }
560
- thenExpr = wrapOptionalBranch(thenExpr, e.then);
561
- elseExpr = wrapOptionalBranch(elseExpr, e.else);
562
- return {
563
- kind: "match", scrutinee: cond,
564
- arms: [
565
- { pattern: `.some ${bound}`, body: thenExpr },
566
- { pattern: ".none", body: elseExpr },
567
- ],
568
- };
569
- }
570
- // Phase 2: Explicit check — x !== undefined ? A : B
571
- // Transform detects the pattern itself (no narrowedVar/narrowedExpr from resolve).
572
- const check = parseOptionalCheck(e.cond);
573
- if (check && !check.negated) {
574
- return lowerOptionalConditional(e, check, null, binds);
575
- }
576
- // Phase 3: && with optional check — x !== undefined && guard(x) ? A : B
577
- if (e.cond.kind === "binop" && e.cond.op === "&&") {
578
- const extracted = extractLeftmostOptional(e.cond);
579
- if (extracted) {
580
- const innerCheck = parseOptionalCheck(extracted.optCond);
581
- if (innerCheck && !innerCheck.negated) {
582
- return lowerOptionalConditional(e, innerCheck, extracted.rest, binds);
583
- }
584
- }
585
- }
586
- // Phase 4: Regular conditional (no optional narrowing)
587
557
  const cond = lowerExpr(e.cond, binds);
588
558
  let thenExpr = lowerExpr(e.then, binds);
589
559
  let elseExpr = lowerExpr(e.else, binds);
@@ -593,6 +563,12 @@ function lowerExpr(e, binds) {
593
563
  }
594
564
  return { kind: "if", cond, then: thenExpr, else: elseExpr };
595
565
  }
566
+ case "optChain":
567
+ // Narrow should have rewritten optChain to someMatch.
568
+ throw new Error(`optChain reached transform — narrow should have rewritten it`);
569
+ case "nullish":
570
+ // Narrow should have rewritten nullish to someMatch.
571
+ throw new Error(`nullish reached transform — narrow should have rewritten it`);
596
572
  case "havoc":
597
573
  // Dafny's * only works in var/assign positions — lift to own declaration
598
574
  if (binds) {
@@ -601,6 +577,41 @@ function lowerExpr(e, binds) {
601
577
  return { kind: "var", name };
602
578
  }
603
579
  return { kind: "havoc", type: e.ty };
580
+ case "someMatch": {
581
+ let someBody;
582
+ let scrutinee;
583
+ const path = asTAccessPath(e.scrutinee);
584
+ if (path) {
585
+ // Pure access path (var or any depth of obj.f.g.h) — substitute the
586
+ // path with the binder pre-lowering.
587
+ const replaced = replacePathInTExpr(e.someBody, path, e.binder, e.binderTy);
588
+ someBody = lowerExpr(replaced, binds);
589
+ scrutinee = path.fields.length === 0 ? path.rootVar : lowerExpr(e.scrutinee, binds);
590
+ }
591
+ else {
592
+ // Complex scrutinee — narrow pre-bound the someBody to use the binder directly,
593
+ // so no substitution needed. Used by optChain rewrites.
594
+ someBody = lowerExpr(e.someBody, binds);
595
+ scrutinee = lowerExpr(e.scrutinee, binds);
596
+ }
597
+ let noneBody = lowerExpr(e.noneBody, binds);
598
+ if (e.ty.kind === "optional") {
599
+ someBody = wrapOptionalBranch(someBody, e.someBody);
600
+ noneBody = wrapOptionalBranch(noneBody, e.noneBody);
601
+ }
602
+ return {
603
+ kind: "match", scrutinee,
604
+ arms: [
605
+ { pattern: `.some ${e.binder}`, body: someBody },
606
+ { pattern: ".none", body: noneBody },
607
+ ],
608
+ };
609
+ }
610
+ case "tagMatch":
611
+ // Spec/expr-position tagMatch — narrow shouldn't produce these (it only
612
+ // emits stmt-form tagMatch from if-chain detection on stmts). If reached,
613
+ // bug in narrow.
614
+ throw new Error(`tagMatch reached lowerExpr — narrow only emits stmt-form tagMatch`);
604
615
  }
605
616
  }
606
617
  function flattenImpl(e) {
@@ -660,27 +671,6 @@ function transformStmts(stmts, typeDecls) {
660
671
  let i = 0;
661
672
  while (i < stmts.length) {
662
673
  const s = stmts[i];
663
- // Detect discriminant if-chain → match
664
- if (s.kind === "if") {
665
- const chain = detectDiscriminantChain(stmts.slice(i));
666
- if (chain) {
667
- result.push(emitMatchStmt(chain.chain, typeDecls));
668
- i += chain.consumed;
669
- continue;
670
- }
671
- // Detect optional check → match on Some/None
672
- const optMatch = prepareOptionalMatch(s, stmts.slice(i + 1));
673
- if (optMatch) {
674
- result.push(emitOptionalMatch(optMatch.check.varName, optMatch.check.negated, s, typeDecls, stmts.slice(i + 1), optMatch.check.fieldExpr));
675
- // If rest was consumed into the Some branch, skip remaining
676
- const origSome = optMatch.check.negated ? s.else : s.then;
677
- if (origSome.length === 0 && i + 1 < stmts.length) {
678
- return result;
679
- }
680
- i++;
681
- continue;
682
- }
683
- }
684
674
  // Transform for-of → for-in over range
685
675
  if (s.kind === "forof") {
686
676
  const varName = s.names[0];
@@ -844,21 +834,6 @@ function transformStmt(s, typeDecls) {
844
834
  return stmts;
845
835
  }
846
836
  }
847
- // Desugar: const x = optCheck && guard ? A : B
848
- // → var x := B; if (optCheck && guard) { x := A; }
849
- // This avoids putting method calls (from guard) inside a match expression,
850
- // which Dafny doesn't allow. The if-case in transformStmt handles the &&
851
- // via extractLeftmostOptional → emitOptionalMatch (statement-level match).
852
- if (s.init.kind === "conditional" && s.init.cond.kind === "binop" && s.init.cond.op === "&&") {
853
- const extracted = extractLeftmostOptional(s.init.cond);
854
- if (extracted) {
855
- const desugared = [
856
- { kind: "let", name: s.name, ty: s.ty, mutable: true, init: s.init.else },
857
- { kind: "if", cond: s.init.cond, then: [{ kind: "assign", target: s.name, value: s.init.then }], else: [] },
858
- ];
859
- return desugared.flatMap(ds => transformStmt(ds, typeDecls));
860
- }
861
- }
862
837
  const { binds, expr } = liftMethodCalls(s.init);
863
838
  return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
864
839
  }
@@ -889,15 +864,19 @@ function transformStmt(s, typeDecls) {
889
864
  const { binds, expr } = liftMethodCalls(s.expr);
890
865
  return [...binds, { kind: "assign", target: receiver, value: expr }];
891
866
  }
892
- // Optional chaining on map.get: m.get(k)?.push(v) → if k in m { m[k] := m[k] + [v] }
893
- if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
894
- s.expr.fn.obj.kind === "call" && s.expr.fn.obj.fn.kind === "field" &&
895
- s.expr.fn.obj.fn.obj.ty.kind === "map" && s.expr.fn.obj.fn.field === "get" &&
896
- s.expr.fn.field === "push") {
897
- const mapExpr = s.expr.fn.obj.fn.obj;
867
+ // Optional chaining on map.get at statement level: m.get(k)?.push(v)
868
+ // → if k in m { m[k] := m[k] + [v] } (actual mutation, not value-discard).
869
+ // Narrow rewrote this to a someMatch — destructure to find the underlying
870
+ // m.get(k) scrutinee and the .push(v) body call.
871
+ if (s.expr.kind === "someMatch" &&
872
+ s.expr.scrutinee.kind === "call" && s.expr.scrutinee.fn.kind === "field" &&
873
+ s.expr.scrutinee.fn.field === "get" && s.expr.scrutinee.fn.obj.ty.kind === "map" &&
874
+ s.expr.someBody.kind === "call" && s.expr.someBody.fn.kind === "field" &&
875
+ s.expr.someBody.fn.field === "push") {
876
+ const mapExpr = s.expr.scrutinee.fn.obj;
898
877
  const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
899
- const keyExpr = lowerExpr(s.expr.fn.obj.args[0], null);
900
- const pushArg = lowerExpr(s.expr.args[0], null);
878
+ const keyExpr = lowerExpr(s.expr.scrutinee.args[0], null);
879
+ const pushArg = lowerExpr(s.expr.someBody.args[0], null);
901
880
  if (mapName) {
902
881
  const mapVar = { kind: "var", name: mapName };
903
882
  const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
@@ -911,18 +890,7 @@ function transformStmt(s, typeDecls) {
911
890
  return [...binds, { kind: "assign", target: "_", value: expr }];
912
891
  }
913
892
  case "if": {
914
- // Restructure && with optional check: extract the leftmost optional check
915
- // from a && chain and nest the rest inside. Handles left-associative chains:
916
- // if ((x !== undefined && b) && c) → if (x !== undefined) { if (b && c) { ... } }
917
- if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
918
- const extracted = extractLeftmostOptional(s.cond);
919
- if (extracted) {
920
- const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
921
- const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
922
- return transformStmts([outerIf], typeDecls);
923
- }
924
- }
925
- // Lift from condition only (Lean rule: don't lift from branches)
893
+ // Lift from condition only (Lean rule: don't lift from branches).
926
894
  const { binds, expr: cond } = liftMethodCalls(s.cond);
927
895
  return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
928
896
  }
@@ -947,180 +915,34 @@ function transformStmt(s, typeDecls) {
947
915
  return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
948
916
  case "assert":
949
917
  return [{ kind: "assert", expr: transformExpr(s.expr) }];
950
- }
951
- }
952
- function detectDiscriminantChain(stmts) {
953
- if (stmts.length === 0 || stmts[0].kind !== "if")
954
- return null;
955
- const first = parseDiscriminantCond(stmts[0].cond);
956
- if (!first)
957
- return null;
958
- const cases = [];
959
- // Follow else branches within one if-else-if tree
960
- function collectElse(s) {
961
- const p = parseDiscriminantCond(s.cond);
962
- if (!p || p.varName !== first.varName)
963
- return [s];
964
- cases.push({ variant: p.variant, body: s.then });
965
- if (s.else.length === 0)
966
- return [];
967
- if (s.else.length === 1 && s.else[0].kind === "if")
968
- return collectElse(s.else[0]);
969
- return s.else;
970
- }
971
- // Walk consecutive top-level ifs on the same discriminant
972
- let consumed = 0;
973
- for (let i = 0; i < stmts.length; i++) {
974
- const s = stmts[i];
975
- if (s.kind !== "if")
976
- break;
977
- const p = parseDiscriminantCond(s.cond);
978
- if (!p || p.varName !== first.varName)
979
- break;
980
- cases.push({ variant: p.variant, body: s.then });
981
- consumed = i + 1;
982
- if (s.else.length > 0) {
983
- const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
984
- return cases.length > 0 ? { chain: { ...first, cases, fallthrough: ft }, consumed } : null;
918
+ case "someMatch": {
919
+ const path = asTAccessPath(s.scrutinee);
920
+ if (path) {
921
+ const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
922
+ const someBody = transformStmts(replaced, typeDecls);
923
+ const noneBody = transformStmts(s.noneBody, typeDecls);
924
+ const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
925
+ return [{
926
+ kind: "match", scrutinee,
927
+ arms: [
928
+ { pattern: `.some ${s.binder}`, body: someBody },
929
+ { pattern: ".none", body: noneBody },
930
+ ],
931
+ }];
932
+ }
933
+ throw new Error(`someMatch stmt scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
934
+ }
935
+ case "tagMatch": {
936
+ const varName = s.scrutinee.kind === "var" ? s.scrutinee.name : "?";
937
+ const chain = { varName, typeName: s.typeName, cases: s.cases, fallthrough: s.fallthrough };
938
+ return [emitMatchStmt(chain, typeDecls)];
985
939
  }
986
940
  }
987
- if (cases.length === 0)
988
- return null;
989
- return { chain: { ...first, cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
990
- }
991
- function parseDiscriminantCond(cond) {
992
- // Pattern: x.discriminant === "variant"
993
- if (cond.kind !== "binop" || cond.op !== "===" || cond.right.kind !== "str")
994
- return null;
995
- if (cond.left.kind !== "field" || !cond.left.isDiscriminant)
996
- return null;
997
- if (cond.left.obj.kind !== "var" || cond.left.obj.ty.kind !== "user")
998
- return null;
999
- return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
1000
- }
1001
- function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
1002
- let someBranch = negated ? s.else : s.then;
1003
- const noneBranch = negated ? s.then : s.else;
1004
- // Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
1005
- // so include remaining statements as the Some body
1006
- if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
1007
- someBranch = restStmts;
1008
- }
1009
- const sanitized = varName.replace(/\./g, "_");
1010
- const bound = matchBinder(`${sanitized}_val`);
1011
- // Replace the narrowed variable/field in the Some branch body.
1012
- // Field chains: replace in TStmt before transform (so downstream narrowing sees simple vars).
1013
- // Simple vars: replace in IR after transform (the original mechanism).
1014
- let someBody;
1015
- if (fieldExpr && fieldExpr.kind === "field" && fieldExpr.obj.kind === "var") {
1016
- const innerTy = fieldExpr.ty.kind === "optional" ? fieldExpr.ty.inner : fieldExpr.ty;
1017
- const replaced = replaceFieldsInTStmts(someBranch, fieldExpr.obj.name, [
1018
- { fieldName: fieldExpr.field, newName: bound, fallbackTy: innerTy },
1019
- ]);
1020
- someBody = transformStmts(replaced, typeDecls);
1021
- }
1022
- else {
1023
- const transformed = transformStmts(someBranch, typeDecls);
1024
- someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound }, true)));
1025
- }
1026
- // For field chains, use an Expr scrutinee so outer match replaceVar can
1027
- // substitute the object variable (e.g. task → i_task_val in task.deletedFromList).
1028
- // String scrutinees are opaque to replaceVar/mapExpr.
1029
- const scrutinee = fieldExpr ? transformExpr(fieldExpr) : varName;
1030
- return {
1031
- kind: "match", scrutinee,
1032
- arms: [
1033
- { pattern: `.some ${bound}`, body: someBody },
1034
- { pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
1035
- ],
1036
- };
1037
941
  }
1038
942
  /** Apply an expression transform to all expressions in a statement (convenience wrapper). */
1039
943
  function mapStmtExprs(s, r) {
1040
944
  return mapStmt(s, e => r(e));
1041
945
  }
1042
- // ── Optional narrowing helpers ──────────────────────────────
1043
- //
1044
- // Optional narrowing converts TS `if (x === undefined)` / `x !== undefined ? a : b`
1045
- // patterns to `match x { Some(val) => ..., None => ... }`.
1046
- //
1047
- // The resolve phase (resolve.ts) handles TYPE narrowing only:
1048
- // - Flow narrowing: after `if (x === undefined) return`, x is non-optional
1049
- // - && narrowing: in `x !== undefined && f(x)`, f(x) sees x as non-optional
1050
- // - Conditional type narrowing: extends env (simple vars) or narrowedFields context
1051
- // (field chains) so the then-branch resolves with the unwrapped type
1052
- //
1053
- // The transform phase (here) handles ALL structural narrowing:
1054
- // - Statement-level: `transformStmts` detects optional checks → `emitOptionalMatch`
1055
- // - Expression-level: `lowerExpr` detects optional checks via `parseOptionalCheck`
1056
- // and `extractLeftmostOptional` → `lowerOptionalConditional`
1057
- // - && restructuring: `extractLeftmostOptional` splits `&&` chains, generating
1058
- // match with guard: `match x { Some(val) => if guard then A else B, None => B }`
1059
- /** Shared logic for optional match in both imperative and pure function paths.
1060
- * Detects optional check, selects branches, handles early-return consumption.
1061
- * Returns null if the condition is not an optional check. */
1062
- function prepareOptionalMatch(s, restStmts) {
1063
- const check = parseOptionalCheck(s.cond);
1064
- if (!check)
1065
- return null;
1066
- let someBranch = check.negated ? s.else : s.then;
1067
- const noneBranch = check.negated ? s.then : (s.else.length > 0 ? s.else : restStmts);
1068
- // Early-return pattern: Some branch is empty → consume rest of block
1069
- if (someBranch.length === 0 && restStmts.length > 0)
1070
- someBranch = restStmts;
1071
- const sanitized = check.varName.replace(/\./g, "_");
1072
- const bound = matchBinder(`${sanitized}_val`);
1073
- return { check, someBranch, noneBranch, bound };
1074
- }
1075
- /** Extract the leftmost optional check from a && chain, returning the check and the rest.
1076
- * (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
1077
- function extractLeftmostOptional(cond) {
1078
- if (cond.kind !== "binop" || cond.op !== "&&")
1079
- return null;
1080
- const check = parseOptionalCheck(cond.left);
1081
- if (check && !check.negated)
1082
- return { optCond: cond.left, rest: cond.right };
1083
- if (cond.left.kind === "binop" && cond.left.op === "&&") {
1084
- const inner = extractLeftmostOptional(cond.left);
1085
- if (inner)
1086
- return { optCond: inner.optCond, rest: { ...cond, left: inner.rest } };
1087
- }
1088
- return null;
1089
- }
1090
- /** Detect `v !== undefined` or `undefined !== v` where v has optional type.
1091
- * Also handles field access chains like `obj.field !== undefined`.
1092
- * When `fieldExpr` is returned, callers must use field-aware replacement. */
1093
- function parseOptionalCheck(cond) {
1094
- if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
1095
- return null;
1096
- let varExpr = null;
1097
- if (cond.right.kind === "var" && cond.right.name === "undefined")
1098
- varExpr = cond.left;
1099
- if (cond.left.kind === "var" && cond.left.name === "undefined")
1100
- varExpr = cond.right;
1101
- if (!varExpr)
1102
- return null;
1103
- if (varExpr.kind === "var" && varExpr.ty.kind === "optional") {
1104
- return { varName: varExpr.name, negated: cond.op === "===" };
1105
- }
1106
- if (varExpr.kind === "field" && varExpr.ty.kind === "optional") {
1107
- // Serialize field chain as a dotted name for use as match scrutinee
1108
- const chain = serializeFieldChain(varExpr);
1109
- if (chain)
1110
- return { varName: chain, negated: cond.op === "===", fieldExpr: varExpr };
1111
- }
1112
- return null;
1113
- }
1114
- /** Serialize a field access chain to a dotted variable path, or null if not a simple chain. */
1115
- function serializeFieldChain(e) {
1116
- if (e.kind === "var")
1117
- return e.name;
1118
- if (e.kind === "field") {
1119
- const parent = serializeFieldChain(e.obj);
1120
- return parent ? `${parent}.${e.field}` : null;
1121
- }
1122
- return null;
1123
- }
1124
946
  /** Build match arms from variant cases — shared by imperative and pure paths.
1125
947
  * Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
1126
948
  * and delegates body transformation to the caller-provided function.
@@ -1142,10 +964,32 @@ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1142
964
  function emitMatchStmt(chain, typeDecls) {
1143
965
  const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
1144
966
  const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
1145
- if (chain.fallthrough.length > 0)
1146
- arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
967
+ if (chain.fallthrough.length > 0) {
968
+ const remaining = remainingVariant(chain, typeDecls);
969
+ if (remaining) {
970
+ // Exactly one variant left — destructure so the fallthrough body can
971
+ // access variant-specific fields (Lean requires this; Dafny tolerates `_`).
972
+ const pattern = buildMatchPattern(remaining.name, remaining.fields, chain.varName);
973
+ const body = transformStmts(replaceFieldAccessInTStmts(chain.fallthrough, chain.varName, remaining.fields), typeDecls);
974
+ arms.push({ pattern, body });
975
+ }
976
+ else {
977
+ arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
978
+ }
979
+ }
1147
980
  return { kind: "match", scrutinee: chain.varName, arms };
1148
981
  }
982
+ /** If the chain has matched all variants but one, return that remaining variant. */
983
+ function remainingVariant(chain, typeDecls) {
984
+ const decl = typeDecls.find(d => d.name === chain.typeName);
985
+ if (!decl?.variants)
986
+ return null;
987
+ const matched = new Set(chain.cases.map(c => c.variant));
988
+ const remaining = decl.variants.filter(v => !matched.has(v.name));
989
+ if (remaining.length !== 1)
990
+ return null;
991
+ return remaining[0];
992
+ }
1149
993
  function emitSwitchStmt(s, typeDecls) {
1150
994
  const varName = s.expr.kind === "var" ? s.expr.name : "?";
1151
995
  const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
@@ -1155,10 +999,7 @@ function emitSwitchStmt(s, typeDecls) {
1155
999
  arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
1156
1000
  return { kind: "match", scrutinee: varName, arms };
1157
1001
  }
1158
- /** Replace obj.field → replacement var in typed IR (before transform).
1159
- * Used by discriminant match/switch and optional match to rewrite field accesses
1160
- * into simple variables before the transform phase, so downstream narrowing
1161
- * (parseOptionalCheck, extractLeftmostOptional) sees simple variable references.
1002
+ /** Replace obj.field → replacement var in typed IR.
1162
1003
  * Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
1163
1004
  function replaceFieldsInTStmts(stmts, objName, replacements) {
1164
1005
  if (replacements.length === 0)
@@ -1199,6 +1040,40 @@ function replaceFieldInTExpr(expr, objName, replacements) {
1199
1040
  return null;
1200
1041
  });
1201
1042
  }
1043
+ function asTAccessPath(e) {
1044
+ if (e.kind === "var")
1045
+ return { rootVar: e.name, fields: [] };
1046
+ if (e.kind === "field") {
1047
+ const inner = asTAccessPath(e.obj);
1048
+ if (!inner)
1049
+ return null;
1050
+ return { rootVar: inner.rootVar, fields: [...inner.fields, e.field] };
1051
+ }
1052
+ return null;
1053
+ }
1054
+ /** Does TExpr `e` match the given access path exactly? */
1055
+ function matchesAccessPath(e, path) {
1056
+ const collected = [];
1057
+ let cur = e;
1058
+ while (cur.kind === "field") {
1059
+ collected.unshift(cur.field);
1060
+ cur = cur.obj;
1061
+ }
1062
+ if (cur.kind !== "var" || cur.name !== path.rootVar)
1063
+ return false;
1064
+ if (collected.length !== path.fields.length)
1065
+ return false;
1066
+ return collected.every((f, i) => f === path.fields[i]);
1067
+ }
1068
+ /** Replace every TExpr matching `path` with `var(binder, binderTy)`. */
1069
+ function replacePathInTExpr(expr, path, binder, binderTy) {
1070
+ return mapTExpr(expr, e => matchesAccessPath(e, path)
1071
+ ? { kind: "var", name: binder, ty: binderTy } : null);
1072
+ }
1073
+ function replacePathInTStmts(stmts, path, binder, binderTy) {
1074
+ return stmts.map(s => mapTStmt(s, e => matchesAccessPath(e, path)
1075
+ ? { kind: "var", name: binder, ty: binderTy } : null));
1076
+ }
1202
1077
  /** Unwrap optional type on match-bound variables in TExpr.
1203
1078
  * After replaceFieldInTExpr, the replaced variable carries the original optional
1204
1079
  * type from the field declaration. The match binding unwraps it to the inner type. */
@@ -1206,80 +1081,14 @@ function fixBoundType(expr, boundName) {
1206
1081
  return mapTExpr(expr, e => e.kind === "var" && e.name === boundName && e.ty.kind === "optional"
1207
1082
  ? { ...e, ty: e.ty.inner } : null);
1208
1083
  }
1209
- /** Lower a conditional with an optional check (Phase 2: explicit, Phase 3: && with guard).
1210
- * Generates: match scrutinee { Some(val) => [if guard then] A [else B], None => B }
1211
- * For field chains, replaces field accesses in TExpr before lowering.
1212
- * For simple vars, replaces variable names in Expr after lowering. */
1213
- function lowerOptionalConditional(e, check, guard, binds) {
1214
- const sanitized = check.varName.replace(/\./g, "_");
1215
- const bound = matchBinder(`${sanitized}_val`);
1216
- const isFieldChain = check.fieldExpr && check.fieldExpr.kind === "field" &&
1217
- check.fieldExpr.obj.kind === "var";
1218
- // Determine which branch is Some (unwrapped) vs None
1219
- let thenTExpr = e.then;
1220
- let guardTExpr = guard;
1221
- // For field chains: replace field access with bound var in TExpr before lowering
1222
- if (isFieldChain) {
1223
- const fe = check.fieldExpr;
1224
- const innerTy = fe.ty.kind === "optional" ? fe.ty.inner : fe.ty;
1225
- const replacements = [{ fieldName: fe.field, newName: bound, fallbackTy: innerTy }];
1226
- thenTExpr = fixBoundType(replaceFieldInTExpr(thenTExpr, fe.obj.name, replacements), bound);
1227
- if (guardTExpr) {
1228
- guardTExpr = fixBoundType(replaceFieldInTExpr(guardTExpr, fe.obj.name, replacements), bound);
1229
- }
1230
- }
1231
- let thenExpr = lowerExpr(thenTExpr, binds);
1232
- let elseExpr = lowerExpr(e.else, binds);
1233
- // For simple vars: replace after lowering
1234
- if (!isFieldChain) {
1235
- thenExpr = replaceVar(thenExpr, check.varName, { kind: "var", name: bound }, true);
1236
- }
1237
- // Optional wrapping: check if either branch is undefined (produces optional result)
1238
- const isOptionalResult = (e.then.kind === "var" && e.then.name === "undefined") ||
1239
- (e.else.kind === "var" && e.else.name === "undefined");
1240
- if (isOptionalResult) {
1241
- thenExpr = wrapOptionalBranch(thenExpr, e.then);
1242
- elseExpr = wrapOptionalBranch(elseExpr, e.else);
1243
- }
1244
- // Build Some arm body — add guard for && patterns
1245
- // Note: if the guard has method calls, the impure path desugars the let to a
1246
- // statement-level if+match in transformStmt, so this expression-level path only
1247
- // runs for pure guards. Use null for binds to avoid escaping the match scope.
1248
- let someBody;
1249
- if (guard) {
1250
- let guardExpr = lowerExpr(guardTExpr, null);
1251
- if (!isFieldChain) {
1252
- guardExpr = replaceVar(guardExpr, check.varName, { kind: "var", name: bound }, true);
1253
- }
1254
- // Guard-else gets the same expression as None arm
1255
- let guardElse = lowerExpr(e.else, null);
1256
- if (isOptionalResult) {
1257
- guardElse = wrapOptionalBranch(guardElse, e.else);
1258
- }
1259
- someBody = { kind: "if", cond: guardExpr, then: thenExpr, else: guardElse };
1260
- }
1261
- else {
1262
- someBody = thenExpr;
1263
- }
1264
- // Build scrutinee
1265
- const scrutinee = isFieldChain
1266
- ? lowerExpr(check.fieldExpr, binds)
1267
- : check.varName;
1268
- return {
1269
- kind: "match", scrutinee,
1270
- arms: [
1271
- { pattern: `.some ${bound}`, body: someBody },
1272
- { pattern: ".none", body: elseExpr },
1273
- ],
1274
- };
1275
- }
1276
1084
  // ── Pure function generation ─────────────────────────────────
1277
1085
  function transformPureBody(stmts, typeDecls) {
1278
- // Detect discriminant if-chain
1279
- if (stmts.length > 0 && stmts[0].kind === "if") {
1280
- const chain = detectDiscriminantChain(stmts);
1281
- if (chain)
1282
- return transformPureMatch(chain.chain, typeDecls);
1086
+ // tagMatch (from narrow's discriminant detection) is the leading stmt and consumes the rest.
1087
+ if (stmts.length > 0 && stmts[0].kind === "tagMatch") {
1088
+ const t = stmts[0];
1089
+ const varName = t.scrutinee.kind === "var" ? t.scrutinee.name : "?";
1090
+ const chain = { varName, typeName: t.typeName, cases: t.cases, fallthrough: t.fallthrough };
1091
+ return transformPureMatch(chain, typeDecls);
1283
1092
  }
1284
1093
  for (let i = 0; i < stmts.length; i++) {
1285
1094
  const s = stmts[i];
@@ -1293,52 +1102,8 @@ function transformPureBody(stmts, typeDecls) {
1293
1102
  return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
1294
1103
  }
1295
1104
  case "if": {
1296
- // Restructure && with optional check: split into nested ifs so
1297
- // prepareOptionalMatch can detect the optional check and bind the unwrapped value
1298
- if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
1299
- const extracted = extractLeftmostOptional(s.cond);
1300
- if (extracted) {
1301
- const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
1302
- const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
1303
- return transformPureBody([outerIf, ...rest], typeDecls);
1304
- }
1305
- }
1306
- // Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
1307
- const optMatch = prepareOptionalMatch(s, rest);
1308
- if (optMatch) {
1309
- // For field chains (a.dueDate !== undefined), replace in TStmt before transform
1310
- let someBranch = [...optMatch.someBranch, ...rest];
1311
- const fe = optMatch.check.fieldExpr;
1312
- if (fe && fe.kind === "field" && fe.obj.kind === "var") {
1313
- const innerTy = fe.ty.kind === "optional" ? fe.ty.inner : fe.ty;
1314
- someBranch = replaceFieldsInTStmts(someBranch, fe.obj.name, [
1315
- { fieldName: fe.field, newName: optMatch.bound, fallbackTy: innerTy },
1316
- ]);
1317
- // The replacement keeps the original optional type, but the match binding
1318
- // unwraps it. Fix the type so downstream record coercion re-wraps with Some().
1319
- someBranch = someBranch.map(s => mapTStmt(s, e => e.kind === "var" && e.name === optMatch.bound && e.ty.kind === "optional"
1320
- ? { ...e, ty: e.ty.inner } : null));
1321
- }
1322
- const someExpr = transformPureBody(someBranch, typeDecls);
1323
- if (!someExpr)
1324
- return null;
1325
- const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
1326
- if (!noneExpr)
1327
- return null;
1328
- // For simple vars, replace in Expr after transform
1329
- const someReplaced = optMatch.check.fieldExpr
1330
- ? someExpr
1331
- : replaceVar(someExpr, optMatch.check.varName, { kind: "var", name: optMatch.bound }, true);
1332
- return {
1333
- kind: "match", scrutinee: optMatch.check.varName,
1334
- arms: [
1335
- { pattern: `.some ${optMatch.bound}`, body: someReplaced },
1336
- { pattern: ".none", body: noneExpr },
1337
- ],
1338
- };
1339
- }
1340
1105
  // Append rest to both branches so nested ifs that fall through
1341
- // can reach the continuation (e.g. early return inside then-branch)
1106
+ // can reach the continuation (e.g. early return inside then-branch).
1342
1107
  const thenExpr = transformPureBody([...s.then, ...rest], typeDecls);
1343
1108
  if (!thenExpr)
1344
1109
  return null;
@@ -1349,6 +1114,27 @@ function transformPureBody(stmts, typeDecls) {
1349
1114
  return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
1350
1115
  }
1351
1116
  case "switch": return transformPureSwitch(s, typeDecls);
1117
+ case "someMatch": {
1118
+ const path = asTAccessPath(s.scrutinee);
1119
+ if (path) {
1120
+ const replaced = replacePathInTStmts(s.someBody, path, s.binder, s.binderTy);
1121
+ const someExpr = transformPureBody([...replaced, ...rest], typeDecls);
1122
+ if (!someExpr)
1123
+ return null;
1124
+ const noneExpr = transformPureBody([...s.noneBody, ...rest], typeDecls);
1125
+ if (!noneExpr)
1126
+ return null;
1127
+ const scrutinee = path.fields.length === 0 ? path.rootVar : transformExpr(s.scrutinee);
1128
+ return {
1129
+ kind: "match", scrutinee,
1130
+ arms: [
1131
+ { pattern: `.some ${s.binder}`, body: someExpr },
1132
+ { pattern: ".none", body: noneExpr },
1133
+ ],
1134
+ };
1135
+ }
1136
+ throw new Error(`someMatch pure-body scrutinee must be a pure access path, got ${s.scrutinee.kind}`);
1137
+ }
1352
1138
  default: return null;
1353
1139
  }
1354
1140
  }
@@ -1398,10 +1184,22 @@ function transformPureMatch(chain, typeDecls) {
1398
1184
  const decl = typeDecls.find(d => d.name === chain.typeName);
1399
1185
  const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
1400
1186
  if (chain.fallthrough.length > 0 && !allCovered) {
1401
- const body = transformPureBody(chain.fallthrough, typeDecls);
1402
- if (!body)
1403
- return null;
1404
- arms.push({ pattern: "_", body });
1187
+ const remaining = remainingVariant(chain, typeDecls);
1188
+ if (remaining) {
1189
+ // Exactly one variant left — destructure for variant-specific field access.
1190
+ let body = transformPureBody(chain.fallthrough, typeDecls);
1191
+ if (!body)
1192
+ return null;
1193
+ if (remaining.fields.length > 0)
1194
+ body = replaceFieldAccess(body, chain.varName, remaining.fields);
1195
+ arms.push({ pattern: buildMatchPattern(remaining.name, remaining.fields, chain.varName), body });
1196
+ }
1197
+ else {
1198
+ const body = transformPureBody(chain.fallthrough, typeDecls);
1199
+ if (!body)
1200
+ return null;
1201
+ arms.push({ pattern: "_", body });
1202
+ }
1405
1203
  }
1406
1204
  return { kind: "match", scrutinee: chain.varName, arms };
1407
1205
  }