lemmascript 0.3.0 → 0.3.1

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
@@ -427,23 +437,41 @@ function lowerExpr(e, binds) {
427
437
  }
428
438
  }
429
439
  }
430
- // For spread records, wrap non-optional values in Some for optional fields
440
+ // For spread records, propagate declared field types and wrap optionals
431
441
  if (e.spread) {
432
442
  const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
433
443
  const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
434
444
  const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
445
+ // Also check discriminated-union variants for field types
446
+ const unionDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "discriminated-union") : undefined;
435
447
  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] };
448
+ // Propagate declared field type onto value if it has unknown type
449
+ let fieldValue = f.value;
450
+ const fieldDecl = structDecl?.fields?.find(sf => sf.name === f.name);
451
+ let declaredTy;
452
+ if (fieldDecl) {
453
+ declaredTy = fieldDecl.type;
454
+ }
455
+ else if (unionDecl?.variants) {
456
+ for (const v of unionDecl.variants) {
457
+ const vf = v.fields.find(vf => vf.name === f.name);
458
+ if (vf) {
459
+ declaredTy = vf.type;
460
+ break;
444
461
  }
445
462
  }
446
463
  }
464
+ if (declaredTy && fieldValue.ty.kind === "unknown") {
465
+ fieldValue = { ...fieldValue, ty: declaredTy };
466
+ }
467
+ let value = lowerExpr(fieldValue, binds);
468
+ // Wrap non-optional values in Some for optional fields
469
+ if (declaredTy?.kind === "optional") {
470
+ const isUndef = f.value.kind === "var" && f.value.name === "undefined";
471
+ if (f.value.ty.kind !== "optional" && !isUndef) {
472
+ value = { kind: "app", fn: "Some", args: [value] };
473
+ }
474
+ }
447
475
  return { name: f.name, value };
448
476
  });
449
477
  return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
