lemmascript 0.0.1 → 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.
@@ -0,0 +1,1091 @@
1
+ /**
2
+ * Transform — Typed IR → Backend IR.
3
+ *
4
+ * Consumes resolved types and classifications.
5
+ * No type lookups, no string parsing, no re-inference.
6
+ */
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
+ }
69
+ export const LEAN_OPTIONS = {
70
+ backend: "lean",
71
+ monadic: true,
72
+ };
73
+ export const DAFNY_OPTIONS = {
74
+ backend: "dafny",
75
+ monadic: false,
76
+ };
77
+ /** Active options — set before each transform call. */
78
+ let _opts = LEAN_OPTIONS;
79
+ /** Prefix match-bound field names to avoid capturing user variables. */
80
+ function matchBinder(fieldName) {
81
+ return `_${fieldName}`;
82
+ }
83
+ const _forofCounters = new Map();
84
+ function isNat(ty) { return ty.kind === "nat"; }
85
+ function isArray(ty) { return ty.kind === "array"; }
86
+ function isUser(ty) { return ty.kind === "user"; }
87
+ /** Check if transformed lambda body contains monadic binds. */
88
+ function isMonadicBody(stmts) {
89
+ for (const s of stmts) {
90
+ if (s.kind === "let-bind" || s.kind === "bind")
91
+ return true;
92
+ if (s.kind === "if" && (isMonadicBody(s.then) || isMonadicBody(s.else)))
93
+ return true;
94
+ if (s.kind === "while" && isMonadicBody(s.body))
95
+ return true;
96
+ if (s.kind === "forin" && isMonadicBody(s.body))
97
+ return true;
98
+ if (s.kind === "match") {
99
+ for (const arm of s.arms)
100
+ if (isMonadicBody(arm.body))
101
+ return true;
102
+ }
103
+ }
104
+ return false;
105
+ }
106
+ // ── Transform expressions ────────────────────────────────────
107
+ /** Prop-valued operators (for specs/invariants). */
108
+ const OP_MAP = {
109
+ "===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
110
+ "&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
111
+ "==": "=", "!=": "≠",
112
+ };
113
+ /** Bool-valued operators (for code-level conditions needing Decidable). */
114
+ const BOOL_OP_MAP = {
115
+ ...OP_MAP, "===": "==", "!==": "!=",
116
+ };
117
+ function transformExpr(e) { return lowerExpr(e, null); }
118
+ /**
119
+ * Lower a typed expression to Backend IR.
120
+ *
121
+ * When `binds` is non-null, embedded method calls are extracted into
122
+ * `let ← ` binds (monadic lifting / selective ANF). Lifting propagates
123
+ * through binop, unop, and call arguments — the expression kinds where
124
+ * a method call can appear inline in TS. It does NOT propagate into
125
+ * field, index, record, forall, or exists sub-expressions.
126
+ */
127
+ function lowerExpr(e, binds) {
128
+ // Monadic lifting: extract embedded method calls to let-binds
129
+ // Pass binds through to args so nested method calls are also lifted
130
+ if (binds && e.kind === "call" && e.callKind === "method") {
131
+ const name = `_t${_liftCounter++}`;
132
+ const fn = e.fn.kind === "var" ? e.fn.name : `${lowerExpr(e.fn, binds)}`;
133
+ const args = e.args.map(a => lowerExpr(a, binds));
134
+ binds.push({ kind: "let-bind", name, value: { kind: "app", fn, args } });
135
+ return { kind: "var", name };
136
+ }
137
+ switch (e.kind) {
138
+ case "var": return { kind: "var", name: e.name };
139
+ case "num": return { kind: "num", value: e.value };
140
+ case "bool": return { kind: "bool", value: e.value };
141
+ case "result": return { kind: "var", name: "res" };
142
+ case "str":
143
+ if (e.ty.kind === "user")
144
+ return { kind: "constructor", name: e.value, type: e.ty.name };
145
+ return { kind: "str", value: e.value };
146
+ case "unop":
147
+ if (e.op === "-" && e.expr.kind === "num")
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
+ }
163
+ return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
164
+ case "binop": {
165
+ // Implication: flatten (A && B) ==> C → implies [A, B] C
166
+ // Spec-only — no lifting through premises/conclusion.
167
+ if (e.op === "==>") {
168
+ const { premises, conclusion } = flattenImpl(e);
169
+ return { kind: "implies", premises: premises.map(transformExpr), conclusion: transformExpr(conclusion) };
170
+ }
171
+ // Discriminant check: x.discriminant === "foo" → x = .foo (before generic string literal comparison)
172
+ if ((e.op === "===" || e.op === "!==") && e.left.kind === "field" && e.left.isDiscriminant && e.right.kind === "str") {
173
+ const objTy = e.left.obj.ty.kind === "user" ? e.left.obj.ty.name : undefined;
174
+ return {
175
+ kind: "binop",
176
+ op: e.op === "===" ? "=" : "≠",
177
+ left: transformExpr(e.left.obj),
178
+ right: { kind: "constructor", name: e.right.value, type: objTy },
179
+ };
180
+ }
181
+ // String literal comparison — constructor if user type, string literal if string
182
+ if ((e.op === "===" || e.op === "!==") && e.right.kind === "str") {
183
+ const left = lowerExpr(e.left, binds);
184
+ const leftTy = e.left.ty.kind === "user" ? e.left.ty.name : undefined;
185
+ const right = isUser(e.left.ty)
186
+ ? { kind: "constructor", name: e.right.value, type: leftTy }
187
+ : { kind: "str", value: e.right.value };
188
+ return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
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
+ }
231
+ return {
232
+ kind: "binop",
233
+ op: OP_MAP[e.op] ?? e.op,
234
+ left: lowerExpr(e.left, binds),
235
+ right: lowerExpr(e.right, binds),
236
+ };
237
+ }
238
+ case "field":
239
+ if (e.field === "length" && isArray(e.obj.ty))
240
+ return { kind: "field", obj: transformExpr(e.obj), field: "size" };
241
+ if (e.field === "length" && e.obj.ty.kind === "string")
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" };
245
+ return { kind: "field", obj: transformExpr(e.obj), field: e.field };
246
+ case "index": {
247
+ const idx = transformExpr(e.idx);
248
+ const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
249
+ return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
250
+ }
251
+ case "call": {
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
260
+ if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
261
+ const arg = e.args[0];
262
+ if (arg.ty.kind === "real")
263
+ return { kind: "app", fn: "FloorReal", args: [lowerExpr(arg, binds)] };
264
+ if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
265
+ return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
266
+ return lowerExpr(arg, binds);
267
+ }
268
+ // Method call: receiver.method(args) → methodCall node
269
+ if (e.fn.kind === "field") {
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 };
291
+ }
292
+ return result;
293
+ }
294
+ if (e.fn.kind !== "var")
295
+ throw new Error(`Unsupported call expression: ${e.fn.kind}`);
296
+ const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
297
+ return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
298
+ }
299
+ case "record":
300
+ return { kind: "record", spread: e.spread ? lowerExpr(e.spread, binds) : null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
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" };
306
+ return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
307
+ case "lambda":
308
+ return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: p.ty })), body: transformStmts(e.body, []) };
309
+ case "forall":
310
+ return { kind: "forall", var: e.var, type: e.varTy, body: transformExpr(e.body) };
311
+ case "exists":
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 };
342
+ }
343
+ }
344
+ function flattenImpl(e) {
345
+ if (e.kind === "binop" && e.op === "==>") {
346
+ const lhs = splitConj(e.left);
347
+ const rest = flattenImpl(e.right);
348
+ return { premises: [...lhs, ...rest.premises], conclusion: rest.conclusion };
349
+ }
350
+ return { premises: [], conclusion: e };
351
+ }
352
+ function splitConj(e) {
353
+ if (e.kind === "binop" && e.op === "&&")
354
+ return [...splitConj(e.left), ...splitConj(e.right)];
355
+ return [e];
356
+ }
357
+ // ── Ensures-to-match for discriminated unions ────────────────
358
+ function ensuresToMatch(e, typeDecls) {
359
+ if (e.kind !== "binop" || e.op !== "==>")
360
+ return null;
361
+ if (e.left.kind !== "binop" || e.left.op !== "===")
362
+ return null;
363
+ if (e.left.left.kind !== "field" || !e.left.left.isDiscriminant || e.left.right.kind !== "str")
364
+ return null;
365
+ const obj = e.left.left.obj;
366
+ if (obj.kind !== "var" || obj.ty.kind !== "user")
367
+ return null;
368
+ const typeName = obj.ty.name;
369
+ const decl = typeDecls.find(d => d.name === typeName && d.kind === "discriminated-union");
370
+ if (!decl)
371
+ return null;
372
+ const variantName = e.left.right.value;
373
+ const variant = decl.variants?.find(v => v.name === variantName);
374
+ if (!variant)
375
+ return null;
376
+ const fields = variant.fields;
377
+ const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${variantName}`;
378
+ let rhs = transformExpr(e.right);
379
+ rhs = replaceFieldAccess(rhs, obj.name, fields);
380
+ return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
381
+ }
382
+ function replaceFieldAccess(e, varName, fields) {
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
+ });
394
+ }
395
+ // ── Transform statements ─────────────────────────────────────
396
+ function transformStmts(stmts, typeDecls) {
397
+ const result = [];
398
+ let i = 0;
399
+ while (i < stmts.length) {
400
+ const s = stmts[i];
401
+ // Detect discriminant if-chain → match
402
+ if (s.kind === "if") {
403
+ const chain = detectDiscriminantChain(stmts.slice(i));
404
+ if (chain) {
405
+ result.push(emitMatchStmt(chain.chain, typeDecls));
406
+ i += chain.consumed;
407
+ continue;
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
+ }
422
+ }
423
+ // Transform for-of → for-in over range
424
+ if (s.kind === "forof") {
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}`;
468
+ const idx = { kind: "var", name: idxName };
469
+ const arrSize = { kind: "field", obj: iterExpr, field: "size" };
470
+ const bodyStmts = transformStmts(s.body, typeDecls);
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 };
474
+ result.push({
475
+ kind: "forin",
476
+ idx: idxName,
477
+ bound: arrSize,
478
+ invariants: [boundInv, ...s.invariants.map(transformExpr)],
479
+ body: [letElem, ...bodyStmts],
480
+ });
481
+ i++;
482
+ continue;
483
+ }
484
+ result.push(...transformStmt(s, typeDecls));
485
+ i++;
486
+ }
487
+ return result;
488
+ }
489
+ let _liftCounter = 0;
490
+ function liftMethodCalls(e) {
491
+ const binds = [];
492
+ return { binds, expr: lowerExpr(e, binds) };
493
+ }
494
+ // ── Transform statements ─────────────────────────────────────
495
+ function transformStmt(s, typeDecls) {
496
+ switch (s.kind) {
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
+ }
561
+ const { binds, expr } = liftMethodCalls(s.init);
562
+ return [...binds, { kind: "let", name: s.name, type: s.ty, mutable: s.mutable, value: expr }];
563
+ }
564
+ case "assign": {
565
+ // Top-level method call → direct monadic bind, no lifting needed
566
+ if (s.value.kind === "call" && s.value.callKind === "method")
567
+ return [{ kind: "bind", target: s.target, value: transformExpr(s.value) }];
568
+ const { binds, expr } = liftMethodCalls(s.value);
569
+ return [...binds, { kind: "assign", target: s.target, value: expr }];
570
+ }
571
+ case "return": {
572
+ const { binds, expr } = liftMethodCalls(s.value);
573
+ return [...binds, { kind: "return", value: expr }];
574
+ }
575
+ case "break": return [{ kind: "break" }];
576
+ case "continue": return [{ kind: "continue" }];
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
+ }
609
+ const { binds, expr } = liftMethodCalls(s.expr);
610
+ return [...binds, { kind: "assign", target: "_", value: expr }];
611
+ }
612
+ case "if": {
613
+ // Lift from condition only (Lean rule: don't lift from branches)
614
+ const { binds, expr: cond } = liftMethodCalls(s.cond);
615
+ return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
616
+ }
617
+ case "while":
618
+ return [{
619
+ kind: "while",
620
+ cond: transformExpr(s.cond),
621
+ invariants: s.invariants.map(transformExpr),
622
+ decreasing: s.decreases ? transformExpr(s.decreases) : null,
623
+ doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
624
+ body: transformStmts(s.body, typeDecls),
625
+ }];
626
+ case "throw":
627
+ return [{ kind: "assert", expr: { kind: "bool", value: false } }];
628
+ case "forof":
629
+ throw new Error("forof should be transformed to forin (range loop) in transformStmts");
630
+ case "switch":
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) }];
638
+ }
639
+ }
640
+ function detectDiscriminantChain(stmts) {
641
+ if (stmts.length === 0 || stmts[0].kind !== "if")
642
+ return null;
643
+ const first = parseDiscriminantCond(stmts[0].cond);
644
+ if (!first)
645
+ return null;
646
+ const cases = [];
647
+ // Follow else branches within one if-else-if tree
648
+ function collectElse(s) {
649
+ const p = parseDiscriminantCond(s.cond);
650
+ if (!p || p.varName !== first.varName)
651
+ return [s];
652
+ cases.push({ variant: p.variant, body: s.then });
653
+ if (s.else.length === 0)
654
+ return [];
655
+ if (s.else.length === 1 && s.else[0].kind === "if")
656
+ return collectElse(s.else[0]);
657
+ return s.else;
658
+ }
659
+ // Walk consecutive top-level ifs on the same discriminant
660
+ let consumed = 0;
661
+ for (let i = 0; i < stmts.length; i++) {
662
+ const s = stmts[i];
663
+ if (s.kind !== "if")
664
+ break;
665
+ const p = parseDiscriminantCond(s.cond);
666
+ if (!p || p.varName !== first.varName)
667
+ break;
668
+ cases.push({ variant: p.variant, body: s.then });
669
+ consumed = i + 1;
670
+ if (s.else.length > 0) {
671
+ const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
672
+ return cases.length > 0 ? { chain: { ...first, cases, fallthrough: ft }, consumed } : null;
673
+ }
674
+ }
675
+ if (cases.length === 0)
676
+ return null;
677
+ return { chain: { ...first, cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
678
+ }
679
+ function parseDiscriminantCond(cond) {
680
+ // Pattern: x.discriminant === "variant"
681
+ if (cond.kind !== "binop" || cond.op !== "===" || cond.right.kind !== "str")
682
+ return null;
683
+ if (cond.left.kind !== "field" || !cond.left.isDiscriminant)
684
+ return null;
685
+ if (cond.left.obj.kind !== "var" || cond.left.obj.ty.kind !== "user")
686
+ return null;
687
+ return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
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
+ }
724
+ function emitMatchStmt(chain, typeDecls) {
725
+ const decl = typeDecls.find(d => d.name === chain.typeName);
726
+ const arms = chain.cases.map(c => {
727
+ const variant = decl?.variants?.find(v => v.name === c.variant);
728
+ const fields = variant?.fields ?? [];
729
+ const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
730
+ let body = transformStmts(c.body, typeDecls);
731
+ body = replaceFieldAccessInStmts(body, chain.varName, fields);
732
+ return { pattern, body };
733
+ });
734
+ if (chain.fallthrough.length > 0)
735
+ arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
736
+ return { kind: "match", scrutinee: chain.varName, arms };
737
+ }
738
+ function emitSwitchStmt(s, typeDecls) {
739
+ const varName = s.expr.kind === "var" ? s.expr.name : "?";
740
+ const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
741
+ const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
742
+ const arms = s.cases.map(c => {
743
+ const variant = decl?.variants?.find(v => v.name === c.label);
744
+ const fields = variant?.fields ?? [];
745
+ const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
746
+ let body = transformStmts(c.body, typeDecls);
747
+ body = replaceFieldAccessInStmts(body, varName, fields);
748
+ return { pattern, body };
749
+ });
750
+ if (s.defaultBody.length > 0)
751
+ arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
752
+ return { kind: "match", scrutinee: varName, arms };
753
+ }
754
+ function replaceFieldAccessInStmts(stmts, varName, fields) {
755
+ if (fields.length === 0)
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
+ };
765
+ const result = [];
766
+ for (const s of stmts) {
767
+ // If a let shadows the matched variable, stop replacing from here on
768
+ if (s.kind === "let" && s.name === varName) {
769
+ result.push(s.value ? { ...s, value: mapExpr(s.value, f) } : s);
770
+ result.push(...stmts.slice(result.length));
771
+ break;
772
+ }
773
+ result.push(mapStmt(s, f));
774
+ }
775
+ return result;
776
+ }
777
+ // ── Pure function generation ─────────────────────────────────
778
+ function transformPureBody(stmts, typeDecls) {
779
+ // Detect discriminant if-chain
780
+ if (stmts.length > 0 && stmts[0].kind === "if") {
781
+ const chain = detectDiscriminantChain(stmts);
782
+ if (chain)
783
+ return transformPureMatch(chain.chain, typeDecls);
784
+ }
785
+ for (let i = 0; i < stmts.length; i++) {
786
+ const s = stmts[i];
787
+ const rest = stmts.slice(i + 1);
788
+ switch (s.kind) {
789
+ case "return": return transformExpr(s.value);
790
+ case "let": {
791
+ const restExpr = transformPureBody(rest, typeDecls);
792
+ if (!restExpr)
793
+ return null;
794
+ return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
795
+ }
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
+ }
820
+ const thenExpr = transformPureBody(s.then, typeDecls);
821
+ if (!thenExpr)
822
+ return null;
823
+ const elseBranch = s.else.length > 0 ? s.else : rest;
824
+ const elseExpr = transformPureBody(elseBranch, typeDecls);
825
+ if (!elseExpr)
826
+ return null;
827
+ return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
828
+ }
829
+ case "switch": return transformPureSwitch(s, typeDecls);
830
+ default: return null;
831
+ }
832
+ }
833
+ return null;
834
+ }
835
+ function transformPureSwitch(s, typeDecls) {
836
+ const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
837
+ if (!decl)
838
+ return null;
839
+ const arms = [];
840
+ for (const c of s.cases) {
841
+ const variant = decl.variants?.find(v => v.name === c.label);
842
+ const fields = variant?.fields ?? [];
843
+ const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
844
+ let body = transformPureBody(c.body, typeDecls);
845
+ if (!body)
846
+ return null;
847
+ if (fields.length > 0 && s.expr.kind === "var")
848
+ body = replaceFieldAccess(body, s.expr.name, fields);
849
+ arms.push({ pattern, body });
850
+ }
851
+ if (s.defaultBody.length > 0) {
852
+ const body = transformPureBody(s.defaultBody, typeDecls);
853
+ if (!body)
854
+ return null;
855
+ arms.push({ pattern: "_", body });
856
+ }
857
+ if (s.expr.kind !== "var")
858
+ return null;
859
+ return { kind: "match", scrutinee: s.expr.name, arms };
860
+ }
861
+ function transformPureMatch(chain, typeDecls) {
862
+ const decl = typeDecls.find(d => d.name === chain.typeName);
863
+ const arms = [];
864
+ for (const c of chain.cases) {
865
+ const variant = decl?.variants?.find(v => v.name === c.variant);
866
+ const fields = variant?.fields ?? [];
867
+ const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
868
+ let body = transformPureBody(c.body, typeDecls);
869
+ if (!body)
870
+ return null;
871
+ if (fields.length > 0)
872
+ body = replaceFieldAccess(body, chain.varName, fields);
873
+ arms.push({ pattern, body });
874
+ }
875
+ // Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
876
+ // discriminated unions. Skip the catch-all arm when all variants are matched,
877
+ // since Lean errors on redundant match arms.
878
+ const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
879
+ if (chain.fallthrough.length > 0 && !allCovered) {
880
+ const body = transformPureBody(chain.fallthrough, typeDecls);
881
+ if (!body)
882
+ return null;
883
+ arms.push({ pattern: "_", body });
884
+ }
885
+ return { kind: "match", scrutinee: chain.varName, arms };
886
+ }
887
+ // ── Generate type declarations ───────────────────────────────
888
+ function transformTypeDecl(d) {
889
+ if (d.kind === "string-union") {
890
+ return {
891
+ kind: "inductive", name: d.name,
892
+ constructors: d.values.map(v => ({ name: v, fields: [] })),
893
+ deriving: ["Repr", "Inhabited", "DecidableEq"],
894
+ };
895
+ }
896
+ else if (d.kind === "discriminated-union") {
897
+ return {
898
+ kind: "inductive", name: d.name,
899
+ constructors: d.variants.map(v => ({
900
+ name: v.name,
901
+ fields: v.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
902
+ })),
903
+ deriving: ["Repr", "Inhabited"],
904
+ };
905
+ }
906
+ else {
907
+ return {
908
+ kind: "structure", name: d.name,
909
+ fields: d.fields.map(f => ({ name: f.name, type: parseTsType(f.tsType) })),
910
+ deriving: ["Repr", "Inhabited", "DecidableEq"],
911
+ };
912
+ }
913
+ }
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
+ }
948
+ /** Replace all occurrences of a variable name with a new expression. */
949
+ function replaceVar(e, name, replacement) {
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
+ });
962
+ }
963
+ // ── Top-level transform ──────────────────────────────────────
964
+ /** Transform for Dafny backend — same logic, Dafny options. */
965
+ export function transformModuleDafny(mod) {
966
+ const prev = _opts;
967
+ _opts = DAFNY_OPTIONS;
968
+ try {
969
+ return transformModule(mod);
970
+ }
971
+ finally {
972
+ _opts = prev;
973
+ }
974
+ }
975
+ export function transformModule(mod, specImport) {
976
+ _forofCounters.clear();
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
+ }));
985
+ // Pure function mirrors
986
+ const pureDefs = [];
987
+ for (const fn of mod.functions) {
988
+ if (!fn.isPure)
989
+ continue;
990
+ const body = transformPureBody(fn.body, mod.typeDecls);
991
+ if (!body)
992
+ continue;
993
+ // For ensures, replace \result (→ "res") with the function call
994
+ const fnCall = { kind: "app", fn: fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) };
995
+ const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "res", fnCall));
996
+ pureDefs.push({
997
+ kind: "def",
998
+ name: fn.name,
999
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1000
+ returnType: fn.returnTy,
1001
+ requires: fn.requires.map(transformExpr),
1002
+ ensures,
1003
+ body,
1004
+ });
1005
+ }
1006
+ const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
1007
+ // Types file
1008
+ const typesImports = ["LemmaScript"];
1009
+ let typesFile = null;
1010
+ const pureNamespace = pureDefs.length > 0
1011
+ ? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
1012
+ : [];
1013
+ if (typeDecls.length > 0 || pureDefs.length > 0) {
1014
+ typesFile = {
1015
+ comment: " Generated by lsc — Lean types and pure function mirrors.",
1016
+ imports: typesImports,
1017
+ options: [],
1018
+ decls: [...typeDecls, ...pureNamespace],
1019
+ };
1020
+ }
1021
+ // Def file: Velvet methods
1022
+ // Pure functions get a thin wrapper that calls Pure.fnName
1023
+ const pureDefNames = new Set(pureDefs.map(d => d.name));
1024
+ const methods = mod.functions.map(fn => {
1025
+ const ensures = [];
1026
+ for (const e of fn.ensures) {
1027
+ const m = ensuresToMatch(e, mod.typeDecls);
1028
+ if (m)
1029
+ ensures.push(m);
1030
+ else
1031
+ ensures.push(transformExpr(e));
1032
+ }
1033
+ _forofCounters.clear();
1034
+ let body = pureDefNames.has(fn.name)
1035
+ ? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
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
+ }
1046
+ return {
1047
+ kind: "method",
1048
+ name: fn.name,
1049
+ params: fn.params.map(p => ({ name: p.name, type: p.ty })),
1050
+ returnType: fn.returnTy,
1051
+ requires: fn.requires.map(transformExpr),
1052
+ ensures,
1053
+ body,
1054
+ };
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
+ });
1079
+ const defImport = specImport ?? (typesFile ? `«${base}.types»` : null);
1080
+ const defBaseImports = defImport ? [defImport] : ["LemmaScript"];
1081
+ const defFile = {
1082
+ comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
1083
+ imports: defBaseImports,
1084
+ options: [
1085
+ { key: "loom.semantics.termination", value: '"total"' },
1086
+ { key: "loom.semantics.choice", value: '"demonic"' },
1087
+ ],
1088
+ decls: [...constDecls, ...methods, ...classDecls],
1089
+ };
1090
+ return { typesFile, defFile };
1091
+ }