lemmascript 0.1.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.
@@ -1,70 +1,142 @@
1
1
  /**
2
- * Transform — Typed IR → Lean IR.
2
+ * Transform — Typed IR → Backend IR.
3
3
  *
4
4
  * Consumes resolved types and classifications.
5
5
  * No type lookups, no string parsing, no re-inference.
6
6
  */
7
- import { parseTsType, tyToLean } from "./types.js";
7
+ import { parseTsType } from "./types.js";
8
+ // ── Generic IR walkers ──────────────────────────────────────
9
+ /**
10
+ * Map over all sub-expressions in an Expr. `f` is called on each node;
11
+ * if it returns non-null, that replaces the node (and recursion stops).
12
+ * If it returns null, the walker recurses into children.
13
+ */
14
+ function mapExpr(e, f) {
15
+ const hit = f(e);
16
+ if (hit)
17
+ return hit;
18
+ const r = (x) => mapExpr(x, f);
19
+ switch (e.kind) {
20
+ case "var":
21
+ case "num":
22
+ case "bool":
23
+ case "str":
24
+ case "constructor":
25
+ case "emptyMap":
26
+ case "emptySet":
27
+ case "havoc": return e;
28
+ case "binop": return { ...e, left: r(e.left), right: r(e.right) };
29
+ case "unop": return { ...e, expr: r(e.expr) };
30
+ case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) };
31
+ case "app": return { ...e, args: e.args.map(r) };
32
+ case "field": return { ...e, obj: r(e.obj) };
33
+ case "toNat": return { ...e, expr: r(e.expr) };
34
+ case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
35
+ case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(fi => ({ ...fi, value: r(fi.value) })) };
36
+ case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
37
+ case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
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
+ }
42
+ case "forall": return { ...e, body: r(e.body) };
43
+ case "exists": return { ...e, body: r(e.body) };
44
+ case "let": return { ...e, value: r(e.value), body: r(e.body) };
45
+ case "methodCall": return { ...e, obj: r(e.obj), args: e.args.map(r) };
46
+ case "lambda": return e;
47
+ }
48
+ }
49
+ /** Map over all expressions in a statement tree. */
50
+ function mapStmt(s, f) {
51
+ const r = (e) => mapExpr(e, f);
52
+ switch (s.kind) {
53
+ case "let": return { ...s, value: r(s.value) };
54
+ case "assign": return { ...s, value: r(s.value) };
55
+ case "bind": return { ...s, value: r(s.value) };
56
+ case "let-bind": return { ...s, value: r(s.value) };
57
+ case "return": return { ...s, value: r(s.value) };
58
+ case "break":
59
+ case "continue": return s;
60
+ case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
61
+ case "match": return { ...s, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
62
+ case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
63
+ case "forin": return { ...s, bound: r(s.bound), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
64
+ case "ghostLet": return { ...s, value: r(s.value) };
65
+ case "ghostAssign": return { ...s, value: r(s.value) };
66
+ case "assert": return { ...s, expr: r(s.expr) };
67
+ }
68
+ }
69
+ function mapStmts(stmts, f) {
70
+ return stmts.map(s => mapStmt(s, f));
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
+ }
8
118
  export const LEAN_OPTIONS = {
9
119
  backend: "lean",
10
120
  monadic: true,
11
- dotMethods: {
12
- array: {
13
- map: { pure: "map", monadic: "mapM" },
14
- filter: { pure: "filter", monadic: "filterM" },
15
- every: { pure: "all", monadic: "allM" },
16
- some: { pure: "any", monadic: "anyM" },
17
- includes: { pure: "contains" },
18
- find: { pure: "find?" },
19
- with: { pure: "set!" },
20
- },
21
- },
22
- methodTable: {
23
- string: {
24
- indexOf: "JSString.indexOf",
25
- slice: "JSString.slice",
26
- },
27
- array: {
28
- push: "Array.push",
29
- },
30
- },
31
121
  };
32
122
  export const DAFNY_OPTIONS = {
33
123
  backend: "dafny",
34
124
  monadic: false,
35
- dotMethods: {
36
- array: {
37
- map: { pure: "map" },
38
- filter: { pure: "filter" },
39
- every: { pure: "every" },
40
- some: { pure: "some" },
41
- includes: { pure: "includes" },
42
- with: { pure: "with" },
43
- },
44
- },
45
- methodTable: {
46
- string: {
47
- indexOf: "StringIndexOf",
48
- slice: "StringSlice",
49
- },
50
- array: {
51
- push: "SeqPush",
52
- },
53
- },
54
125
  };
55
126
  /** Active options — set before each transform call. */
56
- let _opts = LEAN_OPTIONS;
57
- /** Prefix match-bound field names to avoid capturing user variables. */
58
- function matchBinder(fieldName) {
59
- 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}`;
60
135
  }
136
+ const _forofCounters = new Map();
61
137
  function isNat(ty) { return ty.kind === "nat"; }
62
138
  function isArray(ty) { return ty.kind === "array"; }
63
139
  function isUser(ty) { return ty.kind === "user"; }
64
- // ── Method lookup (uses active options) ─────────────────────
65
- function lookupDotMethod(recvTy, method) {
66
- return _opts.dotMethods[recvTy.kind]?.[method];
67
- }
68
140
  /** Check if transformed lambda body contains monadic binds. */
69
141
  function isMonadicBody(stmts) {
70
142
  for (const s of stmts) {
@@ -84,32 +156,20 @@ function isMonadicBody(stmts) {
84
156
  }
85
157
  return false;
86
158
  }
87
- /** Lean modules that don't need explicit imports. */
88
- const BUILTIN_MODULES = new Set(["Array", "String", "List", "Nat", "Int"]);
89
- /** Map from Lean module prefix → import path. */
90
- const MODULE_IMPORTS = {
91
- "JSString": "LemmaScript.JSString",
92
- };
93
- const usedImports = new Set();
94
- function lookupMethod(recvTy, method) {
95
- const tyKey = recvTy.kind === "array" ? "array" : recvTy.kind;
96
- const lean = _opts.methodTable[tyKey]?.[method];
97
- if (lean) {
98
- const mod = lean.split(".")[0];
99
- if (!BUILTIN_MODULES.has(mod))
100
- usedImports.add(mod);
101
- }
102
- return lean;
103
- }
104
159
  // ── Transform expressions ────────────────────────────────────
160
+ /** Prop-valued operators (for specs/invariants). */
105
161
  const OP_MAP = {
106
162
  "===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
107
163
  "&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
108
164
  "==": "=", "!=": "≠",
109
165
  };
166
+ /** Bool-valued operators (for code-level conditions needing Decidable). */
167
+ const BOOL_OP_MAP = {
168
+ ...OP_MAP, "===": "==", "!==": "!=",
169
+ };
110
170
  function transformExpr(e) { return lowerExpr(e, null); }
111
171
  /**
112
- * Lower a typed expression to Lean IR.
172
+ * Lower a typed expression to Backend IR.
113
173
  *
114
174
  * When `binds` is non-null, embedded method calls are extracted into
115
175
  * `let ← ` binds (monadic lifting / selective ANF). Lifting propagates
@@ -139,6 +199,20 @@ function lowerExpr(e, binds) {
139
199
  case "unop":
140
200
  if (e.op === "-" && e.expr.kind === "num")
141
201
  return { kind: "num", value: -e.expr.value };
202
+ // String truthiness: !str → str == ""
203
+ if (e.op === "!" && e.expr.ty.kind === "string")
204
+ return { kind: "binop", op: "=", left: lowerExpr(e.expr, binds), right: { kind: "str", value: "" } };
205
+ // Optional truthiness: !opt → opt is None
206
+ if (e.op === "!" && e.expr.ty.kind === "optional") {
207
+ const bound = matchBinder("value");
208
+ return {
209
+ kind: "match", scrutinee: lowerExpr(e.expr, binds),
210
+ arms: [
211
+ { pattern: `.some ${bound}`, body: { kind: "bool", value: false } },
212
+ { pattern: ".none", body: { kind: "bool", value: true } },
213
+ ],
214
+ };
215
+ }
142
216
  return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
143
217
  case "binop": {
144
218
  // Implication: flatten (A && B) ==> C → implies [A, B] C
@@ -166,6 +240,89 @@ function lowerExpr(e, binds) {
166
240
  : { kind: "str", value: e.right.value };
167
241
  return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
168
242
  }
243
+ // Optional comparison: optExpr op val → match optExpr { Some(v) => v op val, None => false/true }
244
+ if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op) &&
245
+ (e.left.ty.kind === "optional") !== (e.right.ty.kind === "optional")) {
246
+ const [optSide, valSide] = e.left.ty.kind === "optional" ? [e.left, e.right] : [e.right, e.left];
247
+ const optExpr = lowerExpr(optSide, binds);
248
+ // x === undefined → None?, x !== undefined → Some?
249
+ if (valSide.kind === "var" && valSide.name === "undefined") {
250
+ const isNone = e.op === "===";
251
+ return {
252
+ kind: "match", scrutinee: optExpr,
253
+ arms: [
254
+ { pattern: ".some _", body: { kind: "bool", value: !isNone } },
255
+ { pattern: ".none", body: { kind: "bool", value: isNone } },
256
+ ],
257
+ };
258
+ }
259
+ const valExpr = lowerExpr(valSide, binds);
260
+ const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
261
+ const noneVal = e.op === "!==" ? true : false;
262
+ const bound = matchBinder("value");
263
+ return {
264
+ kind: "match", scrutinee: optExpr,
265
+ arms: [
266
+ { pattern: `.some ${bound}`, body: { kind: "binop", op: cmpOp, left: { kind: "var", name: bound }, right: valExpr } },
267
+ { pattern: ".none", body: { kind: "bool", value: noneVal } },
268
+ ],
269
+ };
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
+ }
276
+ // || on optional → match Some/None with default
277
+ if (e.op === "||" && e.left.ty.kind === "optional") {
278
+ const optExpr = lowerExpr(e.left, binds);
279
+ const defaultExpr = lowerExpr(e.right, binds);
280
+ const bound = matchBinder("value");
281
+ return {
282
+ kind: "match", scrutinee: optExpr,
283
+ arms: [
284
+ { pattern: `.some ${bound}`, body: { kind: "var", name: bound } },
285
+ { pattern: ".none", body: defaultExpr },
286
+ ],
287
+ };
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
+ }
169
326
  return {
170
327
  kind: "binop",
171
328
  op: OP_MAP[e.op] ?? e.op,
@@ -178,6 +335,8 @@ function lowerExpr(e, binds) {
178
335
  return { kind: "field", obj: transformExpr(e.obj), field: "size" };
179
336
  if (e.field === "length" && e.obj.ty.kind === "string")
180
337
  return { kind: "field", obj: transformExpr(e.obj), field: "length" };
338
+ if (e.field === "size" && (e.obj.ty.kind === "map" || e.obj.ty.kind === "set"))
339
+ return { kind: "field", obj: transformExpr(e.obj), field: "collectionSize" };
181
340
  return { kind: "field", obj: transformExpr(e.obj), field: e.field };
182
341
  case "index": {
183
342
  const idx = transformExpr(e.idx);
@@ -185,61 +344,198 @@ function lowerExpr(e, binds) {
185
344
  return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
186
345
  }
187
346
  case "call": {
188
- // Math.floor(a / b): Lean int div floors (erase), Dafny truncates (emit JSFloorDiv)
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
+ }
356
+ // Math.ceil(x): CeilReal on real args, identity on int
357
+ if (e.fn.kind === "field" && e.fn.field === "ceil" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
358
+ const arg = e.args[0];
359
+ if (arg.ty.kind === "real")
360
+ return { kind: "app", fn: "CeilReal", args: [lowerExpr(arg, binds)] };
361
+ return lowerExpr(arg, binds);
362
+ }
363
+ // Math.floor(x): FloorReal on real args, JSFloorDiv for int division, identity on int
189
364
  if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
190
365
  const arg = e.args[0];
366
+ if (arg.ty.kind === "real")
367
+ return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
191
368
  if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
192
369
  return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
193
370
  return lowerExpr(arg, binds);
194
371
  }
195
- // Built-in method call: receiver.method(args)
372
+ // Method call: receiver.method(args) → methodCall node
196
373
  if (e.fn.kind === "field") {
197
- // Remapped methods: leanFn receiver args
198
- const lean = lookupMethod(e.fn.obj.ty, e.fn.field);
199
- if (lean)
200
- return { kind: "app", fn: lean, args: [lowerExpr(e.fn.obj, binds), ...e.args.map(a => lowerExpr(a, binds))] };
201
- // Dot-notation methods: receiver.leanName args
202
- const dotEntry = lookupDotMethod(e.fn.obj.ty, e.fn.field);
203
- if (dotEntry) {
204
- const recv = lowerExpr(e.fn.obj, binds);
205
- const args = e.args.map((a, i) => {
206
- const lowered = lowerExpr(a, binds);
207
- // set! index (first arg) needs .toNat when Int-typed
208
- if (dotEntry.pure === "set!" && i === 0 && !isNat(a.ty))
209
- return { kind: "toNat", expr: lowered };
210
- return lowered;
211
- });
212
- // Check if any lambda arg has monadic body use monadic variant
213
- const needsMonadic = _opts.monadic && args.some(a => a.kind === "lambda" && isMonadicBody(a.body));
214
- const method = needsMonadic && dotEntry.monadic ? dotEntry.monadic : dotEntry.pure;
215
- const result = { kind: "dotCall", obj: recv, method, args };
216
- // Monadic HOF call is itself monadic — lift via binds like a method call
217
- if (_opts.monadic && needsMonadic && binds) {
218
- const name = `_t${_liftCounter++}`;
219
- binds.push({ kind: "let-bind", name, value: result });
220
- return { kind: "var", name };
221
- }
222
- return result;
374
+ const recv = lowerExpr(e.fn.obj, binds);
375
+ let method = e.fn.field;
376
+ const args = e.args.map((a, i) => {
377
+ const lowered = lowerExpr(a, binds);
378
+ // arr.with index (first arg) needs .toNat when Int-typed
379
+ if (e.fn.kind === "field" && e.fn.field === "with" && e.fn.obj.ty.kind === "array" && i === 0 && !isNat(a.ty))
380
+ return { kind: "toNat", expr: lowered };
381
+ return lowered;
382
+ });
383
+ // Spec-context map get: result type is non-optional → direct access
384
+ if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
385
+ method = "getDirect";
386
+ }
387
+ // Check if any lambda arg has monadic body
388
+ const needsMonadic = _opts.monadic && args.some(a => a.kind === "lambda" && isMonadicBody(a.body));
389
+ const result = { kind: "methodCall", obj: recv, objTy: e.fn.obj.ty, method, args, monadic: needsMonadic };
390
+ // Monadic HOF call is itself monadic lift via binds like a method call
391
+ if (_opts.monadic && needsMonadic && binds) {
392
+ const name = `_t${_liftCounter++}`;
393
+ binds.push({ kind: "let-bind", name, value: result });
394
+ return { kind: "var", name };
223
395
  }
224
- throw new Error(`Unsupported method call: .${e.fn.field}() on ${e.fn.obj.ty.kind}`);
396
+ return result;
225
397
  }
226
398
  if (e.fn.kind !== "var")
227
399
  throw new Error(`Unsupported call expression: ${e.fn.kind}`);
228
400
  const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
229
401
  return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
230
402
  }
231
- case "record":
232
- 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
+ }
233
453
  case "arrayLiteral":
454
+ if (e.ty.kind === "map" && e.elems.length === 0)
455
+ return { kind: "emptyMap" };
456
+ if (e.ty.kind === "set" && e.elems.length === 0)
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)) };
234
461
  return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
235
462
  case "lambda":
236
- return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })), body: transformStmts(e.body, []) };
463
+ return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
237
464
  case "forall":
238
- return { kind: "forall", var: e.var, type: tyToLean(e.varTy), body: transformExpr(e.body) };
465
+ return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
239
466
  case "exists":
240
- return { kind: "exists", var: e.var, type: tyToLean(e.varTy), body: transformExpr(e.body) };
241
- case "conditional":
242
- return { kind: "if", cond: lowerExpr(e.cond, binds), then: lowerExpr(e.then, binds), else: lowerExpr(e.else, binds) };
467
+ return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
468
+ case "conditional": {
469
+ const cond = lowerExpr(e.cond, binds);
470
+ let thenExpr = lowerExpr(e.then, binds);
471
+ let elseExpr = lowerExpr(e.else, binds);
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
515
+ 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
+ }
528
+ }
529
+ return { kind: "if", cond, then: thenExpr, else: elseExpr };
530
+ }
531
+ case "havoc":
532
+ // Dafny's * only works in var/assign positions — lift to own declaration
533
+ if (binds) {
534
+ const name = `_t${_liftCounter++}`;
535
+ binds.push({ kind: "let", name, type: e.ty, mutable: false, value: { kind: "havoc", type: e.ty } });
536
+ return { kind: "var", name };
537
+ }
538
+ return { kind: "havoc", type: e.ty };
243
539
  }
244
540
  }
245
541
  function flattenImpl(e) {
@@ -275,37 +571,23 @@ function ensuresToMatch(e, typeDecls) {
275
571
  if (!variant)
276
572
  return null;
277
573
  const fields = variant.fields;
278
- 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}`;
279
575
  let rhs = transformExpr(e.right);
280
576
  rhs = replaceFieldAccess(rhs, obj.name, fields);
281
577
  return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
282
578
  }
283
579
  function replaceFieldAccess(e, varName, fields) {
284
- if (e.kind === "field" && e.obj.kind === "var" && e.obj.name === varName) {
285
- const f = fields.find(f => f.name === e.field);
286
- if (f)
287
- return { kind: "var", name: matchBinder(f.name) };
288
- }
289
- const r = (x) => replaceFieldAccess(x, varName, fields);
290
- switch (e.kind) {
291
- case "binop": return { ...e, left: r(e.left), right: r(e.right) };
292
- case "unop": return { ...e, expr: r(e.expr) };
293
- case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) };
294
- case "forall": return { ...e, body: r(e.body) };
295
- case "exists": return { ...e, body: r(e.body) };
296
- case "app": return { ...e, args: e.args.map(r) };
297
- case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(f => ({ ...f, value: r(f.value) })) };
298
- case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
299
- case "let":
300
- // If this let shadows the matched variable, stop replacing in the body
301
- if (e.name === varName)
302
- return { ...e, value: r(e.value) };
303
- return { ...e, value: r(e.value), body: r(e.body) };
304
- case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
305
- case "field": return { ...e, obj: r(e.obj) };
306
- case "match": return { ...e, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
307
- default: return e;
308
- }
580
+ return mapExpr(e, x => {
581
+ if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
582
+ const f = fields.find(f => f.name === x.field);
583
+ if (f)
584
+ return { kind: "var", name: matchBinder(f.name, varName) };
585
+ }
586
+ // If this let shadows the matched variable, stop replacing in the body
587
+ if (x.kind === "let" && x.name === varName)
588
+ return { ...x, value: replaceFieldAccess(x.value, varName, fields) };
589
+ return null;
590
+ });
309
591
  }
