lemmascript 0.1.0 → 0.2.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,56 +1,78 @@
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": return { ...e, arms: e.arms.map(a => ({ ...a, body: r(a.body) })) };
39
+ case "forall": return { ...e, body: r(e.body) };
40
+ case "exists": return { ...e, body: r(e.body) };
41
+ case "let": return { ...e, value: r(e.value), body: r(e.body) };
42
+ case "methodCall": return { ...e, obj: r(e.obj), args: e.args.map(r) };
43
+ case "lambda": return e;
44
+ }
45
+ }
46
+ /** Map over all expressions in a statement tree. */
47
+ function mapStmt(s, f) {
48
+ const r = (e) => mapExpr(e, f);
49
+ switch (s.kind) {
50
+ case "let": return { ...s, value: r(s.value) };
51
+ case "assign": return { ...s, value: r(s.value) };
52
+ case "bind": return { ...s, value: r(s.value) };
53
+ case "let-bind": return { ...s, value: r(s.value) };
54
+ case "return": return { ...s, value: r(s.value) };
55
+ case "break":
56
+ case "continue": return s;
57
+ case "if": return { ...s, cond: r(s.cond), then: s.then.map(t => mapStmt(t, f)), else: s.else.map(t => mapStmt(t, f)) };
58
+ case "match": return { ...s, arms: s.arms.map(a => ({ ...a, body: a.body.map(t => mapStmt(t, f)) })) };
59
+ case "while": return { ...s, cond: r(s.cond), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
60
+ case "forin": return { ...s, bound: r(s.bound), invariants: s.invariants.map(r), body: s.body.map(t => mapStmt(t, f)) };
61
+ case "ghostLet": return { ...s, value: r(s.value) };
62
+ case "ghostAssign": return { ...s, value: r(s.value) };
63
+ case "assert": return { ...s, expr: r(s.expr) };
64
+ }
65
+ }
66
+ function mapStmts(stmts, f) {
67
+ return stmts.map(s => mapStmt(s, f));
68
+ }
8
69
  export const LEAN_OPTIONS = {
9
70
  backend: "lean",
10
71
  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
72
  };
32
73
  export const DAFNY_OPTIONS = {
33
74
  backend: "dafny",
34
75
  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
76
  };
55
77
  /** Active options — set before each transform call. */
56
78
  let _opts = LEAN_OPTIONS;
@@ -58,13 +80,10 @@ let _opts = LEAN_OPTIONS;
58
80
  function matchBinder(fieldName) {
59
81
  return `_${fieldName}`;
60
82
  }
83
+ const _forofCounters = new Map();
61
84
  function isNat(ty) { return ty.kind === "nat"; }
62
85
  function isArray(ty) { return ty.kind === "array"; }
63
86
  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
87
  /** Check if transformed lambda body contains monadic binds. */
69
88
  function isMonadicBody(stmts) {
70
89
  for (const s of stmts) {
@@ -84,32 +103,20 @@ function isMonadicBody(stmts) {
84
103
  }
85
104
  return false;
86
105
  }
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
106
  // ── Transform expressions ────────────────────────────────────
107
+ /** Prop-valued operators (for specs/invariants). */
105
108
  const OP_MAP = {
106
109
  "===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
107
110
  "&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
108
111
  "==": "=", "!=": "≠",
109
112
  };
113
+ /** Bool-valued operators (for code-level conditions needing Decidable). */
114
+ const BOOL_OP_MAP = {
115
+ ...OP_MAP, "===": "==", "!==": "!=",
116
+ };
110
117
  function transformExpr(e) { return lowerExpr(e, null); }
111
118
  /**
112
- * Lower a typed expression to Lean IR.
119
+ * Lower a typed expression to Backend IR.
113
120
  *
114
121
  * When `binds` is non-null, embedded method calls are extracted into
115
122
  * `let ← ` binds (monadic lifting / selective ANF). Lifting propagates
@@ -139,6 +146,20 @@ function lowerExpr(e, binds) {
139
146
  case "unop":
140
147
  if (e.op === "-" && e.expr.kind === "num")
141
148
  return { kind: "num", value: -e.expr.value };
149
+ // String truthiness: !str → str == ""
150
+ if (e.op === "!" && e.expr.ty.kind === "string")
151
+ return { kind: "binop", op: "=", left: lowerExpr(e.expr, binds), right: { kind: "str", value: "" } };
152
+ // Optional truthiness: !opt → opt is None
153
+ if (e.op === "!" && e.expr.ty.kind === "optional") {
154
+ const bound = matchBinder("value");
155
+ return {
156
+ kind: "match", scrutinee: lowerExpr(e.expr, binds),
157
+ arms: [
158
+ { pattern: `.some ${bound}`, body: { kind: "bool", value: false } },
159
+ { pattern: ".none", body: { kind: "bool", value: true } },
160
+ ],
161
+ };
162
+ }
142
163
  return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
143
164
  case "binop": {
144
165
  // Implication: flatten (A && B) ==> C → implies [A, B] C
@@ -166,6 +187,47 @@ function lowerExpr(e, binds) {
166
187
  : { kind: "str", value: e.right.value };
167
188
  return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
168
189
  }
190
+ // Optional comparison: optExpr op val → match optExpr { Some(v) => v op val, None => false/true }
191
+ if (["===", "!==", ">=", "<=", ">", "<"].includes(e.op) &&
192
+ (e.left.ty.kind === "optional") !== (e.right.ty.kind === "optional")) {
193
+ const [optSide, valSide] = e.left.ty.kind === "optional" ? [e.left, e.right] : [e.right, e.left];
194
+ const optExpr = lowerExpr(optSide, binds);
195
+ // x === undefined → None?, x !== undefined → Some?
196
+ if (valSide.kind === "var" && valSide.name === "undefined") {
197
+ const isNone = e.op === "===";
198
+ return {
199
+ kind: "match", scrutinee: optExpr,
200
+ arms: [
201
+ { pattern: ".some _", body: { kind: "bool", value: !isNone } },
202
+ { pattern: ".none", body: { kind: "bool", value: isNone } },
203
+ ],
204
+ };
205
+ }
206
+ const valExpr = lowerExpr(valSide, binds);
207
+ const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
208
+ const noneVal = e.op === "!==" ? true : false;
209
+ const bound = matchBinder("value");
210
+ return {
211
+ kind: "match", scrutinee: optExpr,
212
+ arms: [
213
+ { pattern: `.some ${bound}`, body: { kind: "binop", op: cmpOp, left: { kind: "var", name: bound }, right: valExpr } },
214
+ { pattern: ".none", body: { kind: "bool", value: noneVal } },
215
+ ],
216
+ };
217
+ }
218
+ // || on optional → match Some/None with default
219
+ if (e.op === "||" && e.left.ty.kind === "optional") {
220
+ const optExpr = lowerExpr(e.left, binds);
221
+ const defaultExpr = lowerExpr(e.right, binds);
222
+ const bound = matchBinder("value");
223
+ return {
224
+ kind: "match", scrutinee: optExpr,
225
+ arms: [
226
+ { pattern: `.some ${bound}`, body: { kind: "var", name: bound } },
227
+ { pattern: ".none", body: defaultExpr },
228
+ ],
229
+ };
230
+ }
169
231
  return {
170
232
  kind: "binop",
171
233
  op: OP_MAP[e.op] ?? e.op,
@@ -178,6 +240,8 @@ function lowerExpr(e, binds) {
178
240
  return { kind: "field", obj: transformExpr(e.obj), field: "size" };
179
241
  if (e.field === "length" && e.obj.ty.kind === "string")
180
242
  return { kind: "field", obj: transformExpr(e.obj), field: "length" };
243
+ if (e.field === "size" && (e.obj.ty.kind === "map" || e.obj.ty.kind === "set"))
244
+ return { kind: "field", obj: transformExpr(e.obj), field: "collectionSize" };
181
245
  return { kind: "field", obj: transformExpr(e.obj), field: e.field };
182
246
  case "index": {
183
247
  const idx = transformExpr(e.idx);
@@ -185,43 +249,47 @@ function lowerExpr(e, binds) {
185
249
  return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
186
250
  }
187
251
  case "call": {
188
- // Math.floor(a / b): Lean int div floors (erase), Dafny truncates (emit JSFloorDiv)
252
+ // Math.ceil(x): CeilReal on real args, identity on int
253
+ if (e.fn.kind === "field" && e.fn.field === "ceil" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
254
+ const arg = e.args[0];
255
+ if (arg.ty.kind === "real")
256
+ return { kind: "app", fn: "CeilReal", args: [lowerExpr(arg, binds)] };
257
+ return lowerExpr(arg, binds);
258
+ }
259
+ // Math.floor(x): FloorReal on real args, JSFloorDiv for int division, identity on int
189
260
  if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
190
261
  const arg = e.args[0];
262
+ if (arg.ty.kind === "real")
263
+ return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
191
264
  if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
192
265
  return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
193
266
  return lowerExpr(arg, binds);
194
267
  }
195
- // Built-in method call: receiver.method(args)
268
+ // Method call: receiver.method(args) → methodCall node
196
269
  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;
270
+ const recv = lowerExpr(e.fn.obj, binds);
271
+ let method = e.fn.field;
272
+ const args = e.args.map((a, i) => {
273
+ const lowered = lowerExpr(a, binds);
274
+ // arr.with index (first arg) needs .toNat when Int-typed
275
+ if (e.fn.kind === "field" && e.fn.field === "with" && e.fn.obj.ty.kind === "array" && i === 0 && !isNat(a.ty))
276
+ return { kind: "toNat", expr: lowered };
277
+ return lowered;
278
+ });
279
+ // Spec-context map get: result type is non-optional → direct access
280
+ if (method === "get" && e.fn.obj.ty.kind === "map" && e.ty.kind !== "optional") {
281
+ method = "getDirect";
282
+ }
283
+ // Check if any lambda arg has monadic body
284
+ const needsMonadic = _opts.monadic && args.some(a => a.kind === "lambda" && isMonadicBody(a.body));
285
+ const result = { kind: "methodCall", obj: recv, objTy: e.fn.obj.ty, method, args, monadic: needsMonadic };
286
+ // Monadic HOF call is itself monadic lift via binds like a method call
287
+ if (_opts.monadic && needsMonadic && binds) {
288
+ const name = `_t${_liftCounter++}`;
289
+ binds.push({ kind: "let-bind", name, value: result });
290
+ return { kind: "var", name };
223
291
  }
224
- throw new Error(`Unsupported method call: .${e.fn.field}() on ${e.fn.obj.ty.kind}`);
292
+ return result;
225
293
  }
226
294
  if (e.fn.kind !== "var")
227
295
  throw new Error(`Unsupported call expression: ${e.fn.kind}`);
@@ -231,15 +299,46 @@ function lowerExpr(e, binds) {
231
299
  case "record":
232
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) })) };
233
301
  case "arrayLiteral":
302
+ if (e.ty.kind === "map" && e.elems.length === 0)
303
+ return { kind: "emptyMap" };
304
+ if (e.ty.kind === "set" && e.elems.length === 0)
305
+ return { kind: "emptySet" };
234
306
  return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
235
307
  case "lambda":
236
- return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })), body: transformStmts(e.body, []) };
308
+ return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
237
309
  case "forall":
238
- return { kind: "forall", var: e.var, type: tyToLean(e.varTy), body: transformExpr(e.body) };
310
+ return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
239
311
  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) };
312
+ return { kind: "exists", var: e.var, type: e.varTy, body: transformExpr(e.body) };
313
+ case "conditional": {
314
+ const cond = lowerExpr(e.cond, binds);
315
+ let thenExpr = lowerExpr(e.then, binds);
316
+ 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] };
324
+ }
325
+ if (e.else.kind === "var" && e.else.name === "undefined") {
326
+ elseExpr = { kind: "constructor", name: ".none" };
327
+ }
328
+ else {
329
+ elseExpr = { kind: "app", fn: "Some", args: [elseExpr] };
330
+ }
331
+ }
332
+ return { kind: "if", cond, then: thenExpr, else: elseExpr };
333
+ }
334
+ case "havoc":
335
+ // Dafny's * only works in var/assign positions — lift to own declaration
336
+ if (binds) {
337
+ const name = `_t${_liftCounter++}`;
338
+ binds.push({ kind: "let", name, type: e.ty, mutable: false, value: { kind: "havoc", type: e.ty } });
339
+ return { kind: "var", name };
340
+ }
341
+ return { kind: "havoc", type: e.ty };
243
342
  }
244
343
  }
245
344
  function flattenImpl(e) {
@@ -281,31 +380,17 @@ function ensuresToMatch(e, typeDecls) {
281
380
  return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
282
381
  }
283
382
  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
- }
383
+ return mapExpr(e, x => {
384
+ if (x.kind === "field" && x.obj.kind === "var" && x.obj.name === varName) {
385
+ const f = fields.find(f => f.name === x.field);
386
+ if (f)
387
+ return { kind: "var", name: matchBinder(f.name) };
388
+ }
389
+ // If this let shadows the matched variable, stop replacing in the body
390
+ if (x.kind === "let" && x.name === varName)
391
+ return { ...x, value: replaceFieldAccess(x.value, varName, fields) };
392
+ return null;
393
+ });
309
394
  }
310
395
  // ── Transform statements ─────────────────────────────────────
311
396
  function transformStmts(stmts, typeDecls) {
@@ -321,20 +406,76 @@ function transformStmts(stmts, typeDecls) {
321
406
  i += chain.consumed;
322
407
  continue;
323
408
  }
409
+ // 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));
414
+ // 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) {
417
+ return result;
418
+ }
419
+ i++;
420
+ continue;
421
+ }
324
422
  }
325
423
  // Transform for-of → for-in over range
326
424
  if (s.kind === "forof") {
327
- const arrExpr = transformExpr(s.iterable);
328
- const idxName = `_${s.varName}_idx`;
425
+ const varName = s.names[0];
426
+ const varTy = s.nameTypes[0] ?? { kind: "unknown" };
427
+ let iterExpr = transformExpr(s.iterable);
428
+ // Map iteration: for (const [k, v] of map) → iterate keys, look up values
429
+ if (s.names.length >= 2 && s.iterable.ty.kind === "map") {
430
+ const keyName = s.names[0], valueName = s.names[1];
431
+ const keyTy = s.nameTypes[0] ?? { kind: "unknown" };
432
+ const valueTy = s.nameTypes[1] ?? { kind: "unknown" };
433
+ const keysSeqName = `_${keyName}_keys`;
434
+ const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
435
+ result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
436
+ const keysVar = { kind: "var", name: keysSeqName };
437
+ const count = _forofCounters.get(keyName) ?? 0;
438
+ _forofCounters.set(keyName, count + 1);
439
+ const suffix = count === 0 ? "" : `${count + 1}`;
440
+ const idxName = `_${keyName}_idx${suffix}`;
441
+ const idx = { kind: "var", name: idxName };
442
+ const arrSize = { kind: "field", obj: keysVar, field: "size" };
443
+ const bodyStmts = transformStmts(s.body, typeDecls);
444
+ const letKey = { kind: "let", name: keyName, type: keyTy, mutable: false, value: { kind: "index", arr: keysVar, idx } };
445
+ const letVal = { kind: "let", name: valueName, type: valueTy, mutable: false,
446
+ value: { kind: "methodCall", obj: iterExpr, objTy: s.iterable.ty, method: "getDirect", args: [{ kind: "var", name: keyName }], monadic: false } };
447
+ const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
448
+ result.push({
449
+ kind: "forin", idx: idxName, bound: arrSize,
450
+ invariants: [boundInv, ...s.invariants.map(transformExpr)],
451
+ body: [letKey, letVal, ...bodyStmts],
452
+ });
453
+ i++;
454
+ continue;
455
+ }
456
+ // Sets aren't indexable — bind SetToSeq to a variable for iteration
457
+ if (s.iterable.ty.kind === "set") {
458
+ const seqName = `_${varName}_seq`;
459
+ const convExpr = { kind: "app", fn: "SetToSeq", args: [iterExpr] };
460
+ const elemTy = varTy.kind !== "unknown" ? varTy : { kind: "string" };
461
+ result.push({ kind: "let", name: seqName, type: { kind: "array", elem: elemTy }, mutable: false, value: convExpr });
462
+ iterExpr = { kind: "var", name: seqName };
463
+ }
464
+ const count = _forofCounters.get(varName) ?? 0;
465
+ _forofCounters.set(varName, count + 1);
466
+ const suffix = count === 0 ? "" : `${count + 1}`;
467
+ const idxName = `_${varName}_idx${suffix}`;
329
468
  const idx = { kind: "var", name: idxName };
330
- const arrSize = { kind: "field", obj: arrExpr, field: "size" };
469
+ const arrSize = { kind: "field", obj: iterExpr, field: "size" };
331
470
  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 } };
471
+ const letElem = { kind: "let", name: varName, type: varTy, mutable: false, value: { kind: "index", arr: iterExpr, idx } };
472
+ // Auto-add bound invariant: idx ≤ bound (always true for range loops)
473
+ const boundInv = { kind: "binop", op: "≤", left: idx, right: arrSize };
333
474
  result.push({
334
475
  kind: "forin",
335
476
  idx: idxName,
336
477
  bound: arrSize,
337
- invariants: s.invariants.map(transformExpr),
478
+ invariants: [boundInv, ...s.invariants.map(transformExpr)],
338
479
  body: [letElem, ...bodyStmts],
339
480
  });
340
481
  i++;
@@ -354,8 +495,71 @@ function liftMethodCalls(e) {
354
495
  function transformStmt(s, typeDecls) {
355
496
  switch (s.kind) {
356
497
  case "let": {
498
+ // Whole-init havoc: emit directly, no lifting
499
+ if (s.init.kind === "havoc") {
500
+ return [{ kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: { kind: "havoc", type: s.init.ty } }];
501
+ }
502
+ // arr.shift()! → let x = arr[0]; arr = arr[1..]
503
+ const init = s.init.kind === "call" ? s.init : undefined;
504
+ if (init && init.fn.kind === "field" && init.fn.field === "shift" && init.fn.obj.ty.kind === "array") {
505
+ const arrName = init.fn.obj.kind === "var" ? init.fn.obj.name : undefined;
506
+ if (arrName) {
507
+ const arrVar = { kind: "var", name: arrName };
508
+ const letHead = { kind: "let", name: s.name, type: s.ty, mutable: s.mutable,
509
+ value: { kind: "index", arr: arrVar, idx: { kind: "num", value: 0 } } };
510
+ const sliceTail = { kind: "assign", target: arrName,
511
+ value: { kind: "methodCall", obj: arrVar, objTy: init.fn.obj.ty, method: "slice", args: [{ kind: "num", value: 1 }], monadic: false } };
512
+ return [letHead, sliceTail];
513
+ }
514
+ }
515
+ // new Map(arr.map(n => [n.field, n])) → let m = map[]; for (n of arr) m[n.field] := n
516
+ if (init && init.fn.kind === "var" && init.fn.name === "__mapFromArray" &&
517
+ init.args.length === 1 && init.args[0].kind === "call" &&
518
+ init.args[0].fn.kind === "field" && init.args[0].fn.field === "map" &&
519
+ init.args[0].args.length === 1 && init.args[0].args[0].kind === "lambda") {
520
+ const arrExpr = init.args[0].fn.obj;
521
+ const lam = init.args[0].args[0];
522
+ const param = lam.params[0]?.name ?? "_";
523
+ const lamBody = Array.isArray(lam.body) ? lam.body : [{ kind: "return", value: lam.body }];
524
+ const retStmt = lamBody.find(b => b.kind === "return");
525
+ if (retStmt && retStmt.value.kind === "arrayLiteral" && retStmt.value.elems.length === 2) {
526
+ const keyExpr = retStmt.value.elems[0];
527
+ const valExpr = retStmt.value.elems[1];
528
+ const arrIR = transformExpr(arrExpr);
529
+ const arrTy = arrExpr.ty;
530
+ const elemTy = arrTy.kind === "array" ? arrTy.elem : { kind: "unknown" };
531
+ const idxName = `_${param}_idx`;
532
+ const idx = { kind: "var", name: idxName };
533
+ const arrSize = { kind: "field", obj: arrIR, field: "size" };
534
+ const elemVar = { kind: "var", name: param };
535
+ const keyIR = transformExpr(keyExpr);
536
+ const valIR = transformExpr(valExpr);
537
+ const mapSet = { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "set", args: [keyIR, valIR], monadic: false };
538
+ // Auto-invariant: all processed elements' keys are in the map
539
+ const kVar = { kind: "var", name: "ki" };
540
+ const mapHasKey = {
541
+ kind: "implies",
542
+ premises: [
543
+ { kind: "binop", op: "≥", left: kVar, right: { kind: "num", value: 0 } },
544
+ { kind: "binop", op: "<", left: kVar, right: idx },
545
+ ],
546
+ 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 },
547
+ };
548
+ const autoInv = { kind: "forall", var: "ki", type: { kind: "int" }, body: mapHasKey };
549
+ const stmts = [
550
+ { kind: "let", name: s.name, type: s.ty, mutable: true, value: { kind: "emptyMap" } },
551
+ { kind: "forin", idx: idxName, bound: arrSize,
552
+ invariants: [{ kind: "binop", op: "≤", left: idx, right: arrSize }, autoInv],
553
+ body: [
554
+ { kind: "let", name: param, type: elemTy, mutable: false, value: { kind: "index", arr: arrIR, idx } },
555
+ { kind: "assign", target: s.name, value: mapSet },
556
+ ] },
557
+ ];
558
+ return stmts;
559
+ }
560
+ }
357
561
  const { binds, expr } = liftMethodCalls(s.init);
358
- return [...binds, { kind: "let", name: s.name, type: tyToLean(s.ty), mutable: s.mutable, value: expr }];
562
+ return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
359
563
  }
360
564
  case "assign": {
361
565
  // Top-level method call → direct monadic bind, no lifting needed
@@ -371,6 +575,37 @@ function transformStmt(s, typeDecls) {
371
575
  case "break": return [{ kind: "break" }];
372
576
  case "continue": return [{ kind: "continue" }];
373
577
  case "expr": {
578
+ // Mutating collection call: m.set(k, v) → m := m.set(k, v)
579
+ // Same for s.add(x) on sets, arr.push(x)
580
+ if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
581
+ s.expr.fn.obj.kind === "var" &&
582
+ ((s.expr.fn.obj.ty.kind === "map" || s.expr.fn.obj.ty.kind === "set") &&
583
+ (s.expr.fn.field === "set" || s.expr.fn.field === "add" || s.expr.fn.field === "delete")) ||
584
+ (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
585
+ s.expr.fn.obj.kind === "var" && s.expr.fn.obj.ty.kind === "array" &&
586
+ s.expr.fn.field === "push")) {
587
+ const receiver = s.expr.fn.obj.name;
588
+ const { binds, expr } = liftMethodCalls(s.expr);
589
+ return [...binds, { kind: "assign", target: receiver, value: expr }];
590
+ }
591
+ // Optional chaining on map.get: m.get(k)?.push(v) → if k in m { m[k] := m[k] + [v] }
592
+ if (s.expr.kind === "call" && s.expr.fn.kind === "field" &&
593
+ s.expr.fn.obj.kind === "call" && s.expr.fn.obj.fn.kind === "field" &&
594
+ s.expr.fn.obj.fn.obj.ty.kind === "map" && s.expr.fn.obj.fn.field === "get" &&
595
+ s.expr.fn.field === "push") {
596
+ const mapExpr = s.expr.fn.obj.fn.obj;
597
+ const mapName = mapExpr.kind === "var" ? mapExpr.name : undefined;
598
+ const keyExpr = lowerExpr(s.expr.fn.obj.args[0], null);
599
+ const pushArg = lowerExpr(s.expr.args[0], null);
600
+ if (mapName) {
601
+ const mapVar = { kind: "var", name: mapName };
602
+ const directGet = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "getDirect", args: [keyExpr], monadic: false };
603
+ const pushed = { kind: "methodCall", obj: directGet, objTy: mapExpr.ty.value, method: "push", args: [pushArg], monadic: false };
604
+ const updated = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "set", args: [keyExpr, pushed], monadic: false };
605
+ const hasCond = { kind: "methodCall", obj: mapVar, objTy: mapExpr.ty, method: "has", args: [keyExpr], monadic: false };
606
+ return [{ kind: "if", cond: hasCond, then: [{ kind: "assign", target: mapName, value: updated }], else: [] }];
607
+ }
608
+ }
374
609
  const { binds, expr } = liftMethodCalls(s.expr);
375
610
  return [...binds, { kind: "assign", target: "_", value: expr }];
376
611
  }
@@ -388,10 +623,18 @@ function transformStmt(s, typeDecls) {
388
623
  doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
389
624
  body: transformStmts(s.body, typeDecls),
390
625
  }];
626
+ case "throw":
627
+ return [{ kind: "assert", expr: { kind: "bool", value: false } }];
391
628
  case "forof":
392
629
  throw new Error("forof should be transformed to forin (range loop) in transformStmts");
393
630
  case "switch":
394
631
  return [emitSwitchStmt(s, typeDecls)];
632
+ case "ghostLet":
633
+ return [{ kind: "ghostLet", name: s.name, type: s.ty, value: transformExpr(s.init) }];
634
+ case "ghostAssign":
635
+ return [{ kind: "ghostAssign", target: s.target, value: transformExpr(s.value) }];
636
+ case "assert":
637
+ return [{ kind: "assert", expr: transformExpr(s.expr) }];
395
638
  }
396
639
  }
397
640
  function detectDiscriminantChain(stmts) {
@@ -443,6 +686,41 @@ function parseDiscriminantCond(cond) {
443
686
  return null;
444
687
  return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
445
688
  }
689
+ function emitOptionalMatch(varName, negated, s, typeDecls, restStmts) {
690
+ let someBranch = negated ? s.else : s.then;
691
+ const noneBranch = negated ? s.then : s.else;
692
+ // Early-return pattern: if (x === undefined) { return ... } — Some branch is empty,
693
+ // so include remaining statements as the Some body
694
+ if (someBranch.length === 0 && restStmts && restStmts.length > 0) {
695
+ someBranch = restStmts;
696
+ }
697
+ 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 };
706
+ }
707
+ /** Apply an expression transform to all expressions in a statement (convenience wrapper). */
708
+ function mapStmtExprs(s, r) {
709
+ return mapStmt(s, e => r(e));
710
+ }
711
+ /** Detect `v !== undefined` or `undefined !== v` where v has optional type. */
712
+ function parseOptionalCheck(cond) {
713
+ if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "==="))
714
+ return null;
715
+ let varExpr = null;
716
+ if (cond.right.kind === "var" && cond.right.name === "undefined")
717
+ varExpr = cond.left;
718
+ if (cond.left.kind === "var" && cond.left.name === "undefined")
719
+ varExpr = cond.right;
720
+ if (!varExpr || varExpr.kind !== "var" || varExpr.ty.kind !== "optional")
721
+ return null;
722
+ return { varName: varExpr.name, negated: cond.op === "===" };
723
+ }
446
724
  function emitMatchStmt(chain, typeDecls) {
447
725
  const decl = typeDecls.find(d => d.name === chain.typeName);
448
726
  const arms = chain.cases.map(c => {
@@ -476,36 +754,26 @@ function emitSwitchStmt(s, typeDecls) {
476
754
  function replaceFieldAccessInStmts(stmts, varName, fields) {
477
755
  if (fields.length === 0)
478
756
  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) };
762
+ }
763
+ return null;
764
+ };
479
765
  const result = [];
480
766
  for (const s of stmts) {
481
767
  // If a let shadows the matched variable, stop replacing from here on
482
768
  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
769
+ result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
486
770
  result.push(...stmts.slice(result.length));
487
771
  break;
488
772
  }
489
- result.push(replaceFieldAccessInStmt(s, varName, fields));
773
+ result.push(mapStmt(s, f));
490
774
  }
