lemmascript 0.2.0 → 0.3.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.
@@ -35,7 +35,10 @@ function mapExpr(e, f) {
35
35
  case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
36
36
  case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
37
37
  case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
38
- case "match": return { ...e, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
38
+ case "match": {
39
+ const scr = typeof e.scrutinee === "string" ? e.scrutinee : r(e.scrutinee);
40
+ return { ...e, scrutinee: scr, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
41
+ }
39
42
  case "forall": return { ...e, body: r(e.body) };
40
43
  case "exists": return { ...e, body: r(e.body) };
41
44
  case "let": return { ...e, value: r(e.value), body: r(e.body) };
@@ -66,6 +69,52 @@ function mapStmt(s, f) {
66
69
  function mapStmts(stmts, f) {
67
70
  return stmts.map(s => mapStmt(s, f));
68
71
  }
72
+ /** Map over all sub-expressions in a TExpr (typed IR). */
73
+ function mapTExpr(e, f) {
74
+ const hit = f(e);
75
+ if (hit)
76
+ return hit;
77
+ const r = (x) => mapTExpr(x, f);
78
+ switch (e.kind) {
79
+ case "var":
80
+ case "num":
81
+ case "str":
82
+ case "bool":
83
+ case "result":
84
+ case "havoc": return e;
85
+ case "binop": return { ...e, left: r(e.left), right: r(e.right) };
86
+ case "unop": return { ...e, expr: r(e.expr) };
87
+ case "call": return { ...e, fn: r(e.fn), args: e.args.map(r) };
88
+ case "index": return { ...e, obj: r(e.obj), idx: r(e.idx) };
89
+ case "field": return { ...e, obj: r(e.obj) };
90
+ case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
91
+ case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
92
+ case "conditional": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
93
+ case "forall": return { ...e, body: r(e.body) };
94
+ case "exists": return { ...e, body: r(e.body) };
95
+ case "lambda": return e;
96
+ }
97
+ }
98
+ /** Map over all expressions in a TStmt tree (typed IR). */
99
+ function mapTStmt(s, f) {
100
+ const r = (e) => mapTExpr(e, f);
101
+ switch (s.kind) {
102
+ case "let": return { ...s, init: r(s.init) };
103
+ case "assign": return { ...s, value: r(s.value) };
104
+ case "return": return { ...s, value: r(s.value) };
105
+ case "break":
106
+ case "continue":
107
+ case "throw": return s;
108
+ case "expr": return { ...s, expr: r(s.expr) };
109
+ case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapTStmt(t, f)), else: s.else.map(t => mapTStmt(t, f)) };
110
+ case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
111
+ case "switch": return { ...s, expr: r(s.expr), cases: s.cases.map(c => ({ ...c, body: c.body.map(t => mapTStmt(t, f)) })), defaultBody: s.defaultBody.map(t => mapTStmt(t, f)) };
112
+ case "forof": return { ...s, iterable: r(s.iterable), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
113
+ case "ghostLet": return { ...s, init: r(s.init) };
114
+ case "ghostAssign": return { ...s, value: r(s.value) };
115
+ case "assert": return { ...s, expr: r(s.expr) };
116
+ }
117
+ }
69
118
  export const LEAN_OPTIONS = {
70
119
  backend: "lean",
71
120
  monadic: true,
@@ -75,10 +124,14 @@ export const DAFNY_OPTIONS = {
75
124
  monadic: false,
76
125
  };
77
126
  /** Active options — set before each transform call. */
78
- let _opts = LEAN_OPTIONS;
79
- /** Prefix match-bound field names to avoid capturing user variables. */
80
- function matchBinder(fieldName) {
81
- return `_${fieldName}`;
127
+ let _opts = DAFNY_OPTIONS;
128
+ /** Type declarations set once per module transform for discriminated union handling. */
129
+ let _typeDecls = [];
130
+ /** Prefix match-bound field names to avoid capturing user variables.
131
+ * When prefix is given (the scrutinee name), include it to avoid
132
+ * collisions in nested matches on different variables. */
133
+ function matchBinder(fieldName, prefix) {
134
+ return prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`;
82
135
  }
83
136
  const _forofCounters = new Map();
84
137
  function isNat(ty) { return ty.kind === "nat"; }
@@ -215,6 +268,11 @@ function lowerExpr(e, binds) {
215
268
  ],
216
269
  };
217
270
  }
271
+ // || undefined on optional → identity (no-op: x || undefined = x)
272
+ if (e.op === "||" && e.left.ty.kind === "optional" &&
273
+ e.right.kind === "var" && e.right.name === "undefined") {
274
+ return lowerExpr(e.left, binds);
275
+ }
218
276
  // || on optional → match Some/None with default
219
277
  if (e.op === "||" && e.left.ty.kind === "optional") {
220
278
  const optExpr = lowerExpr(e.left, binds);
@@ -228,6 +286,43 @@ function lowerExpr(e, binds) {
228
286
  ],
229
287
  };
230
288
  }
289
+ // || on map index → if key in map then map[key] else default
290
+ if (e.op === "||" && e.left.kind === "index" && e.left.obj.ty.kind === "map") {
291
+ const map = lowerExpr(e.left.obj, binds);
292
+ const key = lowerExpr(e.left.idx, binds);
293
+ const right = lowerExpr(e.right, binds);
294
+ return {
295
+ kind: "if",
296
+ cond: { kind: "binop", op: "in", left: key, right: map },
297
+ then: { kind: "index", arr: map, idx: key }, else: right,
298
+ };
299
+ }
300
+ // || on non-optional string/array/user → if non-empty then x else default
301
+ if (e.op === "||" && (e.left.ty.kind === "string" || e.left.ty.kind === "array" ||
302
+ (e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
303
+ const left = lowerExpr(e.left, binds);
304
+ const right = lowerExpr(e.right, binds);
305
+ return {
306
+ kind: "if",
307
+ cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: "size" }, right: { kind: "num", value: 0 } },
308
+ then: left, else: right,
309
+ };
310
+ }
311
+ // int + string → NatToString(int) + string (string concatenation)
312
+ if (e.op === "+" && _opts.backend === "dafny") {
313
+ const isIntL = e.left.ty.kind === "int" || e.left.ty.kind === "nat";
314
+ const isIntR = e.right.ty.kind === "int" || e.right.ty.kind === "nat";
315
+ if (isIntL && e.right.ty.kind === "string") {
316
+ return { kind: "binop", op: "+",
317
+ left: { kind: "app", fn: "NatToString", args: [lowerExpr(e.left, binds)] },
318
+ right: lowerExpr(e.right, binds) };
319
+ }
320
+ if (e.left.ty.kind === "string" && isIntR) {
321
+ return { kind: "binop", op: "+",
322
+ left: lowerExpr(e.left, binds),
323
+ right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
324
+ }
325
+ }
231
326
  return {
232
327
  kind: "binop",
233
328
  op: OP_MAP[e.op] ?? e.op,
@@ -249,6 +344,15 @@ function lowerExpr(e, binds) {
249
344
  return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
250
345
  }
251
346
  case "call": {
347
+ // Math.abs/min/max → preamble functions
348
+ if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
349
+ if (e.fn.field === "abs" && e.args.length === 1)
350
+ return { kind: "app", fn: "MathAbs", args: [lowerExpr(e.args[0], binds)] };
351
+ if (e.fn.field === "min" && e.args.length === 2)
352
+ return { kind: "app", fn: "MathMin", args: e.args.map(a => lowerExpr(a, binds)) };
353
+ if (e.fn.field === "max" && e.args.length === 2)
354
+ return { kind: "app", fn: "MathMax", args: e.args.map(a => lowerExpr(a, binds)) };
355
+ }
252
356
  // Math.ceil(x): CeilReal on real args, identity on int
253
357
  if (e.fn.kind === "field" && e.fn.field === "ceil" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
254
358
  const arg = e.args[0];
@@ -296,13 +400,64 @@ function lowerExpr(e, binds) {
296
400
  const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
297
401
  return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
298
402
  }
299
- case "record":
300
- return { kind: "record", spread: e.spread ? lowerExpr(e.spread, binds) : null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
403
+ case "record": {
404
+ // Discriminated union: { kind: 'NoOp' } constructor NoOp
405
+ if (e.ty.kind === "user" && !e.spread) {
406
+ const tyName = e.ty.name;
407
+ // Match base type name (strip generic args: "Result<Model, Err>" → "Result")
408
+ const baseName = tyName.includes("<") ? tyName.slice(0, tyName.indexOf("<")) : tyName;
409
+ const decl = _typeDecls.find(d => d.name === baseName && (d.kind === "discriminated-union" || d.kind === "string-union"));
410
+ if (decl && decl.discriminant) {
411
+ const discField = e.fields.find(f => f.name === decl.discriminant);
412
+ if (discField && (discField.value.kind === "str" || discField.value.kind === "bool")) {
413
+ const variantName = String(discField.value.kind === "str" ? discField.value.value : discField.value.value);
414
+ const variant = decl.variants?.find(v => v.name === variantName);
415
+ if (variant) {
416
+ const nonDiscFields = e.fields.filter(f => f.name !== decl.discriminant);
417
+ if (nonDiscFields.length === 0) {
418
+ return { kind: "constructor", name: variantName, type: tyName };
419
+ }
420
+ // Constructor with args: match variant field order
421
+ const args = variant.fields.map(vf => {
422
+ const ef = nonDiscFields.find(f => f.name === vf.name);
423
+ return ef ? lowerExpr(ef.value, binds) : { kind: "var", name: "None" };
424
+ });
425
+ return { kind: "app", fn: variantName, args };
426
+ }
427
+ }
428
+ }
429
+ }
430
+ // For spread records, wrap non-optional values in Some for optional fields
431
+ if (e.spread) {
432
+ const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
433
+ const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
434
+ const structDecl = structName ? _typeDecls.find(d => d.name === structName && d.kind === "record") : undefined;
435
+ 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] };
444
+ }
445
+ }
446
+ }
447
+ return { name: f.name, value };
448
+ });
449
+ return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
450
+ }
451
+ return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
452
+ }
301
453
  case "arrayLiteral":
302
454
  if (e.ty.kind === "map" && e.elems.length === 0)
303
455
  return { kind: "emptyMap" };
304
456
  if (e.ty.kind === "set" && e.elems.length === 0)
305
457
  return { kind: "emptySet" };
458
+ // Set with initial elements: new Set([a, b]) → {a, b}
459
+ if (e.ty.kind === "set")
460
+ return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
306
461
  return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
307
462
  case "lambda":
308
463
  return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
@@ -314,7 +469,49 @@ function lowerExpr(e, binds) {
314
469
  const cond = lowerExpr(e.cond, binds);
315
470
  let thenExpr = lowerExpr(e.then, binds);
316
471
  let elseExpr = lowerExpr(e.else, binds);
317
- // Optional ternary: wrap non-undefined branch in Some, undefined branch in None
472
+ // Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
473
+ if (e.narrowedVar && e.narrowedExpr) {
474
+ const scrutinee = lowerExpr(e.narrowedExpr, binds);
475
+ const bound = matchBinder(e.narrowedVar);
476
+ if (bound !== e.narrowedVar) {
477
+ thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
478
+ }
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);
484
+ return {
485
+ kind: "match", scrutinee,
486
+ arms: [
487
+ { pattern: `.some ${bound}`, body: thenExpr },
488
+ { pattern: ".none", body: elseExpr },
489
+ ],
490
+ };
491
+ }
492
+ // Optional cond with narrowedVar → match Some/None (truthiness)
493
+ if (e.narrowedVar && e.cond.ty.kind === "optional") {
494
+ const bound = matchBinder(e.narrowedVar);
495
+ // Replace the synthetic/narrowed var with the match-bound name
496
+ if (bound !== e.narrowedVar) {
497
+ thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
498
+ }
499
+ // 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);
506
+ return {
507
+ kind: "match", scrutinee: cond,
508
+ arms: [
509
+ { pattern: `.some ${bound}`, body: thenExpr },
510
+ { pattern: ".none", body: elseExpr },
511
+ ],
512
+ };
513
+ }
514
+ // Non-optional: regular if with optional wrapping
318
515
  if (e.ty.kind === "optional") {
319
516
  if (e.then.kind === "var" && e.then.name === "undefined") {
320
517
  thenExpr = { kind: "constructor", name: ".none" };
@@ -374,7 +571,7 @@ function ensuresToMatch(e, typeDecls) {
374
571
  if (!variant)
375
572
  return null;
376
573
  const fields = variant.fields;
377
- const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${variantName}`;
574
+ const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name, obj.name)).join(" ")}` : `.${variantName}`;
378
575
  let rhs = transformExpr(e.right);
379
576
  rhs = replaceFieldAccess(rhs, obj.name, fields);
380
577
  return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
@@ -384,7 +581,7 @@ function replaceFieldAccess(e, varName, fields) {
384
581
  if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
385
582
  const f = fields.find(f => f.name === x.field);
386
583
  if (f)
387
- return { kind: "var", name: matchBinder(f.name) };
584
+ return { kind: "var", name: matchBinder(f.name, varName) };
388
585
  }
389
586
  // If this let shadows the matched variable, stop replacing in the body
390
587
  if (x.kind === "let" && x.name === varName)
@@ -610,6 +807,17 @@ function transformStmt(s, typeDecls) {
610
807
  return [...binds, { kind: "assign", target: "_", value: expr }];
611
808
  }
612
809
  case "if": {
810
+ // Restructure && with optional check: extract the leftmost optional check
811
+ // from a && chain and nest the rest inside. Handles left-associative chains:
812
+ // if ((x !== undefined && b) && c) → if (x !== undefined) { if (b && c) { ... } }
813
+ if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
814
+ const extracted = extractLeftmostOptional(s.cond);
815
+ if (extracted) {
816
+ const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
817
+ const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
818
+ return transformStmts([outerIf], typeDecls);
819
+ }
820
+ }
613
821
  // Lift from condition only (Lean rule: don't lift from branches)
614
822
  const { binds, expr: cond } = liftMethodCalls(s.cond);
615
823
  return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
@@ -708,6 +916,21 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
708
916
  function mapStmtExprs(s, r) {
709
917
  return mapStmt(s, e => r(e));
710
918
  }
919
+ /** Extract the leftmost optional check from a && chain, returning the check and the rest.
920
+ * (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
921
+ function extractLeftmostOptional(cond) {
922
+ if (cond.kind !== "binop" || cond.op !== "&&")
923
+ return null;
924
+ const check = parseOptionalCheck(cond.left);
925
+ if (check && !check.negated)
926
+ return { optCond: cond.left, rest: cond.right };
927
+ if (cond.left.kind === "binop" && cond.left.op === "&&") {
928
+ const inner = extractLeftmostOptional(cond.left);
929
+ if (inner)
930
+ return { optCond: inner.optCond, rest: { ...cond, left: inner.rest } };
931
+ }
932
+ return null;
933
+ }
711
934
  /** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
712
935
  function parseOptionalCheck(cond) {
713
936
  if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
@@ -726,9 +949,10 @@ function emitMatchStmt(chain, typeDecls) {
726
949
  const arms = chain.cases.map(c => {
727
950
  const variant = decl?.variants?.find(v => v.name === c.variant);
728
951
  const fields = variant?.fields ?? [];
729
- const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
730
- let body = transformStmts(c.body, typeDecls);
731
- body = replaceFieldAccessInStmts(body, chain.varName, 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);
732
956
  return { pattern, body };
733
957
  });
734
958
  if (chain.fallthrough.length > 0)
@@ -742,15 +966,33 @@ function emitSwitchStmt(s, typeDecls) {
742
966
  const arms = s.cases.map(c => {
743
967
  const variant = decl?.variants?.find(v => v.name === c.label);
744
968
  const fields = variant?.fields ?? [];
745
- const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
746
- let body = transformStmts(c.body, typeDecls);
747
- body = replaceFieldAccessInStmts(body, varName, 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);
748
973
  return { pattern, body };
749
974
  });
750
975
  if (s.defaultBody.length > 0)
751
976
  arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
752
977
  return { kind: "match", scrutinee: varName, arms };
753
978
  }
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)
984
+ return stmts;
985
+ 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 };
991
+ }
992
+ }
993
+ return null;
994
+ }));
995
+ }
754
996
  function replaceFieldAccessInStmts(stmts, varName, fields) {
755
997
  if (fields.length === 0)
756
998
  return stmts;
@@ -758,7 +1000,7 @@ function replaceFieldAccessInStmts(stmts, varName, fields) {
758
1000
  if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
759
1001
  const fi = fields.find(fi => fi.name === e.field);
760
1002
  if (fi)
761
- return { kind: "var", name: matchBinder(fi.name) };
1003
+ return { kind: "var", name: matchBinder(fi.name, varName) };
762
1004
  }
763
1005
  return null;
764
1006
  };
@@ -836,11 +1078,12 @@ function transformPureSwitch(s, typeDecls) {
836
1078
  const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
837
1079
  if (!decl)
838
1080
  return null;
1081
+ const varName = s.expr.kind === "var" ? s.expr.name : undefined;
839
1082
  const arms = [];
840
1083
  for (const c of s.cases) {
841
1084
  const variant = decl.variants?.find(v => v.name === c.label);
842
1085
  const fields = variant?.fields ?? [];
843
- const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
1086
+ const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name, varName)).join(" ")}` : `.${c.label}`;
844
1087
  let body = transformPureBody(c.body, typeDecls);
845
1088
  if (!body)
846
1089
  return null;
@@ -864,7 +1107,7 @@ function transformPureMatch(chain, typeDecls) {
864
1107
  for (const c of chain.cases) {
865
1108
  const variant = decl?.variants?.find(v => v.name === c.variant);
866
1109
  const fields = variant?.fields ?? [];
867
- const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
1110
+ const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name, chain.varName)).join(" ")}` : `.${c.variant}`;
868
1111
  let body = transformPureBody(c.body, typeDecls);
869
1112
  if (!body)
870
1113
  return null;
@@ -896,6 +1139,7 @@ function transformTypeDecl(d) {
896
1139
  else if (d.kind === "discriminated-union") {
897
1140
  return {
898
1141
  kind: "inductive", name: d.name,
1142
+ typeParams: d.typeParams,
899
1143
  constructors: d.variants.map(v => ({
900
1144
  name: v.name,
901
1145
  fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
@@ -903,6 +1147,12 @@ function transformTypeDecl(d) {
903
1147
  deriving: ["Repr", "Inhabited"],
904
1148
  };
905
1149
  }
1150
+ else if (d.kind === "alias") {
1151
+ return {
1152
+ kind: "type-alias", name: d.name,
1153
+ target: parseTsType(d.aliasOf),
1154
+ };
1155
+ }
906
1156
  else {
907
1157
  return {
908
1158
  kind: "structure", name: d.name,
@@ -961,6 +1211,17 @@ function replaceVar(e, name, replacement) {
961
1211
  });
962
1212
  }
963
1213
  // ── Top-level transform ──────────────────────────────────────
1214
+ /** Transform for Lean backend — same logic, Lean options. */
1215
+ export function transformModuleLean(mod, specImport) {
1216
+ const prev = _opts;
1217
+ _opts = LEAN_OPTIONS;
1218
+ try {
1219
+ return transformModule(mod, specImport);
1220
+ }
1221
+ finally {
1222
+ _opts = prev;
1223
+ }
1224
+ }
964
1225
  /** Transform for Dafny backend — same logic, Dafny options. */
965
1226
  export function transformModuleDafny(mod) {
966
1227
  const prev = _opts;
@@ -974,6 +1235,7 @@ export function transformModuleDafny(mod) {
974
1235
  }
975
1236
  export function transformModule(mod, specImport) {
976
1237
  _forofCounters.clear();
1238
+ _typeDecls = mod.typeDecls;
977
1239
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
978
1240
  // Module-level constants
979
1241
  const constDecls = (mod.constants ?? []).map(c => ({
@@ -996,6 +1258,7 @@ export function transformModule(mod, specImport) {
996
1258
  pureDefs.push({
997
1259
  kind: "def",
998
1260
  name: fn.name,
1261
+ typeParams: fn.typeParams,
999
1262
  params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1000
1263
  returnType: fn.returnTy,
1001
1264
  requires: fn.requires.map(transformExpr),
@@ -1046,6 +1309,7 @@ export function transformModule(mod, specImport) {
1046
1309
  return {
1047
1310
  kind: "method",
1048
1311
  name: fn.name,
1312
+ typeParams: fn.typeParams,
1049
1313
  params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1050
1314
  returnType: fn.returnTy,
1051
1315
  requires: fn.requires.map(transformExpr),
@@ -1062,6 +1326,7 @@ export function transformModule(mod, specImport) {
1062
1326
  return {
1063
1327
  kind: "method",
1064
1328
  name: fn.name,
1329
+ typeParams: fn.typeParams,
1065
1330
  params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1066
1331
  returnType: fn.returnTy,
1067
1332
  requires: fn.requires.map(transformExpr),
@@ -25,7 +25,14 @@ export function parseTsType(tsType) {
25
25
  const t = tsType.trim();
26
26
  // Union: T | undefined → optional<T>
27
27
  if (t.includes(" | ")) {
28
- const arms = t.split(" | ").map(a => a.trim());
28
+ let arms = t.split(" | ").map(a => a.trim());
29
+ // Normalize expanded boolean literals: true | false → boolean
30
+ const boolLits = new Set(["true", "false"]);
31
+ const hasBoth = arms.includes("true") && arms.includes("false");
32
+ if (hasBoth) {
33
+ arms = arms.filter(a => !boolLits.has(a));
34
+ arms.unshift("boolean");
35
+ }
29
36
  const nonUndef = arms.filter(a => a !== "undefined");
30
37
  if (nonUndef.length === 1 && arms.length === 2) {
31
38
  return { kind: "optional", inner: parseTsType(nonUndef[0]) };
@@ -67,5 +74,11 @@ export function parseTsType(tsType) {
67
74
  const setMatch = t.match(/^Set<(.+)>$/);
68
75
  if (setMatch)
69
76
  return { kind: "set", elem: parseTsType(setMatch[1]) };
77
+ // Tuple [T1, T2, ...] → array of the common element type
78
+ const tupleMatch = t.match(/^\[(.+)\]$/);
79
+ if (tupleMatch) {
80
+ const elems = splitTypeArgs(tupleMatch[1]);
81
+ return { kind: "array", elem: parseTsType(elems[0]) };
82
+ }
70
83
  return { kind: "user", name: t };
71
84
  }