@@ -466,7 +494,9 @@ function lowerExpr(e, binds) {
466
494
  case "exists":
467
495
  return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
468
496
  case "conditional": {
469
- const cond = lowerExpr(e.cond, binds);
497
+ // When narrowedExpr is set, the match replaces the condition — don't lift from it
498
+ const condBinds = (e.narrowedVar && e.narrowedExpr) ? null : binds;
499
+ const cond = lowerExpr(e.cond, condBinds);
470
500
  let thenExpr = lowerExpr(e.then, binds);
471
501
  let elseExpr = lowerExpr(e.else, binds);
472
502
  // Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
@@ -476,11 +506,11 @@ function lowerExpr(e, binds) {
476
506
  if (bound !== e.narrowedVar) {
477
507
  thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
478
508
  }
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);
509
+ // Wrap in Some/None only when result is optional (one branch is undefined)
510
+ if (e.ty.kind === "optional") {
511
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
512
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
513
+ }
484
514
  return {
485
515
  kind: "match", scrutinee,
486
516
  arms: [
@@ -497,12 +527,8 @@ function lowerExpr(e, binds) {
497
527
  thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
498
528
  }
499
529
  // 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);
530
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
531
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
506
532
  return {
507
533
  kind: "match", scrutinee: cond,
508
534
  arms: [
@@ -513,18 +539,8 @@ function lowerExpr(e, binds) {
513
539
  }
514
540
  // Non-optional: regular if with optional wrapping
515
541
  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
- }
542
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
543
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
528
544
  }
529
545
  return { kind: "if", cond, then: thenExpr, else: elseExpr };
530
546
  }
@@ -571,7 +587,7 @@ function ensuresToMatch(e, typeDecls) {
571
587
  if (!variant)
572
588
  return null;
573
589
  const fields = variant.fields;
574
- const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name, obj.name)).join(" ")}` : `.${variantName}`;
590
+ const pattern = buildMatchPattern(variantName, fields, obj.name);
575
591
  let rhs = transformExpr(e.right);
576
592
  rhs = replaceFieldAccess(rhs, obj.name, fields);
577
593
  return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
@@ -604,13 +620,12 @@ function transformStmts(stmts, typeDecls) {
604
620
  continue;
605
621
  }
606
622
  // 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));
623
+ const optMatch = prepareOptionalMatch(s, stmts.slice(i + 1));
624
+ if (optMatch) {
625
+ result.push(emitOptionalMatch(optMatch.check.varName, optMatch.check.negated, s, typeDecls, stmts.slice(i + 1), optMatch.check.fieldExpr));
611
626
  // 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) {
627
+ const origSome = optMatch.check.negated ? s.else : s.then;
628
+ if (origSome.length === 0 && i + 1 < stmts.length) {
614
629
  return result;
615
630
  }
616
631
  i++;
@@ -894,7 +909,7 @@ function parseDiscriminantCond(cond) {
894
909
  return null;
895
910
  return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
896
911
  }
897
- function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
912
+ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
898
913
  let someBranch = negated ? s.else : s.then;
899
914
  const noneBranch = negated ? s.then : s.else;
900
915
  // Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
@@ -903,19 +918,69 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
903
918
  someBranch = restStmts;
904
919
  }
905
920
  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 };
921
+ // Replace the narrowed variable/field in the Some branch body.
922
+ // Field chains: replace in TStmt before transform (so downstream narrowing sees simple vars).
923
+ // Simple vars: replace in IR after transform (the original mechanism).
924
+ let someBody;
925
+ if (fieldExpr && fieldExpr.kind === "field" && fieldExpr.obj.kind === "var") {
926
+ const innerTy = fieldExpr.ty.kind === "optional" ? fieldExpr.ty.inner : fieldExpr.ty;
927
+ const replaced = replaceFieldsInTStmts(someBranch, fieldExpr.obj.name, [
928
+ { fieldName: fieldExpr.field, newName: bound, fallbackTy: innerTy },
929
+ ]);
930
+ someBody = transformStmts(replaced, typeDecls);
931
+ }
932
+ else {
933
+ const transformed = transformStmts(someBranch, typeDecls);
934
+ someBody = transformed.map(stmt => mapStmtExprs(stmt, e => replaceVar(e, varName, { kind: "var", name: bound })));
935
+ }
936
+ return {
937
+ kind: "match", scrutinee: varName,
938
+ arms: [
939
+ { pattern: `.some ${bound}`, body: someBody },
940
+ { pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
941
+ ],
942
+ };
914
943
  }
915
944
  /** Apply an expression transform to all expressions in a statement (convenience wrapper). */
916
945
  function mapStmtExprs(s, r) {
917
946
  return mapStmt(s, e => r(e));
918
947
  }
948
+ // ── Optional narrowing helpers ──────────────────────────────
949
+ //
950
+ // Optional narrowing converts TS `if (x === undefined)` patterns to Dafny
951
+ // `match x { Some(val) => ..., None => ... }`.
952
+ //
953
+ // The resolve phase (resolve.ts) handles:
954
+ // - Flow narrowing: after `if (x === undefined) return`, x is non-optional
955
+ // - && narrowing: in `x !== undefined && f(x)`, f(x) sees x as non-optional
956
+ // - Conditional narrowing: in `x !== undefined ? x.field : default`, sets
957
+ // narrowedVar/narrowedExpr on TExpr for the transform phase
958
+ //
959
+ // The transform phase (here) handles:
960
+ // - Statement-level: `transformStmts` detects optional checks → `emitOptionalMatch`
961
+ // - Expression-level: `lowerExpr` conditional reads narrowedVar/narrowedExpr → match
962
+ // - && restructuring: `extractLeftmostOptional` splits `&&` chains into nested ifs
963
+ // so `emitOptionalMatch` can detect the inner optional check
964
+ //
965
+ // Both phases detect `v !== undefined` patterns. The resolve phase uses
966
+ // `detectOptionalCheck` (on RawExpr), the transform uses `parseOptionalCheck` (on TExpr).
967
+ // These are separate because they operate on different IR types, but both handle
968
+ // simple variables and field access chains.
969
+ /** Shared logic for optional match in both imperative and pure function paths.
970
+ * Detects optional check, selects branches, handles early-return consumption.
971
+ * Returns null if the condition is not an optional check. */
972
+ function prepareOptionalMatch(s, restStmts) {
973
+ const check = parseOptionalCheck(s.cond);
974
+ if (!check)
975
+ return null;
976
+ let someBranch = check.negated ? s.else : s.then;
977
+ const noneBranch = check.negated ? s.then : (s.else.length > 0 ? s.else : restStmts);
978
+ // Early-return pattern: Some branch is empty → consume rest of block
979
+ if (someBranch.length === 0 && restStmts.length > 0)
980
+ someBranch = restStmts;
981
+ const bound = matchBinder(`${check.varName}_val`);
982
+ return { check, someBranch, noneBranch, bound };
983
+ }
919
984
  /** Extract the leftmost optional check from a && chain, returning the check and the rest.
920
985
  * (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
921
986
  function extractLeftmostOptional(cond) {
@@ -931,7 +996,9 @@ function extractLeftmostOptional(cond) {
931
996
  }
932
997
  return null;
933
998
  }
934
- /** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
999
+ /** Detect `v !== undefined` or `undefined !== v` where v has optional type.
1000
+ * Also handles field access chains like `obj.field !== undefined`.
1001
+ * When `fieldExpr` is returned, callers must use field-aware replacement. */
935
1002
  function parseOptionalCheck(cond) {
936
1003
  if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
937
1004
  return null;
@@ -940,21 +1007,50 @@ function parseOptionalCheck(cond) {
940
1007
  varExpr = cond.left;
941
1008
  if (cond.left.kind === "var" && cond.left.name === "undefined")
942
1009
  varExpr = cond.right;
943
- if (!varExpr || varExpr.kind !== "var" || varExpr.ty.kind !== "optional")
1010
+ if (!varExpr)
944
1011
  return null;
945
- return { varName: varExpr.name, negated: cond.op === "===" };
1012
+ if (varExpr.kind === "var" && varExpr.ty.kind === "optional") {
1013
+ return { varName: varExpr.name, negated: cond.op === "===" };
1014
+ }
1015
+ if (varExpr.kind === "field" && varExpr.ty.kind === "optional") {
1016
+ // Serialize field chain as a dotted name for use as match scrutinee
1017
+ const chain = serializeFieldChain(varExpr);
1018
+ if (chain)
1019
+ return { varName: chain, negated: cond.op === "===", fieldExpr: varExpr };
1020
+ }
1021
+ return null;
946
1022
  }
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);
1023
+ /** Serialize a field access chain to a dotted variable path, or null if not a simple chain. */
1024
+ function serializeFieldChain(e) {
1025
+ if (e.kind === "var")
1026
+ return e.name;
1027
+ if (e.kind === "field") {
1028
+ const parent = serializeFieldChain(e.obj);
1029
+ return parent ? `${parent}.${e.field}` : null;
1030
+ }
1031
+ return null;
1032
+ }
1033
+ /** Build match arms from variant cases — shared by imperative and pure paths.
1034
+ * Looks up variant fields from typeDecls, builds patterns via buildMatchPattern,
1035
+ * and delegates body transformation to the caller-provided function.
1036
+ * Returns null if any body transformation returns null (pure path abort). */
1037
+ function buildMatchArms(cases, varName, typeName, typeDecls, transformBody) {
1038
+ const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
1039
+ const arms = [];
1040
+ for (const c of cases) {
1041
+ const variant = decl?.variants?.find(v => v.name === c.name);
951
1042
  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
- });
1043
+ const pattern = buildMatchPattern(c.name, fields, varName);
1044
+ const body = transformBody(c.body, varName, fields);
1045
+ if (body === null)
1046
+ return null;
1047
+ arms.push({ pattern, body });
1048
+ }
1049
+ return arms;
1050
+ }
1051
+ function emitMatchStmt(chain, typeDecls) {
1052
+ const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
1053
+ const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
958
1054
  if (chain.fallthrough.length > 0)
959
1055
  arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
960
1056
  return { kind: "match", scrutinee: chain.varName, arms };
@@ -962,59 +1058,39 @@ function emitMatchStmt(chain, typeDecls) {
962
1058
  function emitSwitchStmt(s, typeDecls) {
963
1059
  const varName = s.expr.kind === "var" ? s.expr.name : "?";
964
1060
  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
- });
1061
+ const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1062
+ const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => transformStmts(replaceFieldAccessInTStmts(body, vn, fields), typeDecls));
975
1063
  if (s.defaultBody.length > 0)
976
1064
  arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
977
1065
  return { kind: "match", scrutinee: varName, arms };
978
1066
  }
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)
1067
+ /** Replace obj.field → replacement var in typed IR (before transform).
1068
+ * Used by discriminant match/switch and optional match to rewrite field accesses
1069
+ * into simple variables before the transform phase, so downstream narrowing
1070
+ * (parseOptionalCheck, extractLeftmostOptional) sees simple variable references.
1071
+ * Uses the TExpr's resolved type when available, falling back to `fallbackTy`. */
1072
+ function replaceFieldsInTStmts(stmts, objName, replacements) {
1073
+ if (replacements.length === 0)
984
1074
  return stmts;
985
1075
  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 };
1076
+ if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === objName) {
1077
+ const r = replacements.find(r => r.fieldName === e.field);
1078
+ if (r) {
1079
+ const ty = e.ty.kind !== "unknown" ? e.ty : r.fallbackTy;
1080
+ return { kind: "var", name: r.newName, ty };
991
1081
  }
992
1082
  }
993
1083
  return null;
994
1084
  }));
995
1085
  }
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;
1086
+ /** Replace all variant fields of obj → match binder vars in typed IR.
1087
+ * Thin wrapper around replaceFieldsInTStmts for discriminant match/switch. */
1088
+ function replaceFieldAccessInTStmts(stmts, varName, fields) {
1089
+ return replaceFieldsInTStmts(stmts, varName, fields.map(f => ({
1090
+ fieldName: f.name,
1091
+ newName: matchBinder(f.name, varName),
1092
+ fallbackTy: f.type ?? parseTsType(f.tsType),
1093
+ })));
1018
1094
  }
1019
1095
  // ── Pure function generation ─────────────────────────────────
1020
1096
  function transformPureBody(stmts, typeDecls) {
@@ -1037,24 +1113,19 @@ function transformPureBody(stmts, typeDecls) {
1037
1113
  }
1038
1114
  case "if": {
1039
1115
  // 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);
1116
+ const optMatch = prepareOptionalMatch(s, rest);
1117
+ if (optMatch) {
1118
+ const someExpr = transformPureBody(optMatch.someBranch, typeDecls);
1048
1119
  if (!someExpr)
1049
1120
  return null;
1050
- const noneExpr = transformPureBody(noneBranch, typeDecls);
1121
+ const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
1051
1122
  if (!noneExpr)
1052
1123
  return null;
1053
- const someReplaced = replaceVar(someExpr, optCheck.varName, { kind: "var", name: bound });
1124
+ const someReplaced = replaceVar(someExpr, optMatch.check.varName, { kind: "var", name: optMatch.bound });
1054
1125
  return {
1055
- kind: "match", scrutinee: optCheck.varName,
1126
+ kind: "match", scrutinee: optMatch.check.varName,
1056
1127
  arms: [
1057
- { pattern: `.some ${bound}`, body: someReplaced },
1128
+ { pattern: `.some ${optMatch.bound}`, body: someReplaced },
1058
1129
  { pattern: ".none", body: noneExpr },
1059
1130
  ],
1060
1131
  };
@@ -1075,22 +1146,21 @@ function transformPureBody(stmts, typeDecls) {
1075
1146
  return null;
1076
1147
  }
1077
1148
  function transformPureSwitch(s, typeDecls) {
1078
- const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
1079
- if (!decl)
1149
+ const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
1150
+ if (!typeDecls.find(d => d.name === typeName))
1080
1151
  return null;
1081
1152
  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)
1153
+ const cases = s.cases.map(c => ({ name: c.label, body: c.body }));
1154
+ const arms = buildMatchArms(cases, varName, typeName, typeDecls, (body, vn, fields) => {
1155
+ let result = transformPureBody(body, typeDecls);
1156
+ if (!result)
1089
1157
  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
- }
1158
+ if (fields.length > 0 && vn)
1159
+ result = replaceFieldAccess(result, vn, fields);
1160
+ return result;
1161
+ });
1162
+ if (!arms)
1163
+ return null;
1094
1164
  if (s.defaultBody.length > 0) {
1095
1165
  const body = transformPureBody(s.defaultBody, typeDecls);
1096
1166
  if (!body)
@@ -1102,22 +1172,21 @@ function transformPureSwitch(s, typeDecls) {
1102
1172
  return { kind: "match", scrutinee: s.expr.name, arms };
1103
1173
  }
1104
1174
  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)
1175
+ const cases = chain.cases.map(c => ({ name: c.variant, body: c.body }));
1176
+ const arms = buildMatchArms(cases, chain.varName, chain.typeName, typeDecls, (body, vn, fields) => {
1177
+ let result = transformPureBody(body, typeDecls);
1178
+ if (!result)
1113
1179
  return null;
1114
- if (fields.length > 0)
1115
- body = replaceFieldAccess(body, chain.varName, fields);
1116
- arms.push({ pattern, body });
1117
- }
1180
+ if (fields.length > 0 && vn)
1181
+ result = replaceFieldAccess(result, vn, fields);
1182
+ return result;
1183
+ });
1184
+ if (!arms)
1185
+ return null;
1118
1186
  // Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
1119
1187
  // discriminated unions. Skip the catch-all arm when all variants are matched,
1120
1188
  // since Lean errors on redundant match arms.
1189
+ const decl = typeDecls.find(d => d.name === chain.typeName);
1121
1190
  const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
1122
1191
  if (chain.fallthrough.length > 0 && !allCovered) {
1123
1192
  const body = transformPureBody(chain.fallthrough, typeDecls);
@@ -1142,7 +1211,7 @@ function transformTypeDecl(d) {
1142
1211
  typeParams: d.typeParams,
1143
1212
  constructors: d.variants.map(v => ({
1144
1213
  name: v.name,
1145
- fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
1214
+ fields: v.fields.map(f => ({ name: f.name, type: f.type })),
1146
1215
  })),
1147
1216
  deriving: ["Repr", "Inhabited"],
1148
1217
  };
@@ -1150,13 +1219,13 @@ function transformTypeDecl(d) {
1150
1219
  else if (d.kind === "alias") {
1151
1220
  return {
1152
1221
  kind: "type-alias", name: d.name,
1153
- target: parseTsType(d.aliasOf),
1222
+ target: d.aliasOfTy,
1154
1223
  };
1155
1224
  }
1156
1225
  else {
1157
1226
  return {
1158
1227
  kind: "structure", name: d.name,
1159
- fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
1228
+ fields: d.fields.map(f => ({ name: f.name, type: f.type })),
1160
1229
  deriving: ["Repr", "Inhabited", "DecidableEq"],
1161
1230
  };
1162
1231
  }
@@ -1235,6 +1304,7 @@ export function transformModuleDafny(mod) {
1235
1304
  }
1236
1305
  export function transformModule(mod, specImport) {
1237
1306
  _forofCounters.clear();
1307
+ _liftCounter = 0;
1238
1308
  _typeDecls = mod.typeDecls;
1239
1309
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
1240
1310
  // Module-level constants