310
592
  // ── Transform statements ─────────────────────────────────────
311
593
  function transformStmts(stmts, typeDecls) {
@@ -321,20 +603,76 @@ function transformStmts(stmts, typeDecls) {
321
603
  i += chain.consumed;
322
604
  continue;
323
605
  }
606
+ // 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));
611
+ // 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) {
614
+ return result;
615
+ }
616
+ i++;
617
+ continue;
618
+ }
324
619
  }
325
620
  // Transform for-of → for-in over range
326
621
  if (s.kind === "forof") {
327
- const arrExpr = transformExpr(s.iterable);
328
- const idxName = `_${s.varName}_idx`;
622
+ const varName = s.names[0];
623
+ const varTy = s.nameTypes[0] ?? { kind: "unknown" };
624
+ let iterExpr = transformExpr(s.iterable);
625
+ // Map iteration: for (const [k, v] of map) → iterate keys, look up values
626
+ if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
627
+ const keyName = s.names[0], valueName = s.names[1];
628
+ const keyTy = s.nameTypes[0] ?? { kind: "unknown" };
629
+ const valueTy = s.nameTypes[1] ?? { kind: "unknown" };
630
+ const keysSeqName = `_${keyName}_keys`;
631
+ const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
632
+ result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
633
+ const keysVar = { kind: "var", name: keysSeqName };
634
+ const count = _forofCounters.get(keyName) ?? 0;
635
+ _forofCounters.set(keyName, count + 1);
636
+ const suffix = count === 0 ? "" : `${count + 1}`;
637
+ const idxName = `_${keyName}_idx${suffix}`;
638
+ const idx = { kind: "var", name: idxName };
639
+ const arrSize = { kind: "field", obj: keysVar, field: "size" };
640
+ const bodyStmts = transformStmts(s.body, typeDecls);
641
+ const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
642
+ const letVal = { kind: "let", name: valueName, type: valueTy, mutable: false,
643
+ value: { kind: "methodCall", obj: iterExpr, objTy: s.iterable.ty, method: "getDirect", args: [{ kind: "var", name: keyName }], monadic: false } };
644
+ const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
645
+ result.push({
646
+ kind: "forin", idx: idxName, bound: arrSize,
647
+ invariants: [boundInv, ...s.invariants.map(transformExpr)],
648
+ body: [letKey, letVal, ...bodyStmts],
649
+ });
650
+ i++;
651
+ continue;
652
+ }
653
+ // Sets aren't indexable — bind SetToSeq to a variable for iteration
654
+ if (s.iterable.ty.kind === "set") {
655
+ const seqName = `_${varName}_seq`;
656
+ const convExpr = { kind: "app", fn: "SetToSeq", args: [iterExpr] };
657
+ const elemTy = varTy.kind !== "unknown" ? varTy : { kind: "string" };
658
+ result.push({ kind: "let", name: seqName, type: { kind: "array", elem: elemTy }, mutable: false, value: convExpr });
659
+ iterExpr = { kind: "var", name: seqName };
660
+ }
661
+ const count = _forofCounters.get(varName) ?? 0;
662
+ _forofCounters.set(varName, count + 1);
663
+ const suffix = count === 0 ? "" : `${count + 1}`;
664
+ const idxName = `_${varName}_idx${suffix}`;
329
665
  const idx = { kind: "var", name: idxName };