491
775
  return result;
492
776
  }
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
777
  // ── Pure function generation ─────────────────────────────────
510
778
  function transformPureBody(stmts, typeDecls) {
511
779
  // Detect discriminant if-chain
@@ -526,6 +794,29 @@ function transformPureBody(stmts, typeDecls) {
526
794
  return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
527
795
  }
528
796
  case "if": {
797
+ // 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);
806
+ if (!someExpr)
807
+ return null;
808
+ const noneExpr = transformPureBody(noneBranch, typeDecls);
809
+ if (!noneExpr)
810
+ return null;
811
+ const someReplaced = replaceVar(someExpr, optCheck.varName, { kind: "var", name: bound });
812
+ return {
813
+ kind: "match", scrutinee: optCheck.varName,
814
+ arms: [
815
+ { pattern: `.some ${bound}`, body: someReplaced },
816
+ { pattern: ".none", body: noneExpr },
817
+ ],
818
+ };
819
+ }
529
820
  const thenExpr = transformPureBody(s.then, typeDecls);
530
821
  if (!thenExpr)
531
822
  return null;
@@ -607,7 +898,7 @@ function transformTypeDecl(d) {
607
898
  kind: "inductive", name: d.name,
608
899
  constructors: d.variants.map(v => ({
609
900
  name: v.name,
610
- fields: v.fields.map(f => ({ name: f.name, type: tyToLean(parseTsType(f.tsType)) })),
901
+ fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
611
902
  })),
612
903
  deriving: ["Repr", "Inhabited"],
613
904
  };
@@ -615,38 +906,59 @@ function transformTypeDecl(d) {
615
906
  else {
616
907
  return {
617
908
  kind: "structure", name: d.name,
618
- fields: d.fields.map(f => ({ name: f.name, type: tyToLean(parseTsType(f.tsType)) })),
909
+ fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
619
910
  deriving: ["Repr", "Inhabited", "DecidableEq"],
620
911
  };
621
912
  }
622
913
  }
623
914
  // ── Helpers ──────────────────────────────────────────────────
915
+ /** Find parameter names that are reassigned anywhere in the body. */
916
+ function findReassignedNames(stmts, names) {
917
+ const found = new Set();
918
+ function scan(stmts) {
919
+ for (const s of stmts) {
920
+ if (s.kind === "assign" && names.has(s.target))
921
+ found.add(s.target);
922
+ if (s.kind === "ghostAssign" && names.has(s.target))
923
+ found.add(s.target);
924
+ // Mutating collection calls: s.add(x), m.set(k,v), s.delete(x), arr.push(x)
925
+ if (s.kind === "expr" && s.expr.kind === "call" && s.expr.fn.kind === "field" &&
926
+ s.expr.fn.obj.kind === "var" && names.has(s.expr.fn.obj.name) &&
927
+ ["add", "set", "delete", "push"].includes(s.expr.fn.field)) {
928
+ found.add(s.expr.fn.obj.name);
929
+ }
930
+ if (s.kind === "if") {
931
+ scan(s.then);
932
+ scan(s.else);
933
+ }
934
+ if (s.kind === "while")
935
+ scan(s.body);
936
+ if (s.kind === "forof")
937
+ scan(s.body);
938
+ if (s.kind === "switch") {
939
+ for (const c of s.cases)
940
+ scan(c.body);
941
+ scan(s.defaultBody);
942
+ }
943
+ }
944
+ }
945
+ scan(stmts);
946
+ return found;
947
+ }
624
948
  /** Replace all occurrences of a variable name with a new expression. */
625
949
  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
- }
950
+ return mapExpr(e, x => {
951
+ if (x.kind === "var" && x.name === name)
952
+ return replacement;
953
+ // Don't descend past bindings that shadow the name
954
+ if (x.kind === "forall" && x.var === name)
955
+ return x;
956
+ if (x.kind === "exists" && x.var === name)
957
+ return x;
958
+ if (x.kind === "let" && x.name === name)
959
+ return { ...x, value: replaceVar(x.value, name, replacement) };
960
+ return null;
961
+ });
650
962
  }
