lemmascript 0.2.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.
@@ -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) };
@@ -63,8 +66,51 @@ function mapStmt(s, f) {
63
66
  case "assert": return { ...s, expr: r(s.expr) };
64
67
  }
65
68
  }
66
- function mapStmts(stmts, f) {
67
- return stmts.map(s => mapStmt(s, f));
69
+ /** Map over all sub-expressions in a TExpr (typed IR). */
70
+ function mapTExpr(e, f) {
71
+ const hit = f(e);
72
+ if (hit)
73
+ return hit;
74
+ const r = (x) => mapTExpr(x, f);
75
+ switch (e.kind) {
76
+ case "var":
77
+ case "num":
78
+ case "str":
79
+ case "bool":
80
+ case "result":
81
+ case "havoc": return e;
82
+ case "binop": return { ...e, left: r(e.left), right: r(e.right) };
83
+ case "unop": return { ...e, expr: r(e.expr) };
84
+ case "call": return { ...e, fn: r(e.fn), args: e.args.map(r) };
85
+ case "index": return { ...e, obj: r(e.obj), idx: r(e.idx) };
86
+ case "field": return { ...e, obj: r(e.obj) };
87
+ case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
88
+ case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
89
+ case "conditional": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
90
+ case "forall": return { ...e, body: r(e.body) };
91
+ case "exists": return { ...e, body: r(e.body) };
92
+ case "lambda": return e;
93
+ }
94
+ }
95
+ /** Map over all expressions in a TStmt tree (typed IR). */
96
+ function mapTStmt(s, f) {
97
+ const r = (e) => mapTExpr(e, f);
98
+ switch (s.kind) {
99
+ case "let": return { ...s, init: r(s.init) };
100
+ case "assign": return { ...s, value: r(s.value) };
101
+ case "return": return { ...s, value: r(s.value) };
102
+ case "break":
103
+ case "continue":
104
+ case "throw": return s;
105
+ case "expr": return { ...s, expr: r(s.expr) };
106
+ case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapTStmt(t, f)), else: s.else.map(t => mapTStmt(t, f)) };
107
+ case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
108
+ 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)) };
109
+ case "forof": return { ...s, iterable: r(s.iterable), invariants: s.invariants.map(r), body: s.body.map(t => mapTStmt(t, f)) };
110
+ case "ghostLet": return { ...s, init: r(s.init) };
111
+ case "ghostAssign": return { ...s, value: r(s.value) };
112
+ case "assert": return { ...s, expr: r(s.expr) };
113
+ }
68
114
  }
69
115
  export const LEAN_OPTIONS = {
70
116
  backend: "lean",
@@ -75,10 +121,20 @@ export const DAFNY_OPTIONS = {
75
121
  monadic: false,
76
122
  };
77
123
  /** 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}`;
124
+ let _opts = DAFNY_OPTIONS;
125
+ /** Type declarations set once per module transform for discriminated union handling. */
126
+ let _typeDecls = [];
127
+ /** Prefix match-bound field names to avoid capturing user variables.
128
+ * When prefix is given (the scrutinee name), include it to avoid
129
+ * collisions in nested matches on different variables. */
130
+ function matchBinder(fieldName, prefix) {
131
+ return prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`;
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(" ")}`;
82
138
  }
83
139
  const _forofCounters = new Map();
84
140
  function isNat(ty) { return ty.kind === "nat"; }
@@ -124,6 +180,13 @@ function transformExpr(e) { return lowerExpr(e, null); }
124
180
  * a method call can appear inline in TS. It does NOT propagate into
125
181
  * field, index, record, forall, or exists sub-expressions.
126
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
+ }
127
190
  function lowerExpr(e, binds) {
128
191
  // Monadic lifting: extract embedded method calls to let-binds
129
192
  // Pass binds through to args so nested method calls are also lifted
@@ -215,6 +278,11 @@ function lowerExpr(e, binds) {
215
278
  ],
216
279
  };
217
280
  }
281
+ // || undefined on optional → identity (no-op: x || undefined = x)
282
+ if (e.op === "||" && e.left.ty.kind === "optional" &&
283
+ e.right.kind === "var" && e.right.name === "undefined") {
284
+ return lowerExpr(e.left, binds);
285
+ }
218
286
  // || on optional → match Some/None with default