330
- const arrSize = { kind: "field", obj: arrExpr, field: "size" };
666
+ const arrSize = { kind: "field", obj: iterExpr, field: "size" };
331
667
  const bodyStmts = transformStmts(s.body, typeDecls);
332
- const letElem = { kind: "let", name: s.varName, type: tyToLean(s.varTy), mutable: false, value: { kind: "index", arr: arrExpr, idx } };
668
+ const letElem = { kind: "let", name: varName, type: varTy, mutable: false, value: { kind: "index", arr: iterExpr, idx } };
669
+ // Auto-add bound invariant: idx ≤ bound (always true for range loops)
670
+ const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
333
671
  result.push({
334
672
  kind: "forin",
335
673
  idx: idxName,
336
674
  bound: arrSize,
337
- invariants: s.invariants.map(transformExpr),
675
+ invariants: [boundInv, ...s.invariants.map(transformExpr)],
338
676
  body: [letElem, ...bodyStmts],
339
677
  });
340
678
  i++;
@@ -354,8 +692,71 @@ function liftMethodCalls(e) {
354
692
  function transformStmt(s, typeDecls) {
355
693
  switch (s.kind) {
356
694
  case "let": {
695
+ // Whole-init havoc: emit directly, no lifting
696
+ if (s.init.kind === "havoc") {
697
+ return [{ kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: { kind: "havoc", type: s.init.ty } }];
698
+ }
699
+ // arr.shift()! → let x = arr[0]; arr = arr[1..]
700
+ const init = s.init.kind === "call" ? s.init : undefined;
701
+ if (init && init.fn.kind === "field" && init.fn.field === "shift" && init.fn.obj.ty.kind === "array") {
702
+ const arrName = init.fn.obj.kind === "var" ? init.fn.obj.name : undefined;
703
+ if (arrName) {
704
+ const arrVar = { kind: "var", name: arrName };
705
+ const letHead = { kind: "let", name: s.name, type: s.ty, mutable: s.mutable,
706
+ value: { kind: "index", arr: arrVar, idx: { kind: "num", value: 0 } } };
707
+ const sliceTail = { kind: "assign", target: arrName,
708
+ value: { kind: "methodCall", obj: arrVar, objTy: init.fn.obj.ty, method: "slice", args: [{ kind: "num", value: 1 }], monadic: false } };
709
+ return [letHead, sliceTail];
710
+ }
711
+ }
712
+ // new Map(arr.map(n => [n.field, n])) → let m = map[]; for (n of arr) m[n.field] := n
713
+ if (init && init.fn.kind === "var" && init.fn.name === "__mapFromArray" &&
714
+ init.args.length === 1 && init.args[0].kind === "call" &&
715
+ init.args[0].fn.kind === "field" && init.args[0].fn.field === "map" &&
716
+ init.args[0].args.length === 1 && init.args[0].args[0].kind === "lambda") {
717
+ const arrExpr = init.args[0].fn.obj;
718
+ const lam = init.args[0].args[0];
719
+ const param = lam.params[0]?.name ?? "_";
720
+ const lamBody = Array.isArray(lam.body) ? lam.body : [{ kind: "return", value: lam.body }];
721
+ const retStmt = lamBody.find(b => b.kind === "return");
722
+ if (retStmt && retStmt.value.kind === "arrayLiteral" && retStmt.value.elems.length === 2) {
723
+ const keyExpr = retStmt.value.elems[0];
724
+ const valExpr = retStmt.value.elems[1];
725
+ const arrIR = transformExpr(arrExpr);
726
+ const arrTy = arrExpr.ty;
727
+ const elemTy = arrTy.kind === "array" ? arrTy.elem : { kind: "unknown" };
728
+ const idxName = `_${param}_idx`;
729
+ const idx = { kind: "var", name: idxName };
730
+ const arrSize = { kind: "field", obj: arrIR, field: "size" };
731
+ const elemVar = { kind: "var", name: param };
732
+ const keyIR = transformExpr(keyExpr);
733
+ const valIR = transformExpr(valExpr);
734
+ const mapSet = { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "set", args: [keyIR, valIR], monadic: false };
735
+ // Auto-invariant: all processed elements' keys are in the map
736
+ const kVar = { kind: "var", name: "ki" };
737
+ const mapHasKey = {
738
+ kind: "implies",
739
+ premises: [
740
+ { kind: "binop", op: "≥", left: kVar, right: { kind: "num", value: 0 } },
741
+ { kind: "binop", op: "<", left: kVar, right: idx },
742
+ ],
743
+ conclusion: { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "has", args: [keyIR.kind === "field" ? { kind: "field", obj: { kind: "index", arr: arrIR, idx: kVar }, field: keyIR.field } : keyIR], monadic: false },
744
+ };
745
+ const autoInv = { kind: "forall", var: "ki", type: { kind: "int" }, body: mapHasKey };
746
+ const stmts = [
747
+ { kind: "let", name: s.name, type: s.ty, mutable: true, value: { kind: "emptyMap" } },
748
+ { kind: "forin", idx: idxName, bound: arrSize,
749
+ invariants: [{ kind: "binop", op: "≤", left: idx, right: arrSize }, autoInv],
750
+ body: [
751
+ { kind: "let", name: param, type: elemTy, mutable: false, value: { kind: "index", arr: arrIR, idx } },
752
+ { kind: "assign", target: s.name, value: mapSet },
753
+ ] },
754
+ ];
755
+ return stmts;
756
+ }
757
+ }
357
758
  const { binds, expr } = liftMethodCalls(s.init);
358
- return [...binds, { kind: "let", name: s.name, type: tyToLean(s.ty), mutable: s.mutable, value: expr }];
759
+ return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
359
760
  }
360
761
  case "assign": {
361
762
  // Top-level method call → direct monadic bind, no lifting needed
@@ -371,10 +772,52 @@ function transformStmt(s, typeDecls) {
371
772
  case "break": return [{ kind: "break" }];
372
773
  case "continue": return [{ kind: "continue" }];
373
774
  case "expr": {
775
+ // Mutating collection call: m.set(k, v) → m := m.set(k, v)
776
+ // Same for s.add(x) on sets, arr.push(x)
777
+ if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
778
+ s.expr.fn.obj.kind === "var" &&
779
+ ((s.expr.fn.obj.ty.kind === "map" || s.expr.fn.obj.ty.kind === "set") &&
780
+ (s.expr.fn.field === "set" || s.expr.fn.field === "add" || s.expr.fn.field === "delete")) ||
781
+ (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
782
+ s.expr.fn.obj.kind === "var" && s.expr.fn.obj.ty.kind === "array" &&
783
+ s.expr.fn.field === "push")) {
784
+ const receiver = s.expr.fn.obj.name;
785
+ const { binds, expr } = liftMethodCalls(s.expr);
786
+ return [...binds, { kind: "assign", target: receiver, value: expr }];
787
+ }
788
+ // Optional chaining on map.get: m.get(k)?.push(v) → if k in m { m[k] := m[k] + [v] }
789
+ if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
790
+ s.expr.fn.obj.kind === "call" && s.expr.fn.obj.fn.kind === "field" &&
791
+ s.expr.fn.obj.fn.obj.ty.kind === "map" && s.expr.fn.obj.fn.field === "get" &&
792
+ s.expr.fn.field === "push") {
793
+ const mapExpr = s.expr.fn.obj.fn.obj;
794
+ const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
795
+ const keyExpr = lowerExpr(s.expr.fn.obj.args[0], null);
796
+ const pushArg = lowerExpr(s.expr.args[0], null);
797
+ if (mapName) {
798
+ const mapVar = { kind: "var", name: mapName };
799
+ const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
800
+ const pushed = { kind: "methodCall", obj: directGet, objTy: mapExpr.ty.value, method: "push", args: [pushArg], monadic: false };
801
+ const updated = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "set", args: [keyExpr, pushed], monadic: false };
802
+ const hasCond = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "has", args: [keyExpr], monadic: false };
803
+ return [{ kind: "if", cond: hasCond, then: [{ kind: "assign", target: mapName, value: updated }], else: [] }];
804
+ }
805
+ }
374
806
  const { binds, expr } = liftMethodCalls(s.expr);
375
807
  return [...binds, { kind: "assign", target: "_", value: expr }];
376
808
  }
377
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
+ }
378
821
  // Lift from condition only (Lean rule: don't lift from branches)