651
963
  // ── Top-level transform ──────────────────────────────────────
652
964
  /** Transform for Dafny backend — same logic, Dafny options. */
@@ -661,7 +973,15 @@ export function transformModuleDafny(mod) {
661
973
  }
662
974
  }
663
975
  export function transformModule(mod, specImport) {
976
+ _forofCounters.clear();
664
977
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
978
+ // Module-level constants
979
+ const constDecls = (mod.constants ?? []).map(c => ({
980
+ kind: "const",
981
+ name: c.name,
982
+ type: c.ty,
983
+ value: transformExpr(c.value),
984
+ }));
665
985
  // Pure function mirrors
666
986
  const pureDefs = [];
667
987
  for (const fn of mod.functions) {
@@ -676,8 +996,8 @@ export function transformModule(mod, specImport) {
676
996
  pureDefs.push({
677
997
  kind: "def",
678
998
  name: fn.name,
679
- params: fn.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })),
680
- returnType: tyToLean(fn.returnTy),
999
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1000
+ returnType: fn.returnTy,
681
1001
  requires: fn.requires.map(transformExpr),
682
1002
  ensures,
683
1003
  body,
@@ -686,9 +1006,6 @@ export function transformModule(mod, specImport) {
686
1006
  const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
687
1007
  // Types file
688
1008
  const typesImports = ["LemmaScript"];
689
- for (const m of usedImports)
690
- typesImports.push(MODULE_IMPORTS[m] ?? m);
691
- usedImports.clear();
692
1009
  let typesFile = null;
693
1010
  const pureNamespace = pureDefs.length > 0
694
1011
  ? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
@@ -713,25 +1030,54 @@ export function transformModule(mod, specImport) {
713
1030
  else
714
1031
  ensures.push(transformExpr(e));
715
1032
  }
716
- const body = pureDefNames.has(fn.name)
1033
+ _forofCounters.clear();
1034
+ let body = pureDefNames.has(fn.name)
717
1035
  ? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
718
1036
  : transformStmts(fn.body, mod.typeDecls);
1037
+ // Shadow reassigned parameters with mutable locals
1038
+ const paramNames = new Set(fn.params.map(p => p.name));
1039
+ const reassigned = findReassignedNames(fn.body, paramNames);
1040
+ if (reassigned.size > 0) {
1041
+ const shadows = fn.params
1042
+ .filter(p => reassigned.has(p.name))
1043
+ .map(p => ({ kind: "let", name: p.name, type: p.ty, mutable: true, value: { kind: "var", name: p.name } }));
1044
+ body = [...shadows, ...body];
1045
+ }
719
1046
  return {
720
1047
  kind: "method",
721
1048
  name: fn.name,
722
- params: fn.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })),
723
- returnType: tyToLean(fn.returnTy),
1049
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1050
+ returnType: fn.returnTy,
724
1051
  requires: fn.requires.map(transformExpr),
725
1052
  ensures,
726
1053
  body,
727
1054
  };