219
287
  if (e.op === "||" && e.left.ty.kind === "optional") {
220
288
  const optExpr = lowerExpr(e.left, binds);
@@ -228,6 +296,43 @@ function lowerExpr(e, binds) {
228
296
  ],
229
297
  };
230
298
  }
299
+ // || on map index → if key in map then map[key] else default
300
+ if (e.op === "||" && e.left.kind === "index" && e.left.obj.ty.kind === "map") {
301
+ const map = lowerExpr(e.left.obj, binds);
302
+ const key = lowerExpr(e.left.idx, binds);
303
+ const right = lowerExpr(e.right, binds);
304
+ return {
305
+ kind: "if",
306
+ cond: { kind: "binop", op: "in", left: key, right: map },
307
+ then: { kind: "index", arr: map, idx: key }, else: right,
308
+ };
309
+ }
310
+ // || on non-optional string/array/user → if non-empty then x else default
311
+ if (e.op === "||" && (e.left.ty.kind === "string" || e.left.ty.kind === "array" ||
312
+ (e.left.ty.kind === "user" && e.right.ty.kind === "string"))) {
313
+ const left = lowerExpr(e.left, binds);
314
+ const right = lowerExpr(e.right, binds);
315
+ return {
316
+ kind: "if",
317
+ cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: "size" }, right: { kind: "num", value: 0 } },
318
+ then: left, else: right,
319
+ };
320
+ }
321
+ // int + string → NatToString(int) + string (string concatenation)
322
+ if (e.op === "+" && _opts.backend === "dafny") {
323
+ const isIntL = e.left.ty.kind === "int" || e.left.ty.kind === "nat";
324
+ const isIntR = e.right.ty.kind === "int" || e.right.ty.kind === "nat";
325
+ if (isIntL && e.right.ty.kind === "string") {
326
+ return { kind: "binop", op: "+",
327
+ left: { kind: "app", fn: "NatToString", args: [lowerExpr(e.left, binds)] },
328
+ right: lowerExpr(e.right, binds) };
329
+ }
330
+ if (e.left.ty.kind === "string" && isIntR) {
331
+ return { kind: "binop", op: "+",
332
+ left: lowerExpr(e.left, binds),
333
+ right: { kind: "app", fn: "NatToString", args: [lowerExpr(e.right, binds)] } };
334
+ }
335
+ }
231
336
  return {
232
337
  kind: "binop",
233
338
  op: OP_MAP[e.op] ?? e.op,
@@ -249,6 +354,15 @@ function lowerExpr(e, binds) {
249
354
  return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
250
355
  }
251
356
  case "call": {
357
+ // Math.abs/min/max → preamble functions
358
+ if (e.fn.kind === "field" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math") {
359
+ if (e.fn.field === "abs" && e.args.length === 1)
360
+ return { kind: "app", fn: "MathAbs", args: [lowerExpr(e.args[0], binds)] };
361
+ if (e.fn.field === "min" && e.args.length === 2)
362
+ return { kind: "app", fn: "MathMin", args: e.args.map(a => lowerExpr(a, binds)) };
363
+ if (e.fn.field === "max" && e.args.length === 2)
364
+ return { kind: "app", fn: "MathMax", args: e.args.map(a => lowerExpr(a, binds)) };
365
+ }
252
366
  // Math.ceil(x): CeilReal on real args, identity on int
253
367
  if (e.fn.kind === "field" && e.fn.field === "ceil" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
254
368
  const arg = e.args[0];
@@ -296,13 +410,82 @@ function lowerExpr(e, binds) {
296
410
  const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
297
411
  return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
298
412
  }
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) })) };
413
+ case "record": {
414
+ // Discriminated union: { kind: 'NoOp' } constructor NoOp
415
+ if (e.ty.kind === "user" && !e.spread) {
416
+ const tyName = e.ty.name;
417
+ // Match base type name (strip generic args: "Result<Model, Err>" → "Result")
418
+ const baseName = tyName.includes("<") ? tyName.slice(0, tyName.indexOf("<")) : tyName;
419
+ const decl = _typeDecls.find(d => d.name === baseName && (d.kind === "discriminated-union" || d.kind === "string-union"));
420
+ if (decl && decl.discriminant) {
421
+ const discField = e.fields.find(f => f.name === decl.discriminant);
422
+ if (discField && (discField.value.kind === "str" || discField.value.kind === "bool")) {
423
+ const variantName = String(discField.value.kind === "str" ? discField.value.value : discField.value.value);
424
+ const variant = decl.variants?.find(v => v.name === variantName);
425
+ if (variant) {
426
+ const nonDiscFields = e.fields.filter(f => f.name !== decl.discriminant);
427
+ if (nonDiscFields.length === 0) {
428
+ return { kind: "constructor", name: variantName, type: tyName };
429
+ }
430
+ // Constructor with args: match variant field order
431
+ const args = variant.fields.map(vf => {
432
+ const ef = nonDiscFields.find(f => f.name === vf.name);
433
+ return ef ? lowerExpr(ef.value, binds) : { kind: "var", name: "None" };
434
+ });
435
+ return { kind: "app", fn: variantName, args };
436
+ }
437
+ }
438
+ }
439
+ }
440
+ // For spread records, propagate declared field types and wrap optionals
441
+ if (e.spread) {
442
+ const spreadTy = e.spread.ty.kind === "optional" ? e.spread.ty.inner : e.spread.ty;
443
+ const structName = spreadTy.kind === "user" ? spreadTy.name : undefined;
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;
447
+ const loweredFields = e.fields.map(f => {
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;
461
+ }
462
+ }
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
+ }
475
+ return { name: f.name, value };
476
+ });
477
+ return { kind: "record", spread: lowerExpr(e.spread, binds), fields: loweredFields };
478
+ }
479
+ return { kind: "record", spread: null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
480
+ }
301
481
  case "arrayLiteral":