379
822
  const { binds, expr: cond } = liftMethodCalls(s.cond);
380
823
  return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
@@ -388,10 +831,18 @@ function transformStmt(s, typeDecls) {
388
831
  doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
389
832
  body: transformStmts(s.body, typeDecls),
390
833
  }];
834
+ case "throw":
835
+ return [{ kind: "assert", expr: { kind: "bool", value: false } }];
391
836
  case "forof":
392
837
  throw new Error("forof should be transformed to forin (range loop) in transformStmts");
393
838
  case "switch":
394
839
  return [emitSwitchStmt(s, typeDecls)];
840
+ case "ghostLet":
841
+ return [{ kind: "ghostLet", name: s.name, type: s.ty, value: transformExpr(s.init) }];
842
+ case "ghostAssign":
843
+ return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
844
+ case "assert":
845
+ return [{ kind: "assert", expr: transformExpr(s.expr) }];
395
846
  }
396
847
  }
397
848
  function detectDiscriminantChain(stmts) {
@@ -443,14 +894,65 @@ function parseDiscriminantCond(cond) {
443
894
  return null;
444
895
  return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
445
896
  }
897
+ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
898
+ let someBranch = negated ? s.else : s.then;
899
+ const noneBranch = negated ? s.then : s.else;
900
+ // Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
901
+ // so include remaining statements as the Some body
902
+ if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
903
+ someBranch = restStmts;
904
+ }
905
+ 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 };
914
+ }
915
+ /** Apply an expression transform to all expressions in a statement (convenience wrapper). */
916
+ function mapStmtExprs(s, r) {
917
+ return mapStmt(s, e => r(e));
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
+ }
934
+ /** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
935
+ function parseOptionalCheck(cond) {
936
+ if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
937
+ return null;
938
+ let varExpr = null;
939
+ if (cond.right.kind === "var" && cond.right.name === "undefined")
940
+ varExpr = cond.left;
941
+ if (cond.left.kind === "var" && cond.left.name === "undefined")
942
+ varExpr = cond.right;
943
+ if (!varExpr || varExpr.kind !== "var" || varExpr.ty.kind !== "optional")
944
+ return null;
945
+ return { varName: varExpr.name, negated: cond.op === "===" };
946
+ }
446
947
  function emitMatchStmt(chain, typeDecls) {
447
948
  const decl = typeDecls.find(d => d.name === chain.typeName);
448
949
  const arms = chain.cases.map(c => {
449
950
  const variant = decl?.variants?.find(v => v.name === c.variant);
450
951
  const fields = variant?.fields ?? [];
451
- const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
452
- let body = transformStmts(c.body, typeDecls);
453
- 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);
454
956
  return { pattern, body };
455
957
  });
456
958
  if (chain.fallthrough.length > 0)
@@ -464,48 +966,56 @@ function emitSwitchStmt(s, typeDecls) {
464
966
  const arms = s.cases.map(c => {
465
967
  const variant = decl?.variants?.find(v => v.name === c.label);
466
968
  const fields = variant?.fields ?? [];
467
- const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
468
- let body = transformStmts(c.body, typeDecls);
469
- 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);
470
973
  return { pattern, body };
471
974
  });
472
975
  if (s.defaultBody.length > 0)
473
976
  arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
474
977
  return { kind: "match", scrutinee: varName, arms };
475
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
+ }
476
996
  function replaceFieldAccessInStmts(stmts, varName, fields) {
477
997
  if (fields.length === 0)
478
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
+ };
479
1007
  const result = [];
480
1008
  for (const s of stmts) {
481
1009
  // If a let shadows the matched variable, stop replacing from here on
482
1010
  if (s.kind === "let" && s.name === varName) {
483
- const r = (e) => replaceFieldAccess(e, varName, fields);
484
- result.push({ ...s, value: r(s.value) });
485
- // Remaining statements see the shadowed name — no more replacement
1011
+ result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
486
1012
  result.push(...stmts.slice(result.length));
487
1013
  break;
488
1014
  }
489
- result.push(replaceFieldAccessInStmt(s, varName, fields));
1015
+ result.push(mapStmt(s, f));
490
1016
  }
491
1017
  return result;
492
1018
  }
493
- function replaceFieldAccessInStmt(s, varName, fields) {
494
- const r = (e) => replaceFieldAccess(e, varName, fields);
495
- switch (s.kind) {
496
- case "let": return { ...s, value: r(s.value) };
497
- case "assign": return { ...s, value: r(s.value) };
498
- case "bind": return { ...s, value: r(s.value) };
499
- case "let-bind": return { ...s, value: r(s.value) };
500
- case "return": return { ...s, value: r(s.value) };
501
- case "break":
502
- case "continue": return s;
503
- case "if": return { ...s, cond: r(s.cond), then: replaceFieldAccessInStmts(s.then, varName, fields), else: replaceFieldAccessInStmts(s.else, varName, fields) };
504
- case "match": return { ...s, arms: s.arms.map(a => ({ ...a, body: replaceFieldAccessInStmts(a.body, varName, fields) })) };
505
- case "while": return { ...s, cond: r(s.cond), body: replaceFieldAccessInStmts(s.body, varName, fields) };
506
- case "forin": return { ...s, invariants: s.invariants.map(r), body: replaceFieldAccessInStmts(s.body, varName, fields) };
507
- }
508
- }
509
1019
  // ── Pure function generation ─────────────────────────────────
510
1020
  function transformPureBody(stmts, typeDecls) {
511
1021
  // Detect discriminant if-chain
@@ -526,6 +1036,29 @@ function transformPureBody(stmts, typeDecls) {
526
1036
  return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
527
1037
  }
528
1038
  case "if": {
1039
+ // 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);
1048
+ if (!someExpr)
1049
+ return null;
1050
+ const noneExpr = transformPureBody(noneBranch, typeDecls);
1051
+ if (!noneExpr)
1052
+ return null;
1053
+ const someReplaced = replaceVar(someExpr, optCheck.varName, { kind: "var", name: bound });
1054
+ return {
1055
+ kind: "match", scrutinee: optCheck.varName,
1056
+ arms: [
1057
+ { pattern: `.some ${bound}`, body: someReplaced },
1058
+ { pattern: ".none", body: noneExpr },
1059
+ ],
1060
+ };
1061
+ }
529
1062
  const thenExpr = transformPureBody(s.then, typeDecls);
530
1063
  if (!thenExpr)
531
1064
  return null;
@@ -545,11 +1078,12 @@ function transformPureSwitch(s, typeDecls) {
545
1078
  const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
546
1079
  if (!decl)
547
1080
  return null;
1081
+ const varName = s.expr.kind === "var" ? s.expr.name : undefined;
548
1082
  const arms = [];
549
1083
  for (const c of s.cases) {
550
1084
  const variant = decl.variants?.find(v => v.name === c.label);
551
1085
  const fields = variant?.fields ?? [];
552
- 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}`;
553
1087
  let body = transformPureBody(c.body, typeDecls);
554
1088
  if (!body)
555
1089
  return null;
@@ -573,7 +1107,7 @@ function transformPureMatch(chain, typeDecls) {
573
1107
  for (const c of chain.cases) {
574
1108
  const variant = decl?.variants?.find(v => v.name === c.variant);
575
1109
  const fields = variant?.fields ?? [];
576
- 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}`;
577
1111
  let body = transformPureBody(c.body, typeDecls);
578
1112
  if (!body)
579
1113
  return null;
@@ -605,50 +1139,89 @@ function transformTypeDecl(d) {
605
1139
  else if (d.kind === "discriminated-union") {
606
1140
  return {
607
1141
  kind: "inductive", name: d.name,
1142
+ typeParams: d.typeParams,
608
1143
  constructors: d.variants.map(v => ({
609
1144
  name: v.name,
610
- fields: v.fields.map(f => ({ name: f.name, type: tyToLean(parseTsType(f.tsType)) })),
1145
+ fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
611
1146
  })),
612
1147
  deriving: ["Repr", "Inhabited"],
613
1148
  };
614
1149
  }
1150
+ else if (d.kind === "alias") {
1151
+ return {
1152
+ kind: "type-alias", name: d.name,
1153
+ target: parseTsType(d.aliasOf),
1154
+ };
1155
+ }
615
1156
  else {
616
1157
  return {
617
1158
  kind: "structure", name: d.name,
618
- fields: d.fields.map(f => ({ name: f.name, type: tyToLean(parseTsType(f.tsType)) })),
1159
+ fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
619
1160
  deriving: ["Repr", "Inhabited", "DecidableEq"],
620
1161
  };
621
1162
  }
622
1163
  }
623
1164
  // ── Helpers ──────────────────────────────────────────────────
1165
+ /** Find parameter names that are reassigned anywhere in the body. */
1166
+ function findReassignedNames(stmts, names) {
1167
+ const found = new Set();
1168
+ function scan(stmts) {
1169
+ for (const s of stmts) {
1170
+ if (s.kind === "assign" && names.has(s.target))
1171
+ found.add(s.target);
1172
+ if (s.kind === "ghostAssign" && names.has(s.target))
1173
+ found.add(s.target);
1174
+ // Mutating collection calls: s.add(x), m.set(k,v), s.delete(x), arr.push(x)
1175
+ if (s.kind === "expr" && s.expr.kind === "call" && s.expr.fn.kind === "field" &&
1176
+ s.expr.fn.obj.kind === "var" && names.has(s.expr.fn.obj.name) &&
1177
+ ["add", "set", "delete", "push"].includes(s.expr.fn.field)) {
1178
+ found.add(s.expr.fn.obj.name);
1179
+ }
1180
+ if (s.kind === "if") {
1181
+ scan(s.then);
1182
+ scan(s.else);
1183
+ }
1184
+ if (s.kind === "while")
1185
+ scan(s.body);
1186
+ if (s.kind === "forof")
1187
+ scan(s.body);
1188
+ if (s.kind === "switch") {
1189
+ for (const c of s.cases)
1190
+ scan(c.body);
1191
+ scan(s.defaultBody);
1192
+ }
1193
+ }
1194
+ }
1195
+ scan(stmts);
1196
+ return found;
1197
+ }
624
1198
  /** Replace all occurrences of a variable name with a new expression. */
