lemmascript 0.3.0 → 0.3.2

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.
@@ -66,9 +66,6 @@ function mapStmt(s, f) {
66
66
  case "assert": return { ...s, expr: r(s.expr) };
67
67
  }
68
68
  }
69
- function mapStmts(stmts, f) {
70
- return stmts.map(s => mapStmt(s, f));
71
- }
72
69
  /** Map over all sub-expressions in a TExpr (typed IR). */
73
70
  function mapTExpr(e, f) {
74
71
  const hit = f(e);
@@ -133,6 +130,12 @@ let _typeDecls = [];
133
130
  function matchBinder(fieldName, prefix) {
134
131
  return prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`;
135
132
  }
133
+ /** Build a match arm pattern like `.VariantName _v_field1 _v_field2` from variant info. */
134
+ function buildMatchPattern(variantName, fields, scopePrefix) {
135
+ if (fields.length === 0)
136
+ return `.${variantName}`;
137
+ return `.${variantName} ${fields.map(f => matchBinder(f.name, scopePrefix)).join(" ")}`;
138
+ }
136
139
  const _forofCounters = new Map();
137
140
  function isNat(ty) { return ty.kind === "nat"; }
138
141
  function isArray(ty) { return ty.kind === "array"; }
@@ -177,6 +180,13 @@ function transformExpr(e) { return lowerExpr(e, null); }
177
180
  * a method call can appear inline in TS. It does NOT propagate into
178
181
  * field, index, record, forall, or exists sub-expressions.
179
182
  */
183
+ /** Wrap an expression in Some/None for optional-typed conditionals.
184
+ * If the raw TExpr is `undefined`, emit `.none`; otherwise wrap in `Some`. */
185
+ function wrapOptionalBranch(expr, raw) {
186
+ return (raw.kind === "var" && raw.name === "undefined")
187
+ ? { kind: "constructor", name: ".none" }
188
+ : { kind: "app", fn: "Some", args: [expr] };
189
+ }
180
190
  function lowerExpr(e, binds) {
181
191
  // Monadic lifting: extract embedded method calls to let-binds
182
192
  // Pass binds through to args so nested method calls are also lifted
@@ -340,6 +350,9 @@ function lowerExpr(e, binds) {
340
350
  return { kind: "field", obj: transformExpr(e.obj), field: e.field };
341
351
  case "index": {
342
352
  const idx = transformExpr(e.idx);
353
+ if (e.obj.ty.kind === "map") {
354
+ return { kind: "methodCall", obj: transformExpr(e.obj), objTy: e.obj.ty, method: "get", args: [idx], monadic: false };
355
+ }
343
356
  const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
344
357
  return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
345
358
  }
@@ -427,23 +440,41 @@ function lowerExpr(e, binds) {
427
440
  }
428
441
  }
429
442
  }
430
- // For spread records, wrap non-optional values in Some for optional fields
443
+ // For spread records, propagate declared field types and wrap optionals
431
444
  if (e.spread) {
432
445
  const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
433
446
  const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
434
447
  const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
448
+ // Also check discriminated-union variants for field types
449
+ const unionDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "discriminated-union") : undefined;
435
450
  const loweredFields = e.fields.map(f => {
436
- let value = lowerExpr(f.value, binds);
437
- if (structDecl?.fields) {
438
- const fieldDecl = structDecl.fields.find(sf => sf.name === f.name);
439
- if (fieldDecl) {
440
- const fieldTy = parseTsType(fieldDecl.tsType);
441
- const isUndef = f.value.kind === "var" && f.value.name === "undefined";
442
- if (fieldTy.kind === "optional" && f.value.ty.kind !== "optional" && !isUndef) {
443
- value = { kind: "app", fn: "Some", args: [value] };
451
+ // Propagate declared field type onto value if it has unknown type
452
+ let fieldValue = f.value;
453
+ const fieldDecl = structDecl?.fields?.find(sf => sf.name === f.name);
454
+ let declaredTy;
455
+ if (fieldDecl) {
456
+ declaredTy = fieldDecl.type;
457
+ }
458
+ else if (unionDecl?.variants) {
459
+ for (const v of unionDecl.variants) {
460
+ const vf = v.fields.find(vf => vf.name === f.name);
461
+ if (vf) {
462
+ declaredTy = vf.type;
463
+ break;
444
464
  }
445
465
  }
446
466
  }
467
+ if (declaredTy && fieldValue.ty.kind === "unknown") {
468
+ fieldValue = { ...fieldValue, ty: declaredTy };
469
+ }
470
+ let value = lowerExpr(fieldValue, binds);
471
+ // Wrap non-optional values in Some for optional fields
472
+ if (declaredTy?.kind === "optional") {
473
+ const isUndef = f.value.kind === "var" && f.value.name === "undefined";
474
+ if (f.value.ty.kind !== "optional" && !isUndef) {
475
+ value = { kind: "app", fn: "Some", args: [value] };
476
+ }
477
+ }
447
478
  return { name: f.name, value };
448
479
  });
449
480
  return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
@@ -466,7 +497,9 @@ function lowerExpr(e, binds) {
466
497
  case "exists":
467
498
  return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
468
499
  case "conditional": {
469
- const cond = lowerExpr(e.cond, binds);
500
+ // When narrowedExpr is set, the match replaces the condition — don't lift from it
501
+ const condBinds = (e.narrowedVar && e.narrowedExpr) ? null : binds;
502
+ const cond = lowerExpr(e.cond, condBinds);
470
503
  let thenExpr = lowerExpr(e.then, binds);
471
504
  let elseExpr = lowerExpr(e.else, binds);
472
505
  // Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
@@ -476,11 +509,11 @@ function lowerExpr(e, binds) {
476
509
  if (bound !== e.narrowedVar) {
477
510
  thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
478
511
  }
479
- const wrapSomeNone = (expr, raw) => (raw.kind === "var" && raw.name === "undefined")
480
- ? { kind: "constructor", name: ".none" }
481
- : { kind: "app", fn: "Some", args: [expr] };
482
- thenExpr = wrapSomeNone(thenExpr, e.then);
483
- elseExpr = wrapSomeNone(elseExpr, e.else);
512
+ // Wrap in Some/None only when result is optional (one branch is undefined)
513
+ if (e.ty.kind === "optional") {
514
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
515
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
516
+ }
484
517
  return {
485
518
  kind: "match", scrutinee,
486
519
  arms: [
@@ -497,12 +530,8 @@ function lowerExpr(e, binds) {
497
530
  thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
498
531
  }
499
532
  // The match produces an Optional: wrap branches in Some/None.
500
- // Either branch being undefined signals None; otherwise wrap in Some.
501
- const wrapSomeNone = (expr, raw) => (raw.kind === "var" && raw.name === "undefined")
502
- ? { kind: "constructor", name: ".none" }
503
- : { kind: "app", fn: "Some", args: [expr] };
504
- thenExpr = wrapSomeNone(thenExpr, e.then);
505
- elseExpr = wrapSomeNone(elseExpr, e.else);
533
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
534
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
506
535
  return {
507
536
  kind: "match", scrutinee: cond,
508
537
  arms: [
@@ -513,18 +542,8 @@ function lowerExpr(e, binds) {
513
542
  }
514
543
  // Non-optional: regular if with optional wrapping
515
544
  if (e.ty.kind === "optional") {
516
- if (e.then.kind === "var" && e.then.name === "undefined") {
517
- thenExpr = { kind: "constructor", name: ".none" };
518
- }
519
- else {
520
- thenExpr = { kind: "app", fn: "Some", args: [thenExpr] };
521
- }
522
- if (e.else.kind === "var" && e.else.name === "undefined") {
523
- elseExpr = { kind: "constructor", name: ".none" };
524
- }
525
- else {
526
- elseExpr = { kind: "app", fn: "Some", args: [elseExpr] };
527
- }
545
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
546
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
528
547
  }
529
548
  return { kind: "if", cond, then: thenExpr, else: elseExpr };
530
549
  }
@@ -571,7 +590,7 @@ function ensuresToMatch(e, typeDecls) {
571
590
  if (!variant)
572
591
  return null;
573
592
  const fields = variant.fields;
574
- const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name, obj.name)).join(" ")}` : `.${variantName}`;
593
+ const pattern = buildMatchPattern(variantName, fields, obj.name);
575
594
  let rhs = transformExpr(e.right);
576
595
  rhs = replaceFieldAccess(rhs, obj.name, fields);
577
596
  return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
@@ -604,13 +623,12 @@ function transformStmts(stmts, typeDecls) {
604
623
  continue;
605
624
  }
606
625
  // Detect optional check → match on Some/None
607
- const opt = parseOptionalCheck(s.cond);
608
- if (opt) {
609
- const rest = stmts.slice(i + 1);
610
- result.push(emitOptionalMatch(opt.varName, opt.negated, s, typeDecls, rest));
626
+ const optMatch = prepareOptionalMatch(s, stmts.slice(i + 1));
627
+ if (optMatch) {
628
+ result.push(emitOptionalMatch(optMatch.check.varName, optMatch.check.negated, s, typeDecls, stmts.slice(i + 1), optMatch.check.fieldExpr));
611
629
  // If rest was consumed into the Some branch, skip remaining
612
- const someBranch = opt.negated ? s.else : s.then;
613
- if (someBranch.length === 0 && rest.length > 0) {
630
+ const origSome = optMatch.check.negated ? s.else : s.then;
631
+ if (origSome.length === 0 && i + 1 < stmts.length) {
614
632
  return result;
615
633
  }
616
634
  i++;
@@ -622,6 +640,31 @@ function transformStmts(stmts, typeDecls) {
622
640
  const varName = s.names[0];
623
641
  const varTy = s.nameTypes[0] ?? { kind: "unknown" };
624
642
  let iterExpr = transformExpr(s.iterable);
643
+ // Map key-only iteration: for (const k in record) → iterate keys only
644
+ if (s.names.length === 1 && s.iterable.ty.kind === "map") {
645
+ const keyName = s.names[0];
646
+ const keyTy = s.nameTypes[0] ?? s.iterable.ty.key ?? { kind: "unknown" };
647
+ const keysSeqName = `_${keyName}_keys`;
648
+ const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
649
+ result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
650
+ const keysVar = { kind: "var", name: keysSeqName };
651
+ const count = _forofCounters.get(keyName) ?? 0;
652
+ _forofCounters.set(keyName, count + 1);
653
+ const suffix = count === 0 ? "" : `${count + 1}`;
654
+ const idxName = `_${keyName}_idx${suffix}`;
655
+ const idx = { kind: "var", name: idxName };
656
+ const arrSize = { kind: "field", obj: keysVar, field: "size" };
657
+ const bodyStmts = transformStmts(s.body, typeDecls);
658
+ const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
659
+ const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
660
+ result.push({
661
+ kind: "forin", idx: idxName, bound: arrSize,
662
+ invariants: [boundInv, ...s.invariants.map(transformExpr)],
663
+ body: [letKey, ...bodyStmts],
664
+ });
665
+ i++;
666
+ continue;
667
+ }
625
668
  // Map iteration: for (const [k, v] of map) → iterate keys, look up values
626
669
  if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
627
670
  const keyName = s.names[0], valueName = s.names[1];
@@ -894,7 +937,7 @@ function parseDiscriminantCond(cond) {
894
937
  return null;
895
938
  return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
896
939
  }
897
- function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
940
+ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
898
941
  let someBranch = negated ? s.else : s.then;
899
942
  const noneBranch = negated ? s.then : s.else;
900
943
  // Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
@@ -903,19 +946,69 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
903
946
  someBranch = restStmts;
904
947
  }
905
948
  const bound = matchBinder(`${varName}_val`);
906
- const someBody = transformStmts(someBranch, typeDecls);
907
- const r = (e) => replaceVar(e, varName, { kind: "var", name: bound });
908
- const someReplaced = someBody.map(stmt => mapStmtExprs(stmt, r));
909
- const arms = [
910
- { pattern: `.some ${bound}`, body: someReplaced },
911
- { pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
912
- ];
913
- return { kind: "match", scrutinee: varName, arms };
949
+ // Replace the narrowed variable/field in the Some branch body.
950
+ // Field chains: replace in TStmt before transform (so downstream narrowing sees simple vars).
951
+ // Simple vars: replace in IR after transform (the original mechanism).
952
+ let someBody;
953
+ if (fieldExpr && fieldExpr.kind === "field" && fieldExpr.obj.kind === "var") {
954
+ const innerTy = fieldExpr.ty.kind === "optional" ? fieldExpr.ty.inner : fieldExpr.ty;
955
+ const replaced = replaceFieldsInTStmts(someBranch, fieldExpr.obj.name, [
956
+ { fieldName: fieldExpr.field, newName: bound, fallbackTy: innerTy },
957
+ ]);
958
+ someBody = transformStmts(replaced, typeDecls);
959
+ }
960
+ else {
961
+ const transformed = transformStmts(someBranch, typeDecls);
962
+ someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound })));
963
+ }
964
+ return {
965
+ kind: "match", scrutinee: varName,
966
+ arms: [
967
+ { pattern: `.some ${bound}`, body: someBody },
968
+ { pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
969
+ ],
970
+ };
914
971
  }
915
972
  /** Apply an expression transform to all expressions in a statement (convenience wrapper). */
916
973
  function mapStmtExprs(s, r) {
917
974
  return mapStmt(s, e => r(e));
918
975
  }
976
+ // ── Optional narrowing helpers ──────────────────────────────
977
+ //
978
+ // Optional narrowing converts TS `if (x === undefined)` patterns to Dafny
979
+ // `match x { Some(val) => ..., None => ... }`.
980
+ //
981
+ // The resolve phase (resolve.ts) handles:
982
+ // - Flow narrowing: after `if (x === undefined) return`, x is non-optional
983
+ // - && narrowing: in `x !== undefined && f(x)`, f(x) sees x as non-optional
984
+ // - Conditional narrowing: in `x !== undefined ? x.field : default`, sets
985
+ // narrowedVar/narrowedExpr on TExpr for the transform phase
986
+ //
987
+ // The transform phase (here) handles:
988
+ // - Statement-level: `transformStmts` detects optional checks → `emitOptionalMatch`
989
+ // - Expression-level: `lowerExpr` conditional reads narrowedVar/narrowedExpr → match
990
+ // - && restructuring: `extractLeftmostOptional` splits `&&` chains into nested ifs
991
+ // so `emitOptionalMatch` can detect the inner optional check
992
+ //
993
+ // Both phases detect `v !== undefined` patterns. The resolve phase uses
994
+ // `detectOptionalCheck` (on RawExpr), the transform uses `parseOptionalCheck` (on TExpr).
995
+ // These are separate because they operate on different IR types, but both handle
996
+ // simple variables and field access chains.
997
+ /** Shared logic for optional match in both imperative and pure function paths.
998
+ * Detects optional check, selects branches, handles early-return consumption.
999
+ * Returns null if the condition is not an optional check. */
1000
+ function prepareOptionalMatch(s, restStmts) {
1001
+ const check = parseOptionalCheck(s.cond);
1002
+ if (!check)
1003
+ return null;
1004
+ let someBranch = check.negated ? s.else : s.then;
1005
+ const noneBranch = check.negated ? s.then : (s.else.length > 0 ? s.else : restStmts);
1006
+ // Early-return pattern: Some branch is empty → consume rest of block
1007
+ if (someBranch.length === 0 && restStmts.length > 0)
1008
+ someBranch = restStmts;
1009
+ const bound = matchBinder(`${check.varName}_val`);
1010
+ return { check, someBranch, noneBranch, bound };
1011
+ }
919
1012
  /** Extract the leftmost optional check from a && chain, returning the check and the rest.
920
1013
  * (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
921
1014
  function extractLeftmostOptional(cond) {
@@ -931,7 +1024,9 @@ function extractLeftmostOptional(cond) {
931
1024
  }
932
1025
  return null;
933
1026
  }
934
- /** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
1027
+ /** Detect `v !== undefined` or `undefined !== v` where v has optional type.
1028
+ * Also handles field access chains like `obj.field !== undefined`.
1029
+ * When `fieldExpr` is returned, callers must use field-aware replacement. */
935
1030
  function parseOptionalCheck(cond) {
936
1031
  if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
937
1032
  return null;
@@ -940,21 +1035,50 @@ function parseOptionalCheck(cond) {
940
1035
  varExpr = cond.left;
941
1036
  if (cond.left.kind === "var" && cond.left.name === "undefined")
942
1037
  varExpr = cond.right;
943
- if (!varExpr || varExpr.kind !== "var" || varExpr.ty.kind !== "optional")
1038
+ if (!varExpr)
944
1039
  return null;
945
- return { varName: varExpr.name, negated: cond.op === "===" };
1040
+ if (varExpr.kind === "var" && varExpr.ty.kind === "optional") {
1041
+ return { varName: varExpr.name, negated: cond.op === "===" };
1042
+ }
1043
+ if (varExpr.kind === "field" && varExpr.ty.kind === "optional") {
1044
+ // Serialize field chain as a dotted name for use as match scrutinee
1045
+ const chain = serializeFieldChain(varExpr);
1046
+ if (chain)
1047
+ return { varName: chain, negated: cond.op === "===", fieldExpr: varExpr };
1048
+ }
1049
+ return null;
946
1050
  }
947
- function emitMatchStmt(chain, typeDecls) {
948
- const decl = typeDecls.find(d => d.name === chain.typeName);
949
- const arms = chain.cases.map(c => {
950
- const variant = decl?.variants?.find(v => v.name === c.variant);
1051
+ /** Serialize a field access chain to a dotted variable path, or null if not a simple chain. */
1052
+ function serializeFieldChain(e) {
1053
+ if (e.kind === "var")
1054
+ return e.name;
1055
+ if (e.kind === "field") {
1056
+ const parent = serializeFieldChain(e.obj);
1057
+ return parent ? `${parent}.${e.field}` : null;
1058
+ }
1059
+ return null;
1060
+ }
1061
+ /** Build match arms from variant cases — shared by imperative and pure paths.
1062
+ * Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
1063
+ * and delegates body transformation to the caller-provided function.
1064
+ * Returns null if any body transformation returns null (pure path abort). */
1065
+ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1066
+ const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
1067
+ const arms = [];
1068
+ for (const c of cases) {
1069
+ const variant = decl?.variants?.find(v => v.name === c.name);
951
1070
  const fields = variant?.fields ?? [];
952
- const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
953
- // Replace field accesses in TStmt BEFORE transforming, so optional narrowing sees simple vars
954
- const replaced = replaceFieldAccessInTStmts(c.body, chain.varName, fields);
955
- const body = transformStmts(replaced, typeDecls);
956
- return { pattern, body };
957
- });
1071
+ const pattern = buildMatchPattern(c.name, fields, varName);
1072
+ const body = transformBody(c.body, varName, fields);
1073
+ if (body === null)
1074
+ return null;
1075
+ arms.push({ pattern, body });
1076
+ }
1077
+ return arms;
1078
+ }
1079
+ function emitMatchStmt(chain, typeDecls) {
1080
+ const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
1081
+ const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
958
1082
  if (chain.fallthrough.length > 0)
959
1083
  arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
960
1084
  return { kind: "match", scrutinee: chain.varName, arms };
@@ -962,59 +1086,39 @@ function emitMatchStmt(chain, typeDecls) {
962
1086
  function emitSwitchStmt(s, typeDecls) {
963
1087
  const varName = s.expr.kind === "var" ? s.expr.name : "?";
964
1088
  const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
965
- const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
966
- const arms = s.cases.map(c => {
967
- const variant = decl?.variants?.find(v => v.name === c.label);
968
- const fields = variant?.fields ?? [];
969
- const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
970
- // Replace field accesses in TStmt BEFORE transforming, so optional narrowing sees simple vars
971
- const replaced = replaceFieldAccessInTStmts(c.body, varName, fields);
972
- const body = transformStmts(replaced, typeDecls);
973
- return { pattern, body };
974
- });
1089
+ const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1090
+ const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
975
1091
  if (s.defaultBody.length > 0)
976
1092
  arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
977
1093
  return { kind: "match", scrutinee: varName, arms };
978
1094
  }
979
- /** Replace obj.field → binder var in typed IR (before transform).
980
- * Uses the variant's declared field type since the resolve phase may not
981
- * resolve field types on discriminated unions correctly. */
982
- function replaceFieldAccessInTStmts(stmts, varName, fields) {
983
- if (fields.length === 0)
1095
+ /** Replace obj.field → replacement var in typed IR (before transform).
1096
+ * Used by discriminant match/switch and optional match to rewrite field accesses
1097
+ * into simple variables before the transform phase, so downstream narrowing
1098
+ * (parseOptionalCheck, extractLeftmostOptional) sees simple variable references.
1099
+ * Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
1100
+ function replaceFieldsInTStmts(stmts, objName, replacements) {
1101
+ if (replacements.length === 0)
984
1102
  return stmts;
985
1103
  return stmts.map(s => mapTStmt(s, e => {
986
- if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
987
- const fi = fields.find(fi => fi.name === e.field);
988
- if (fi) {
989
- const ty = e.ty.kind !== "unknown" ? e.ty : parseTsType(fi.tsType);
990
- return { kind: "var", name: matchBinder(fi.name, varName), ty };
1104
+ if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === objName) {
1105
+ const r = replacements.find(r => r.fieldName === e.field);
1106
+ if (r) {
1107
+ const ty = e.ty.kind !== "unknown" ? e.ty : r.fallbackTy;
1108
+ return { kind: "var", name: r.newName, ty };
991
1109
  }
992
1110
  }
993
1111
  return null;
994
1112
  }));
995
1113
  }
996
- function replaceFieldAccessInStmts(stmts, varName, fields) {
997
- if (fields.length === 0)
998
- return stmts;
999
- const f = (e) => {
1000
- if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
1001
- const fi = fields.find(fi => fi.name === e.field);
1002
- if (fi)
1003
- return { kind: "var", name: matchBinder(fi.name, varName) };
1004
- }
1005
- return null;
1006
- };
1007
- const result = [];
1008
- for (const s of stmts) {
1009
- // If a let shadows the matched variable, stop replacing from here on
1010
- if (s.kind === "let" && s.name === varName) {
1011
- result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
1012
- result.push(...stmts.slice(result.length));
1013
- break;
1014
- }
1015
- result.push(mapStmt(s, f));
1016
- }
1017
- return result;
1114
+ /** Replace all variant fields of obj → match binder vars in typed IR.
1115
+ * Thin wrapper around replaceFieldsInTStmts for discriminant match/switch. */
1116
+ function replaceFieldAccessInTStmts(stmts, varName, fields) {
1117
+ return replaceFieldsInTStmts(stmts, varName, fields.map(f => ({
1118
+ fieldName: f.name,
1119
+ newName: matchBinder(f.name, varName),
1120
+ fallbackTy: f.type ?? parseTsType(f.tsType),
1121
+ })));
1018
1122
  }
1019
1123
  // ── Pure function generation ─────────────────────────────────
1020
1124
  function transformPureBody(stmts, typeDecls) {
@@ -1037,24 +1141,19 @@ function transformPureBody(stmts, typeDecls) {
1037
1141
  }
1038
1142
  case "if": {
1039
1143
  // Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
1040
- const optCheck = parseOptionalCheck(s.cond);
1041
- if (optCheck) {
1042
- let someBranch = optCheck.negated ? s.else : s.then;
1043
- const noneBranch = optCheck.negated ? s.then : (s.else.length > 0 ? s.else : rest);
1044
- if (someBranch.length === 0)
1045
- someBranch = rest;
1046
- const bound = matchBinder(`${optCheck.varName}_val`);
1047
- const someExpr = transformPureBody(someBranch, typeDecls);
1144
+ const optMatch = prepareOptionalMatch(s, rest);
1145
+ if (optMatch) {
1146
+ const someExpr = transformPureBody(optMatch.someBranch, typeDecls);
1048
1147
  if (!someExpr)
1049
1148
  return null;
1050
- const noneExpr = transformPureBody(noneBranch, typeDecls);
1149
+ const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
1051
1150
  if (!noneExpr)
1052
1151
  return null;
1053
- const someReplaced = replaceVar(someExpr, optCheck.varName, { kind: "var", name: bound });
1152
+ const someReplaced = replaceVar(someExpr, optMatch.check.varName, { kind: "var", name: optMatch.bound });
1054
1153
  return {
1055
- kind: "match", scrutinee: optCheck.varName,
1154
+ kind: "match", scrutinee: optMatch.check.varName,
1056
1155
  arms: [
1057
- { pattern: `.some ${bound}`, body: someReplaced },
1156
+ { pattern: `.some ${optMatch.bound}`, body: someReplaced },
1058
1157
  { pattern: ".none", body: noneExpr },
1059
1158
  ],
1060
1159
  };
@@ -1075,22 +1174,21 @@ function transformPureBody(stmts, typeDecls) {
1075
1174
  return null;
1076
1175
  }
1077
1176
  function transformPureSwitch(s, typeDecls) {
1078
- const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
1079
- if (!decl)
1177
+ const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
1178
+ if (!typeDecls.find(d => d.name === typeName))
1080
1179
  return null;
1081
1180
  const varName = s.expr.kind === "var" ? s.expr.name : undefined;
1082
- const arms = [];
1083
- for (const c of s.cases) {
1084
- const variant = decl.variants?.find(v => v.name === c.label);
1085
- const fields = variant?.fields ?? [];
1086
- const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
1087
- let body = transformPureBody(c.body, typeDecls);
1088
- if (!body)
1181
+ const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1182
+ const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => {
1183
+ let result = transformPureBody(body, typeDecls);
1184
+ if (!result)
1089
1185
  return null;
1090
- if (fields.length > 0 && s.expr.kind === "var")
1091
- body = replaceFieldAccess(body, s.expr.name, fields);
1092
- arms.push({ pattern, body });
1093
- }
1186
+ if (fields.length > 0 && vn)
1187
+ result = replaceFieldAccess(result, vn, fields);
1188
+ return result;
1189
+ });
1190
+ if (!arms)
1191
+ return null;
1094
1192
  if (s.defaultBody.length > 0) {
1095
1193
  const body = transformPureBody(s.defaultBody, typeDecls);
1096
1194
  if (!body)
@@ -1102,22 +1200,21 @@ function transformPureSwitch(s, typeDecls) {
1102
1200
  return { kind: "match", scrutinee: s.expr.name, arms };
1103
1201
  }
1104
1202
  function transformPureMatch(chain, typeDecls) {
1105
- const decl = typeDecls.find(d => d.name === chain.typeName);
1106
- const arms = [];
1107
- for (const c of chain.cases) {
1108
- const variant = decl?.variants?.find(v => v.name === c.variant);
1109
- const fields = variant?.fields ?? [];
1110
- const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
1111
- let body = transformPureBody(c.body, typeDecls);
1112
- if (!body)
1203
+ const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
1204
+ const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
1205
+ let result = transformPureBody(body, typeDecls);
1206
+ if (!result)
1113
1207
  return null;
1114
- if (fields.length > 0)
1115
- body = replaceFieldAccess(body, chain.varName, fields);
1116
- arms.push({ pattern, body });
1117
- }
1208
+ if (fields.length > 0 && vn)
1209
+ result = replaceFieldAccess(result, vn, fields);
1210
+ return result;
1211
+ });
1212
+ if (!arms)
1213
+ return null;
1118
1214
  // Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
1119
1215
  // discriminated unions. Skip the catch-all arm when all variants are matched,
1120
1216
  // since Lean errors on redundant match arms.
1217
+ const decl = typeDecls.find(d => d.name === chain.typeName);
1121
1218
  const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
1122
1219
  if (chain.fallthrough.length > 0 && !allCovered) {
1123
1220
  const body = transformPureBody(chain.fallthrough, typeDecls);
@@ -1142,7 +1239,7 @@ function transformTypeDecl(d) {
1142
1239
  typeParams: d.typeParams,
1143
1240
  constructors: d.variants.map(v => ({
1144
1241
  name: v.name,
1145
- fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
1242
+ fields: v.fields.map(f => ({ name: f.name, type: f.type })),
1146
1243
  })),
1147
1244
  deriving: ["Repr", "Inhabited"],
1148
1245
  };
@@ -1150,13 +1247,13 @@ function transformTypeDecl(d) {
1150
1247
  else if (d.kind === "alias") {
1151
1248
  return {
1152
1249
  kind: "type-alias", name: d.name,
1153
- target: parseTsType(d.aliasOf),
1250
+ target: d.aliasOfTy,
1154
1251
  };
1155
1252
  }
1156
1253
  else {
1157
1254
  return {
1158
1255
  kind: "structure", name: d.name,
1159
- fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
1256
+ fields: d.fields.map(f => ({ name: f.name, type: f.type })),
1160
1257
  deriving: ["Repr", "Inhabited", "DecidableEq"],
1161
1258
  };
1162
1259
  }
@@ -1235,6 +1332,7 @@ export function transformModuleDafny(mod) {
1235
1332
  }
1236
1333
  export function transformModule(mod, specImport) {
1237
1334
  _forofCounters.clear();
1335
+ _liftCounter = 0;
1238
1336
  _typeDecls = mod.typeDecls;
1239
1337
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
1240
1338
  // Module-level constants