302
482
  if (e.ty.kind === "map" && e.elems.length === 0)
303
483
  return { kind: "emptyMap" };
304
484
  if (e.ty.kind === "set" && e.elems.length === 0)
305
485
  return { kind: "emptySet" };
486
+ // Set with initial elements: new Set([a, b]) → {a, b}
487
+ if (e.ty.kind === "set")
488
+ return { kind: "app", fn: "SetLiteral", args: e.elems.map(el => lowerExpr(el, binds)) };
306
489
  return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
307
490
  case "lambda":
308
491
  return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
@@ -311,23 +494,53 @@ function lowerExpr(e, binds) {
311
494
  case "exists":
312
495
  return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
313
496
  case "conditional": {
314
- 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);
315
500
  let thenExpr = lowerExpr(e.then, binds);
316
501
  let elseExpr = lowerExpr(e.else, binds);
317
- // Optional ternary: wrap non-undefined branch in Some, undefined branch in None
318
- if (e.ty.kind === "optional") {
319
- if (e.then.kind === "var" && e.then.name === "undefined") {
320
- thenExpr = { kind: "constructor", name: ".none" };
321
- }
322
- else {
323
- thenExpr = { kind: "app", fn: "Some", args: [thenExpr] };
502
+ // Explicit !== undefined with narrowedExpr → match Some/None on the optional expression
503
+ if (e.narrowedVar && e.narrowedExpr) {
504
+ const scrutinee = lowerExpr(e.narrowedExpr, binds);
505
+ const bound = matchBinder(e.narrowedVar);
506
+ if (bound !== e.narrowedVar) {
507
+ thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
324
508
  }
325
- if (e.else.kind === "var" && e.else.name === "undefined") {
326
- elseExpr = { kind: "constructor", name: ".none" };
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);
327
513
  }
328
- else {
329
- elseExpr = { kind: "app", fn: "Some", args: [elseExpr] };
514
+ return {
515
+ kind: "match", scrutinee,
516
+ arms: [
517
+ { pattern: `.some ${bound}`, body: thenExpr },
518
+ { pattern: ".none", body: elseExpr },
519
+ ],
520
+ };
521
+ }
522
+ // Optional cond with narrowedVar → match Some/None (truthiness)
523
+ if (e.narrowedVar && e.cond.ty.kind === "optional") {
524
+ const bound = matchBinder(e.narrowedVar);
525
+ // Replace the synthetic/narrowed var with the match-bound name
526
+ if (bound !== e.narrowedVar) {
527
+ thenExpr = replaceVar(thenExpr, e.narrowedVar, { kind: "var", name: bound });
330
528
  }
529
+ // The match produces an Optional: wrap branches in Some/None.
530
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
531
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
532
+ return {
533
+ kind: "match", scrutinee: cond,
534
+ arms: [
535
+ { pattern: `.some ${bound}`, body: thenExpr },
536
+ { pattern: ".none", body: elseExpr },
537
+ ],
538
+ };
539
+ }
540
+ // Non-optional: regular if with optional wrapping
541
+ if (e.ty.kind === "optional") {
542
+ thenExpr = wrapOptionalBranch(thenExpr, e.then);
543
+ elseExpr = wrapOptionalBranch(elseExpr, e.else);
331
544
  }
332
545
  return { kind: "if", cond, then: thenExpr, else: elseExpr };
333
546
  }
@@ -374,7 +587,7 @@ function ensuresToMatch(e, typeDecls) {
374
587
  if (!variant)
375
588
  return null;
376
589
  const fields = variant.fields;
377
- const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${variantName}`;
590
+ const pattern = buildMatchPattern(variantName, fields, obj.name);
378
591
  let rhs = transformExpr(e.right);
379
592
  rhs = replaceFieldAccess(rhs, obj.name, fields);
380
593
  return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
@@ -384,7 +597,7 @@ function replaceFieldAccess(e, varName, fields) {
384
597
  if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
385
598
  const f = fields.find(f => f.name === x.field);
386
599
  if (f)
387
- return { kind: "var", name: matchBinder(f.name) };
600
+ return { kind: "var", name: matchBinder(f.name, varName) };
388
601
  }
389
602
  // If this let shadows the matched variable, stop replacing in the body
390
603
  if (x.kind === "let" && x.name === varName)
@@ -407,13 +620,12 @@ function transformStmts(stmts, typeDecls) {
407
620
  continue;
408
621
  }
409
622
  // Detect optional check → match on Some/None
410
- const opt = parseOptionalCheck(s.cond);
411
- if (opt) {
412
- const rest = stmts.slice(i + 1);
413
- 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));
414
626
  // If rest was consumed into the Some branch, skip remaining
415
- const someBranch = opt.negated ? s.else : s.then;
416
- 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) {
417
629
  return result;
418
630
  }
419
631
  i++;
@@ -610,6 +822,17 @@ function transformStmt(s, typeDecls) {
610
822
  return [...binds, { kind: "assign", target: "_", value: expr }];
611
823
  }
612
824
  case "if": {
825
+ // Restructure && with optional check: extract the leftmost optional check
826
+ // from a && chain and nest the rest inside. Handles left-associative chains:
827
+ // if ((x !== undefined && b) && c) → if (x !== undefined) { if (b && c) { ... } }
828
+ if (s.cond.kind === "binop" && s.cond.op === "&&" && s.else.length === 0) {
829
+ const extracted = extractLeftmostOptional(s.cond);
830
+ if (extracted) {
831
+ const innerIf = { kind: "if", cond: extracted.rest, then: s.then, else: [] };
832
+ const outerIf = { kind: "if", cond: extracted.optCond, then: [innerIf], else: [] };
833
+ return transformStmts([outerIf], typeDecls);
834
+ }
835
+ }
613
836
  // Lift from condition only (Lean rule: don't lift from branches)
614
837
  const { binds, expr: cond } = liftMethodCalls(s.cond);
615
838
  return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
@@ -686,7 +909,7 @@ function parseDiscriminantCond(cond) {
686
909
  return null;
687
910
  return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
688
911
  }
689
- function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
912
+ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts, fieldExpr) {
690
913
  let someBranch = negated ? s.else : s.then;
691
914
  const noneBranch = negated ? s.then : s.else;
692
915
  // Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
@@ -695,20 +918,87 @@ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
695
918
  someBranch = restStmts;
696
919
  }
697
920
  const bound = matchBinder(`${varName}_val`);
698
- const someBody = transformStmts(someBranch, typeDecls);
699
- const r = (e) => replaceVar(e, varName, { kind: "var", name: bound });
700
- const someReplaced = someBody.map(stmt => mapStmtExprs(stmt, r));
701
- const arms = [
702
- { pattern: `.some ${bound}`, body: someReplaced },
703
- { pattern: ".none", body: noneBranch.length > 0 ? transformStmts(noneBranch, typeDecls) : [] },
704
- ];
705
- 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
+ };
706
943
  }
707
944
  /** Apply an expression transform to all expressions in a statement (convenience wrapper). */
708
945
  function mapStmtExprs(s, r) {
709
946
  return mapStmt(s, e => r(e));
710
947
  }
711
- /** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
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
+ }
984
+ /** Extract the leftmost optional check from a && chain, returning the check and the rest.
985
+ * (x !== undefined && b) && c → { optCond: x !== undefined, rest: b && c } */
986
+ function extractLeftmostOptional(cond) {
987
+ if (cond.kind !== "binop" || cond.op !== "&&")
988
+ return null;
989
+ const check = parseOptionalCheck(cond.left);
990
+ if (check && !check.negated)
991
+ return { optCond: cond.left, rest: cond.right };
992
+ if (cond.left.kind === "binop" && cond.left.op === "&&") {
993
+ const inner = extractLeftmostOptional(cond.left);
994
+ if (inner)
995
+ return { optCond: inner.optCond, rest: { ...cond, left: inner.rest } };
996
+ }
997
+ return null;
998
+ }
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. */
712
1002
  function parseOptionalCheck(cond) {
713
1003
  if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
714
1004
  return null;
@@ -717,20 +1007,50 @@ function parseOptionalCheck(cond) {
717
1007
  varExpr = cond.left;
718
1008
  if (cond.left.kind === "var" && cond.left.name === "undefined")
719
1009
  varExpr = cond.right;
720
- if (!varExpr || varExpr.kind !== "var" || varExpr.ty.kind !== "optional")
1010
+ if (!varExpr)
721
1011
  return null;
722
- 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;
723
1022
  }
724
- function emitMatchStmt(chain, typeDecls) {
725
- const decl = typeDecls.find(d => d.name === chain.typeName);
726
- const arms = chain.cases.map(c => {
727
- 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);
728
1042
  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);
732
- return { pattern, body };
733
- });
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));
734
1054
  if (chain.fallthrough.length > 0)
735
1055
  arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
736
1056
  return { kind: "match", scrutinee: chain.varName, arms };
@@ -738,41 +1058,39 @@ function emitMatchStmt(chain, typeDecls) {
738
1058
  function emitSwitchStmt(s, typeDecls) {
739
1059
  const varName = s.expr.kind === "var" ? s.expr.name : "?";
740
1060
  const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
741
- const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
742
- const arms = s.cases.map(c => {
743
- const variant = decl?.variants?.find(v => v.name === c.label);
744
- 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);
748
- return { pattern, body };
749
- });
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));
750
1063
  if (s.defaultBody.length > 0)
751
1064
  arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
752
1065
  return { kind: "match", scrutinee: varName, arms };
753
1066
  }
754
- function replaceFieldAccessInStmts(stmts, varName, fields) {
755
- 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)
756
1074
  return stmts;
757
- const f = (e) => {
758
- if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
759
- const fi = fields.find(fi => fi.name === e.field);
760
- if (fi)
761
- return { kind: "var", name: matchBinder(fi.name) };
1075
+ return stmts.map(s => mapTStmt(s, e => {
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 };
1081
+ }
762
1082
  }
763
1083
  return null;
764
- };
765
- const result = [];
766
- for (const s of stmts) {
767
- // If a let shadows the matched variable, stop replacing from here on
768
- if (s.kind === "let" && s.name === varName) {
769
- result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
770
- result.push(...stmts.slice(result.length));
771
- break;
772
- }
773
- result.push(mapStmt(s, f));
774
- }
775
- return result;
1084
+ }));
1085
+ }
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
+ })));
776
1094
  }
777
1095
  // ── Pure function generation ─────────────────────────────────
778
1096
  function transformPureBody(stmts, typeDecls) {
@@ -795,24 +1113,19 @@ function transformPureBody(stmts, typeDecls) {
795
1113
  }
796
1114
  case "if": {
797
1115
  // Optional narrowing: if (x === undefined) → match x { None => ..., Some(x_val) => ... }
798
- const optCheck = parseOptionalCheck(s.cond);
799
- if (optCheck) {
800
- let someBranch = optCheck.negated ? s.else : s.then;
801
- const noneBranch = optCheck.negated ? s.then : (s.else.length > 0 ? s.else : rest);
802
- if (someBranch.length === 0)
803
- someBranch = rest;
804
- const bound = matchBinder(`${optCheck.varName}_val`);
805
- const someExpr = transformPureBody(someBranch, typeDecls);
1116
+ const optMatch = prepareOptionalMatch(s, rest);
1117
+ if (optMatch) {
1118
+ const someExpr = transformPureBody(optMatch.someBranch, typeDecls);
806
1119
  if (!someExpr)
807
1120
  return null;
808
- const noneExpr = transformPureBody(noneBranch, typeDecls);
1121
+ const noneExpr = transformPureBody(optMatch.noneBranch, typeDecls);
809
1122
  if (!noneExpr)
810
1123
  return null;
811
- const someReplaced = replaceVar(someExpr, optCheck.varName, { kind: "var", name: bound });
1124
+ const someReplaced = replaceVar(someExpr, optMatch.check.varName, { kind: "var", name: optMatch.bound });
812
1125
  return {
813
- kind: "match", scrutinee: optCheck.varName,
1126
+ kind: "match", scrutinee: optMatch.check.varName,
814
1127
  arms: [
815
- { pattern: `.some ${bound}`, body: someReplaced },
1128
+ { pattern: `.some ${optMatch.bound}`, body: someReplaced },
816
1129
  { pattern: ".none", body: noneExpr },
817
1130
  ],
818
1131
  };
@@ -833,21 +1146,21 @@ function transformPureBody(stmts, typeDecls) {
833
1146
  return null;
834
1147
  }
835
1148
  function transformPureSwitch(s, typeDecls) {
836
- const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
837
- if (!decl)
1149
+ const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : "";
1150
+ if (!typeDecls.find(d => d.name === typeName))
838
1151
  return null;
839
- const arms = [];
840
- for (const c of s.cases) {
841
- const variant = decl.variants?.find(v => v.name === c.label);
842
- const fields = variant?.fields ?? [];
843
- const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
844
- let body = transformPureBody(c.body, typeDecls);
845
- if (!body)
1152
+ const varName = s.expr.kind === "var" ? s.expr.name : undefined;
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)
846
1157
  return null;
847
- if (fields.length > 0 && s.expr.kind === "var")
848
- body = replaceFieldAccess(body, s.expr.name, fields);
849
- arms.push({ pattern, body });
850
- }
1158
+ if (fields.length > 0 && vn)
1159
+ result = replaceFieldAccess(result, vn, fields);
1160
+ return result;
1161
+ });
1162
+ if (!arms)
1163
+ return null;
851
1164
  if (s.defaultBody.length > 0) {
852
1165
  const body = transformPureBody(s.defaultBody, typeDecls);
853
1166
  if (!body)
@@ -859,22 +1172,21 @@ function transformPureSwitch(s, typeDecls) {
859
1172
  return { kind: "match", scrutinee: s.expr.name, arms };
860
1173
  }
861
1174
  function transformPureMatch(chain, typeDecls) {
862
- const decl = typeDecls.find(d => d.name === chain.typeName);
863
- const arms = [];
864
- for (const c of chain.cases) {
865
- const variant = decl?.variants?.find(v => v.name === c.variant);
866
- const fields = variant?.fields ?? [];
867
- const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
868
- let body = transformPureBody(c.body, typeDecls);
869
- 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)
870
1179
  return null;
871
- if (fields.length > 0)
872
- body = replaceFieldAccess(body, chain.varName, fields);
873
- arms.push({ pattern, body });
874
- }
1180
+ if (fields.length > 0 && vn)
1181
+ result = replaceFieldAccess(result, vn, fields);
1182
+ return result;
1183
+ });
1184
+ if (!arms)
1185
+ return null;
875
1186
  // Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
876
1187
  // discriminated unions. Skip the catch-all arm when all variants are matched,
877
1188
  // since Lean errors on redundant match arms.
1189
+ const decl = typeDecls.find(d => d.name === chain.typeName);
878
1190
  const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
879
1191
  if (chain.fallthrough.length > 0 && !allCovered) {
880
1192
  const body = transformPureBody(chain.fallthrough, typeDecls);
@@ -896,17 +1208,24 @@ function transformTypeDecl(d) {
896
1208
  else if (d.kind === "discriminated-union") {
897
1209
  return {
898
1210
  kind: "inductive", name: d.name,
1211
+ typeParams: d.typeParams,
899
1212
  constructors: d.variants.map(v => ({
900
1213
  name: v.name,
901
- fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
1214
+ fields: v.fields.map(f => ({ name: f.name, type: f.type })),
902
1215
  })),
903
1216
  deriving: ["Repr", "Inhabited"],
904
1217
  };
905
1218
  }
1219
+ else if (d.kind === "alias") {
1220
+ return {
1221
+ kind: "type-alias", name: d.name,
1222
+ target: d.aliasOfTy,
1223
+ };
1224
+ }
906
1225
  else {
907
1226
  return {
908
1227
  kind: "structure", name: d.name,
909
- fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
1228
+ fields: d.fields.map(f => ({ name: f.name, type: f.type })),
910
1229
  deriving: ["Repr", "Inhabited", "DecidableEq"],
911
1230
  };
912
1231
  }
@@ -961,6 +1280,17 @@ function replaceVar(e, name, replacement) {
961
1280
  });
962
1281
  }
963
1282
  // ── Top-level transform ──────────────────────────────────────
1283
+ /** Transform for Lean backend — same logic, Lean options. */
1284
+ export function transformModuleLean(mod, specImport) {
1285
+ const prev = _opts;
1286
+ _opts = LEAN_OPTIONS;
1287
+ try {
1288
+ return transformModule(mod, specImport);
1289
+ }
1290
+ finally {
1291
+ _opts = prev;
1292
+ }
1293
+ }
964
1294
  /** Transform for Dafny backend — same logic, Dafny options. */
965
1295
  export function transformModuleDafny(mod) {
966
1296
  const prev = _opts;
@@ -974,6 +1304,8 @@ export function transformModuleDafny(mod) {
974
1304
  }
975
1305
  export function transformModule(mod, specImport) {
976
1306
  _forofCounters.clear();
1307
+ _liftCounter = 0;
1308
+ _typeDecls = mod.typeDecls;
977
1309
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
978
1310
  // Module-level constants
979
1311
  const constDecls = (mod.constants ?? []).map(c => ({
@@ -996,6 +1328,7 @@ export function transformModule(mod, specImport) {
996
1328
  pureDefs.push({
997
1329
  kind: "def",
998
1330
  name: fn.name,
1331
+ typeParams: fn.typeParams,
999
1332
  params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1000
1333
  returnType: fn.returnTy,
1001
1334
  requires: fn.requires.map(transformExpr),
@@ -1046,6 +1379,7 @@ export function transformModule(mod, specImport) {
1046
1379
  return {
1047
1380
  kind: "method",
1048
1381
  name: fn.name,
1382
+ typeParams: fn.typeParams,
1049
1383
  params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1050
1384
  returnType: fn.returnTy,
1051
1385
  requires: fn.requires.map(transformExpr),
@@ -1062,6 +1396,7 @@ export function transformModule(mod, specImport) {
1062
1396
  return {
1063
1397
  kind: "method",
1064
1398
  name: fn.name,
1399
+ typeParams: fn.typeParams,
1065
1400
  params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1066
1401
  returnType: fn.returnTy,
1067
1402
  requires: fn.requires.map(transformExpr),