625
1199
  function replaceVar(e, name, replacement) {
626
- const r = (x) => replaceVar(x, name, replacement);
627
- switch (e.kind) {
628
- case "var": return e.name === name ? replacement : e;
629
- case "num":
630
- case "bool":
631
- case "str":
632
- case "constructor": return e;
633
- case "binop": return { ...e, left: r(e.left), right: r(e.right) };
634
- case "unop": return { ...e, expr: r(e.expr) };
635
- case "implies": return { ...e, premises: e.premises.map(r), conclusion: r(e.conclusion) };
636
- case "app": return { ...e, args: e.args.map(r) };
637
- case "field": return { ...e, obj: r(e.obj) };
638
- case "toNat": return { ...e, expr: r(e.expr) };
639
- case "index": return { ...e, arr: r(e.arr), idx: r(e.idx) };
640
- case "record": return { ...e, spread: e.spread ? r(e.spread) : null, fields: e.fields.map(f => ({ ...f, value: r(f.value) })) };
641
- case "arrayLiteral": return { ...e, elems: e.elems.map(r) };
642
- case "if": return { ...e, cond: r(e.cond), then: r(e.then), else: r(e.else) };
643
- case "match": return { ...e, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
644
- case "forall": return { ...e, body: e.var === name ? e : { ...e, body: r(e.body) } };
645
- case "exists": return { ...e, body: e.var === name ? e : { ...e, body: r(e.body) } };
646
- case "let": return { ...e, value: r(e.value), body: e.name === name ? e : { ...e, body: r(e.body) } };
647
- case "dotCall": return { ...e, obj: r(e.obj), args: e.args.map(r) };
648
- case "lambda": return e; // don't descend into lambdas
649
- }
1200
+ return mapExpr(e, x => {
1201
+ if (x.kind === "var" && x.name === name)
1202
+ return replacement;
1203
+ // Don't descend past bindings that shadow the name
1204
+ if (x.kind === "forall" && x.var === name)
1205
+ return x;
1206
+ if (x.kind === "exists" && x.var === name)
1207
+ return x;
1208
+ if (x.kind === "let" && x.name === name)
1209
+ return { ...x, value: replaceVar(x.value, name, replacement) };
1210
+ return null;
1211
+ });
650
1212
  }
651
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
+ }
652
1225
  /** Transform for Dafny backend — same logic, Dafny options. */