728
1055
  });
1056
+ // Class declarations
1057
+ const classDecls = (mod.classes ?? []).map(cls => {
1058
+ const classMethods = cls.methods.map(fn => {
1059
+ const ensures = fn.ensures.map(transformExpr);
1060
+ _forofCounters.clear();
1061
+ const body = transformStmts(fn.body, mod.typeDecls);
1062
+ return {
1063
+ kind: "method",
1064
+ name: fn.name,
1065
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1066
+ returnType: fn.returnTy,
1067
+ requires: fn.requires.map(transformExpr),
1068
+ ensures,
1069
+ body,
1070
+ };
1071
+ });
1072
+ return {
1073
+ kind: "class",
1074
+ name: cls.name,
1075
+ fields: cls.fields.map(f => ({ name: f.name, type: f.ty })),
1076
+ methods: classMethods,
1077
+ };
1078
+ });
729
1079
  const defImport = specImport ?? (typesFile ? `«${base}.types»` : null);
730
1080
  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
1081
  const defFile = {
736
1082
  comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
737
1083
  imports: defBaseImports,
@@ -739,7 +1085,7 @@ export function transformModule(mod, specImport) {
739
1085
  { key: "loom.semantics.termination", value: '"total"' },
740
1086
  { key: "loom.semantics.choice", value: '"demonic"' },
741
1087
  ],
742
- decls: methods,
1088
+ decls: [...constDecls, ...methods, ...classDecls],
743
1089
  };
744
1090
  return { typesFile, defFile };
745
1091
  }