653
1226
  export function transformModuleDafny(mod) {
654
1227
  const prev = _opts;
@@ -661,7 +1234,16 @@ export function transformModuleDafny(mod) {
661
1234
  }
662
1235
  }
663
1236
  export function transformModule(mod, specImport) {
1237
+ _forofCounters.clear();
1238
+ _typeDecls = mod.typeDecls;
664
1239
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
1240
+ // Module-level constants
1241
+ const constDecls = (mod.constants ?? []).map(c => ({
1242
+ kind: "const",
1243
+ name: c.name,
1244
+ type: c.ty,
1245
+ value: transformExpr(c.value),
1246
+ }));
665
1247
  // Pure function mirrors
666
1248
  const pureDefs = [];
667
1249
  for (const fn of mod.functions) {
@@ -676,8 +1258,9 @@ export function transformModule(mod, specImport) {
676
1258
  pureDefs.push({
677
1259
  kind: "def",
678
1260
  name: fn.name,
679
- params: fn.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })),
680
- returnType: tyToLean(fn.returnTy),
1261
+ typeParams: fn.typeParams,
1262
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1263
+ returnType: fn.returnTy,
681
1264
  requires: fn.requires.map(transformExpr),
682
1265
  ensures,
683
1266
  body,
@@ -686,9 +1269,6 @@ export function transformModule(mod, specImport) {
686
1269
  const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
687
1270
  // Types file
688
1271
  const typesImports = ["LemmaScript"];
689
- for (const m of usedImports)
690
- typesImports.push(MODULE_IMPORTS[m] ?? m);
691
- usedImports.clear();
692
1272
  let typesFile = null;
693
1273
  const pureNamespace = pureDefs.length > 0
694
1274
  ? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
@@ -713,25 +1293,56 @@ export function transformModule(mod, specImport) {
713
1293
  else
714
1294
  ensures.push(transformExpr(e));
715
1295
  }
716
- const body = pureDefNames.has(fn.name)
1296
+ _forofCounters.clear();
1297
+ let body = pureDefNames.has(fn.name)
717
1298
  ? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
718
1299
  : transformStmts(fn.body, mod.typeDecls);
1300
+ // Shadow reassigned parameters with mutable locals
1301
+ const paramNames = new Set(fn.params.map(p => p.name));
1302
+ const reassigned = findReassignedNames(fn.body, paramNames);
1303
+ if (reassigned.size > 0) {
1304
+ const shadows = fn.params
1305
+ .filter(p => reassigned.has(p.name))
1306
+ .map(p => ({ kind: "let", name: p.name, type: p.ty, mutable: true, value: { kind: "var", name: p.name } }));
1307
+ body = [...shadows, ...body];
1308
+ }
719
1309
  return {
720
1310
  kind: "method",
721
1311
  name: fn.name,
722
- params: fn.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })),
723
- returnType: tyToLean(fn.returnTy),
1312
+ typeParams: fn.typeParams,
1313
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1314
+ returnType: fn.returnTy,
724
1315
  requires: fn.requires.map(transformExpr),
725
1316
  ensures,
726
1317
  body,
727
1318
  };
728
1319
  });
1320
+ // Class declarations
1321
+ const classDecls = (mod.classes ?? []).map(cls => {
1322
+ const classMethods = cls.methods.map(fn => {
1323
+ const ensures = fn.ensures.map(transformExpr);
1324
+ _forofCounters.clear();
1325
+ const body = transformStmts(fn.body, mod.typeDecls);
1326
+ return {
1327
+ kind: "method",
1328
+ name: fn.name,
1329
+ typeParams: fn.typeParams,
1330
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1331
+ returnType: fn.returnTy,
1332
+ requires: fn.requires.map(transformExpr),
1333
+ ensures,
1334
+ body,
1335
+ };
1336
+ });
1337
+ return {
1338
+ kind: "class",
1339
+ name: cls.name,
1340
+ fields: cls.fields.map(f => ({ name: f.name, type: f.ty })),
1341
+ methods: classMethods,
1342
+ };
1343
+ });
729
1344
  const defImport = specImport ?? (typesFile ? `«${base}.types»` : null);
730
1345
  const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
731
- if (!typesFile)
732
- for (const m of usedImports)
733
- defBaseImports.push(MODULE_IMPORTS[m] ?? m);
734
- usedImports.clear();
735
1346
  const defFile = {
736
1347
  comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
737
1348
  imports: defBaseImports,
@@ -739,7 +1350,7 @@ export function transformModule(mod, specImport) {
739
1350
  { key: "loom.semantics.termination", value: '"total"' },
740
1351
  { key: "loom.semantics.choice", value: '"demonic"' },
741
1352
  ],
742
- decls: methods,
1353
+ decls: [...constDecls, ...methods, ...classDecls],
743
1354
  };
744
1355
  return { typesFile, defFile };
745
1356
  }