lemmascript 0.0.1 → 0.1.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,745 @@
1
+ /**
2
+ * Transform — Typed IR → Lean IR.
3
+ *
4
+ * Consumes resolved types and classifications.
5
+ * No type lookups, no string parsing, no re-inference.
6
+ */
7
+ import { parseTsType, tyToLean } from "./types.js";
8
+ export const LEAN_OPTIONS = {
9
+ backend: "lean",
10
+ 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
+ };
32
+ export const DAFNY_OPTIONS = {
33
+ backend: "dafny",
34
+ 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
+ };
55
+ /** Active options — set before each transform call. */
56
+ let _opts = LEAN_OPTIONS;
57
+ /** Prefix match-bound field names to avoid capturing user variables. */
58
+ function matchBinder(fieldName) {
59
+ return `_${fieldName}`;
60
+ }
61
+ function isNat(ty) { return ty.kind === "nat"; }
62
+ function isArray(ty) { return ty.kind === "array"; }
63
+ 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
+ /** Check if transformed lambda body contains monadic binds. */
69
+ function isMonadicBody(stmts) {
70
+ for (const s of stmts) {
71
+ if (s.kind === "let-bind" || s.kind === "bind")
72
+ return true;
73
+ if (s.kind === "if" && (isMonadicBody(s.then) || isMonadicBody(s.else)))
74
+ return true;
75
+ if (s.kind === "while" && isMonadicBody(s.body))
76
+ return true;
77
+ if (s.kind === "forin" && isMonadicBody(s.body))
78
+ return true;
79
+ if (s.kind === "match") {
80
+ for (const arm of s.arms)
81
+ if (isMonadicBody(arm.body))
82
+ return true;
83
+ }
84
+ }
85
+ return false;
86
+ }
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
+ // ── Transform expressions ────────────────────────────────────
105
+ const OP_MAP = {
106
+ "===": "=", "!==": "≠", ">=": "≥", "<=": "≤", ">": ">", "<": "<",
107
+ "&&": "∧", "||": "∨", "+": "+", "-": "-", "*": "*", "/": "/", "%": "%",
108
+ "==": "=", "!=": "≠",
109
+ };
110
+ function transformExpr(e) { return lowerExpr(e, null); }
111
+ /**
112
+ * Lower a typed expression to Lean IR.
113
+ *
114
+ * When `binds` is non-null, embedded method calls are extracted into
115
+ * `let ← ` binds (monadic lifting / selective ANF). Lifting propagates
116
+ * through binop, unop, and call arguments — the expression kinds where
117
+ * a method call can appear inline in TS. It does NOT propagate into
118
+ * field, index, record, forall, or exists sub-expressions.
119
+ */
120
+ function lowerExpr(e, binds) {
121
+ // Monadic lifting: extract embedded method calls to let-binds
122
+ // Pass binds through to args so nested method calls are also lifted
123
+ if (binds && e.kind === "call" && e.callKind === "method") {
124
+ const name = `_t${_liftCounter++}`;
125
+ const fn = e.fn.kind === "var" ? e.fn.name : `${lowerExpr(e.fn, binds)}`;
126
+ const args = e.args.map(a => lowerExpr(a, binds));
127
+ binds.push({ kind: "let-bind", name, value: { kind: "app", fn, args } });
128
+ return { kind: "var", name };
129
+ }
130
+ switch (e.kind) {
131
+ case "var": return { kind: "var", name: e.name };
132
+ case "num": return { kind: "num", value: e.value };
133
+ case "bool": return { kind: "bool", value: e.value };
134
+ case "result": return { kind: "var", name: "res" };
135
+ case "str":
136
+ if (e.ty.kind === "user")
137
+ return { kind: "constructor", name: e.value, type: e.ty.name };
138
+ return { kind: "str", value: e.value };
139
+ case "unop":
140
+ if (e.op === "-" && e.expr.kind === "num")
141
+ return { kind: "num", value: -e.expr.value };
142
+ return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
143
+ case "binop": {
144
+ // Implication: flatten (A && B) ==> C → implies [A, B] C
145
+ // Spec-only — no lifting through premises/conclusion.
146
+ if (e.op === "==>") {
147
+ const { premises, conclusion } = flattenImpl(e);
148
+ return { kind: "implies", premises: premises.map(transformExpr), conclusion: transformExpr(conclusion) };
149
+ }
150
+ // Discriminant check: x.discriminant === "foo" → x = .foo (before generic string literal comparison)
151
+ if ((e.op === "===" || e.op === "!==") && e.left.kind === "field" && e.left.isDiscriminant && e.right.kind === "str") {
152
+ const objTy = e.left.obj.ty.kind === "user" ? e.left.obj.ty.name : undefined;
153
+ return {
154
+ kind: "binop",
155
+ op: e.op === "===" ? "=" : "≠",
156
+ left: transformExpr(e.left.obj),
157
+ right: { kind: "constructor", name: e.right.value, type: objTy },
158
+ };
159
+ }
160
+ // String literal comparison — constructor if user type, string literal if string
161
+ if ((e.op === "===" || e.op === "!==") && e.right.kind === "str") {
162
+ const left = lowerExpr(e.left, binds);
163
+ const leftTy = e.left.ty.kind === "user" ? e.left.ty.name : undefined;
164
+ const right = isUser(e.left.ty)
165
+ ? { kind: "constructor", name: e.right.value, type: leftTy }
166
+ : { kind: "str", value: e.right.value };
167
+ return { kind: "binop", op: e.op === "===" ? "=" : "≠", left, right };
168
+ }
169
+ return {
170
+ kind: "binop",
171
+ op: OP_MAP[e.op] ?? e.op,
172
+ left: lowerExpr(e.left, binds),
173
+ right: lowerExpr(e.right, binds),
174
+ };
175
+ }
176
+ case "field":
177
+ if (e.field === "length" && isArray(e.obj.ty))
178
+ return { kind: "field", obj: transformExpr(e.obj), field: "size" };
179
+ if (e.field === "length" && e.obj.ty.kind === "string")
180
+ return { kind: "field", obj: transformExpr(e.obj), field: "length" };
181
+ return { kind: "field", obj: transformExpr(e.obj), field: e.field };
182
+ case "index": {
183
+ const idx = transformExpr(e.idx);
184
+ const wrappedIdx = isArray(e.obj.ty) && !isNat(e.idx.ty) ? { kind: "toNat", expr: idx } : idx;
185
+ return { kind: "index", arr: transformExpr(e.obj), idx: wrappedIdx };
186
+ }
187
+ case "call": {
188
+ // Math.floor(a / b): Lean int div floors (erase), Dafny truncates (emit JSFloorDiv)
189
+ if (e.fn.kind === "field" && e.fn.field === "floor" && e.fn.obj.kind === "var" && e.fn.obj.name === "Math" && e.args.length === 1) {
190
+ const arg = e.args[0];
191
+ if (_opts.backend === "dafny" && arg.kind === "binop" && arg.op === "/")
192
+ return { kind: "app", fn: "JSFloorDiv", args: [lowerExpr(arg.left, binds), lowerExpr(arg.right, binds)] };
193
+ return lowerExpr(arg, binds);
194
+ }
195
+ // Built-in method call: receiver.method(args)
196
+ 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;
223
+ }
224
+ throw new Error(`Unsupported method call: .${e.fn.field}() on ${e.fn.obj.ty.kind}`);
225
+ }
226
+ if (e.fn.kind !== "var")
227
+ throw new Error(`Unsupported call expression: ${e.fn.kind}`);
228
+ const prefix = e.callKind === "spec-pure" && _opts.backend === "lean" ? "Pure." : "";
229
+ return { kind: "app", fn: prefix + e.fn.name, args: e.args.map(a => lowerExpr(a, binds)) };
230
+ }
231
+ case "record":
232
+ return { kind: "record", spread: e.spread ? lowerExpr(e.spread, binds) : null, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
233
+ case "arrayLiteral":
234
+ return { kind: "arrayLiteral", elems: e.elems.map(el => lowerExpr(el, binds)) };
235
+ case "lambda":
236
+ return { kind: "lambda", params: e.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })), body: transformStmts(e.body, []) };
237
+ case "forall":
238
+ return { kind: "forall", var: e.var, type: tyToLean(e.varTy), body: transformExpr(e.body) };
239
+ 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) };
243
+ }
244
+ }
245
+ function flattenImpl(e) {
246
+ if (e.kind === "binop" && e.op === "==>") {
247
+ const lhs = splitConj(e.left);
248
+ const rest = flattenImpl(e.right);
249
+ return { premises: [...lhs, ...rest.premises], conclusion: rest.conclusion };
250
+ }
251
+ return { premises: [], conclusion: e };
252
+ }
253
+ function splitConj(e) {
254
+ if (e.kind === "binop" && e.op === "&&")
255
+ return [...splitConj(e.left), ...splitConj(e.right)];
256
+ return [e];
257
+ }
258
+ // ── Ensures-to-match for discriminated unions ────────────────
259
+ function ensuresToMatch(e, typeDecls) {
260
+ if (e.kind !== "binop" || e.op !== "==>")
261
+ return null;
262
+ if (e.left.kind !== "binop" || e.left.op !== "===")
263
+ return null;
264
+ if (e.left.left.kind !== "field" || !e.left.left.isDiscriminant || e.left.right.kind !== "str")
265
+ return null;
266
+ const obj = e.left.left.obj;
267
+ if (obj.kind !== "var" || obj.ty.kind !== "user")
268
+ return null;
269
+ const typeName = obj.ty.name;
270
+ const decl = typeDecls.find(d => d.name === typeName && d.kind === "discriminated-union");
271
+ if (!decl)
272
+ return null;
273
+ const variantName = e.left.right.value;
274
+ const variant = decl.variants?.find(v => v.name === variantName);
275
+ if (!variant)
276
+ return null;
277
+ const fields = variant.fields;
278
+ const pattern = fields.length > 0 ? `.${variantName} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${variantName}`;
279
+ let rhs = transformExpr(e.right);
280
+ rhs = replaceFieldAccess(rhs, obj.name, fields);
281
+ return { kind: "match", scrutinee: obj.name, arms: [{ pattern, body: rhs }, { pattern: "_", body: { kind: "bool", value: true } }] };
282
+ }
283
+ 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
+ }
309
+ }
310
+ // ── Transform statements ─────────────────────────────────────
311
+ function transformStmts(stmts, typeDecls) {
312
+ const result = [];
313
+ let i = 0;
314
+ while (i < stmts.length) {
315
+ const s = stmts[i];
316
+ // Detect discriminant if-chain → match
317
+ if (s.kind === "if") {
318
+ const chain = detectDiscriminantChain(stmts.slice(i));
319
+ if (chain) {
320
+ result.push(emitMatchStmt(chain.chain, typeDecls));
321
+ i += chain.consumed;
322
+ continue;
323
+ }
324
+ }
325
+ // Transform for-of → for-in over range
326
+ if (s.kind === "forof") {
327
+ const arrExpr = transformExpr(s.iterable);
328
+ const idxName = `_${s.varName}_idx`;
329
+ const idx = { kind: "var", name: idxName };
330
+ const arrSize = { kind: "field", obj: arrExpr, field: "size" };
331
+ 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 } };
333
+ result.push({
334
+ kind: "forin",
335
+ idx: idxName,
336
+ bound: arrSize,
337
+ invariants: s.invariants.map(transformExpr),
338
+ body: [letElem, ...bodyStmts],
339
+ });
340
+ i++;
341
+ continue;
342
+ }
343
+ result.push(...transformStmt(s, typeDecls));
344
+ i++;
345
+ }
346
+ return result;
347
+ }
348
+ let _liftCounter = 0;
349
+ function liftMethodCalls(e) {
350
+ const binds = [];
351
+ return { binds, expr: lowerExpr(e, binds) };
352
+ }
353
+ // ── Transform statements ─────────────────────────────────────
354
+ function transformStmt(s, typeDecls) {
355
+ switch (s.kind) {
356
+ case "let": {
357
+ const { binds, expr } = liftMethodCalls(s.init);
358
+ return [...binds, { kind: "let", name: s.name, type: tyToLean(s.ty), mutable: s.mutable, value: expr }];
359
+ }
360
+ case "assign": {
361
+ // Top-level method call → direct monadic bind, no lifting needed
362
+ if (s.value.kind === "call" && s.value.callKind === "method")
363
+ return [{ kind: "bind", target: s.target, value: transformExpr(s.value) }];
364
+ const { binds, expr } = liftMethodCalls(s.value);
365
+ return [...binds, { kind: "assign", target: s.target, value: expr }];
366
+ }
367
+ case "return": {
368
+ const { binds, expr } = liftMethodCalls(s.value);
369
+ return [...binds, { kind: "return", value: expr }];
370
+ }
371
+ case "break": return [{ kind: "break" }];
372
+ case "continue": return [{ kind: "continue" }];
373
+ case "expr": {
374
+ const { binds, expr } = liftMethodCalls(s.expr);
375
+ return [...binds, { kind: "assign", target: "_", value: expr }];
376
+ }
377
+ case "if": {
378
+ // Lift from condition only (Lean rule: don't lift from branches)
379
+ const { binds, expr: cond } = liftMethodCalls(s.cond);
380
+ return [...binds, { kind: "if", cond, then: transformStmts(s.then, typeDecls), else: transformStmts(s.else, typeDecls) }];
381
+ }
382
+ case "while":
383
+ return [{
384
+ kind: "while",
385
+ cond: transformExpr(s.cond),
386
+ invariants: s.invariants.map(transformExpr),
387
+ decreasing: s.decreases ? transformExpr(s.decreases) : null,
388
+ doneWith: s.doneWith ? transformExpr(s.doneWith) : null,
389
+ body: transformStmts(s.body, typeDecls),
390
+ }];
391
+ case "forof":
392
+ throw new Error("forof should be transformed to forin (range loop) in transformStmts");
393
+ case "switch":
394
+ return [emitSwitchStmt(s, typeDecls)];
395
+ }
396
+ }
397
+ function detectDiscriminantChain(stmts) {
398
+ if (stmts.length === 0 || stmts[0].kind !== "if")
399
+ return null;
400
+ const first = parseDiscriminantCond(stmts[0].cond);
401
+ if (!first)
402
+ return null;
403
+ const cases = [];
404
+ // Follow else branches within one if-else-if tree
405
+ function collectElse(s) {
406
+ const p = parseDiscriminantCond(s.cond);
407
+ if (!p || p.varName !== first.varName)
408
+ return [s];
409
+ cases.push({ variant: p.variant, body: s.then });
410
+ if (s.else.length === 0)
411
+ return [];
412
+ if (s.else.length === 1 && s.else[0].kind === "if")
413
+ return collectElse(s.else[0]);
414
+ return s.else;
415
+ }
416
+ // Walk consecutive top-level ifs on the same discriminant
417
+ let consumed = 0;
418
+ for (let i = 0; i < stmts.length; i++) {
419
+ const s = stmts[i];
420
+ if (s.kind !== "if")
421
+ break;
422
+ const p = parseDiscriminantCond(s.cond);
423
+ if (!p || p.varName !== first.varName)
424
+ break;
425
+ cases.push({ variant: p.variant, body: s.then });
426
+ consumed = i + 1;
427
+ if (s.else.length > 0) {
428
+ const ft = (s.else.length === 1 && s.else[0].kind === "if") ? collectElse(s.else[0]) : s.else;
429
+ return cases.length > 0 ? { chain: { ...first, cases, fallthrough: ft }, consumed } : null;
430
+ }
431
+ }
432
+ if (cases.length === 0)
433
+ return null;
434
+ return { chain: { ...first, cases, fallthrough: stmts.slice(consumed) }, consumed: stmts.length };
435
+ }
436
+ function parseDiscriminantCond(cond) {
437
+ // Pattern: x.discriminant === "variant"
438
+ if (cond.kind !== "binop" || cond.op !== "===" || cond.right.kind !== "str")
439
+ return null;
440
+ if (cond.left.kind !== "field" || !cond.left.isDiscriminant)
441
+ return null;
442
+ if (cond.left.obj.kind !== "var" || cond.left.obj.ty.kind !== "user")
443
+ return null;
444
+ return { varName: cond.left.obj.name, typeName: cond.left.obj.ty.name, variant: cond.right.value };
445
+ }
446
+ function emitMatchStmt(chain, typeDecls) {
447
+ const decl = typeDecls.find(d => d.name === chain.typeName);
448
+ const arms = chain.cases.map(c => {
449
+ const variant = decl?.variants?.find(v => v.name === c.variant);
450
+ const fields = variant?.fields ?? [];
451
+ const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
452
+ let body = transformStmts(c.body, typeDecls);
453
+ body = replaceFieldAccessInStmts(body, chain.varName, fields);
454
+ return { pattern, body };
455
+ });
456
+ if (chain.fallthrough.length > 0)
457
+ arms.push({ pattern: "_", body: transformStmts(chain.fallthrough, typeDecls) });
458
+ return { kind: "match", scrutinee: chain.varName, arms };
459
+ }
460
+ function emitSwitchStmt(s, typeDecls) {
461
+ const varName = s.expr.kind === "var" ? s.expr.name : "?";
462
+ const typeName = s.expr.ty.kind === "user" ? s.expr.ty.name : undefined;
463
+ const decl = typeName ? typeDecls.find(d => d.name === typeName) : undefined;
464
+ const arms = s.cases.map(c => {
465
+ const variant = decl?.variants?.find(v => v.name === c.label);
466
+ const fields = variant?.fields ?? [];
467
+ const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
468
+ let body = transformStmts(c.body, typeDecls);
469
+ body = replaceFieldAccessInStmts(body, varName, fields);
470
+ return { pattern, body };
471
+ });
472
+ if (s.defaultBody.length > 0)
473
+ arms.push({ pattern: "_", body: transformStmts(s.defaultBody, typeDecls) });
474
+ return { kind: "match", scrutinee: varName, arms };
475
+ }
476
+ function replaceFieldAccessInStmts(stmts, varName, fields) {
477
+ if (fields.length === 0)
478
+ return stmts;
479
+ const result = [];
480
+ for (const s of stmts) {
481
+ // If a let shadows the matched variable, stop replacing from here on
482
+ 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
486
+ result.push(...stmts.slice(result.length));
487
+ break;
488
+ }
489
+ result.push(replaceFieldAccessInStmt(s, varName, fields));
490
+ }
491
+ return result;
492
+ }
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
+ // ── Pure function generation ─────────────────────────────────
510
+ function transformPureBody(stmts, typeDecls) {
511
+ // Detect discriminant if-chain
512
+ if (stmts.length > 0 && stmts[0].kind === "if") {
513
+ const chain = detectDiscriminantChain(stmts);
514
+ if (chain)
515
+ return transformPureMatch(chain.chain, typeDecls);
516
+ }
517
+ for (let i = 0; i < stmts.length; i++) {
518
+ const s = stmts[i];
519
+ const rest = stmts.slice(i + 1);
520
+ switch (s.kind) {
521
+ case "return": return transformExpr(s.value);
522
+ case "let": {
523
+ const restExpr = transformPureBody(rest, typeDecls);
524
+ if (!restExpr)
525
+ return null;
526
+ return { kind: "let", name: s.name, value: transformExpr(s.init), body: restExpr };
527
+ }
528
+ case "if": {
529
+ const thenExpr = transformPureBody(s.then, typeDecls);
530
+ if (!thenExpr)
531
+ return null;
532
+ const elseBranch = s.else.length > 0 ? s.else : rest;
533
+ const elseExpr = transformPureBody(elseBranch, typeDecls);
534
+ if (!elseExpr)
535
+ return null;
536
+ return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
537
+ }
538
+ case "switch": return transformPureSwitch(s, typeDecls);
539
+ default: return null;
540
+ }
541
+ }
542
+ return null;
543
+ }
544
+ function transformPureSwitch(s, typeDecls) {
545
+ const decl = typeDecls.find(d => d.name === (s.expr.ty.kind === "user" ? s.expr.ty.name : ""));
546
+ if (!decl)
547
+ return null;
548
+ const arms = [];
549
+ for (const c of s.cases) {
550
+ const variant = decl.variants?.find(v => v.name === c.label);
551
+ const fields = variant?.fields ?? [];
552
+ const pattern = fields.length > 0 ? `.${c.label} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.label}`;
553
+ let body = transformPureBody(c.body, typeDecls);
554
+ if (!body)
555
+ return null;
556
+ if (fields.length > 0 && s.expr.kind === "var")
557
+ body = replaceFieldAccess(body, s.expr.name, fields);
558
+ arms.push({ pattern, body });
559
+ }
560
+ if (s.defaultBody.length > 0) {
561
+ const body = transformPureBody(s.defaultBody, typeDecls);
562
+ if (!body)
563
+ return null;
564
+ arms.push({ pattern: "_", body });
565
+ }
566
+ if (s.expr.kind !== "var")
567
+ return null;
568
+ return { kind: "match", scrutinee: s.expr.name, arms };
569
+ }
570
+ function transformPureMatch(chain, typeDecls) {
571
+ const decl = typeDecls.find(d => d.name === chain.typeName);
572
+ const arms = [];
573
+ for (const c of chain.cases) {
574
+ const variant = decl?.variants?.find(v => v.name === c.variant);
575
+ const fields = variant?.fields ?? [];
576
+ const pattern = fields.length > 0 ? `.${c.variant} ${fields.map(f => matchBinder(f.name)).join(" ")}` : `.${c.variant}`;
577
+ let body = transformPureBody(c.body, typeDecls);
578
+ if (!body)
579
+ return null;
580
+ if (fields.length > 0)
581
+ body = replaceFieldAccess(body, chain.varName, fields);
582
+ arms.push({ pattern, body });
583
+ }
584
+ // Idiomatic TS often has an unreachable fallthrough after exhaustive if-chains on
585
+ // discriminated unions. Skip the catch-all arm when all variants are matched,
586
+ // since Lean errors on redundant match arms.
587
+ const allCovered = decl?.variants && chain.cases.length >= decl.variants.length;
588
+ if (chain.fallthrough.length > 0 && !allCovered) {
589
+ const body = transformPureBody(chain.fallthrough, typeDecls);
590
+ if (!body)
591
+ return null;
592
+ arms.push({ pattern: "_", body });
593
+ }
594
+ return { kind: "match", scrutinee: chain.varName, arms };
595
+ }
596
+ // ── Generate type declarations ───────────────────────────────
597
+ function transformTypeDecl(d) {
598
+ if (d.kind === "string-union") {
599
+ return {
600
+ kind: "inductive", name: d.name,
601
+ constructors: d.values.map(v => ({ name: v, fields: [] })),
602
+ deriving: ["Repr", "Inhabited", "DecidableEq"],
603
+ };
604
+ }
605
+ else if (d.kind === "discriminated-union") {
606
+ return {
607
+ kind: "inductive", name: d.name,
608
+ constructors: d.variants.map(v => ({
609
+ name: v.name,
610
+ fields: v.fields.map(f => ({ name: f.name, type: tyToLean(parseTsType(f.tsType)) })),
611
+ })),
612
+ deriving: ["Repr", "Inhabited"],
613
+ };
614
+ }
615
+ else {
616
+ return {
617
+ kind: "structure", name: d.name,
618
+ fields: d.fields.map(f => ({ name: f.name, type: tyToLean(parseTsType(f.tsType)) })),
619
+ deriving: ["Repr", "Inhabited", "DecidableEq"],
620
+ };
621
+ }
622
+ }
623
+ // ── Helpers ──────────────────────────────────────────────────
624
+ /** Replace all occurrences of a variable name with a new expression. */
625
+ 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
+ }
650
+ }
651
+ // ── Top-level transform ──────────────────────────────────────
652
+ /** Transform for Dafny backend — same logic, Dafny options. */
653
+ export function transformModuleDafny(mod) {
654
+ const prev = _opts;
655
+ _opts = DAFNY_OPTIONS;
656
+ try {
657
+ return transformModule(mod);
658
+ }
659
+ finally {
660
+ _opts = prev;
661
+ }
662
+ }
663
+ export function transformModule(mod, specImport) {
664
+ const typeDecls = mod.typeDecls.map(transformTypeDecl);
665
+ // Pure function mirrors
666
+ const pureDefs = [];
667
+ for (const fn of mod.functions) {
668
+ if (!fn.isPure)
669
+ continue;
670
+ const body = transformPureBody(fn.body, mod.typeDecls);
671
+ if (!body)
672
+ continue;
673
+ // For ensures, replace \result (→ "res") with the function call
674
+ const fnCall = { kind: "app", fn: fn.name, args: fn.params.map(p => ({ kind: "var", name: p.name })) };
675
+ const ensures = fn.ensures.map(e => replaceVar(transformExpr(e), "res", fnCall));
676
+ pureDefs.push({
677
+ kind: "def",
678
+ name: fn.name,
679
+ params: fn.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })),
680
+ returnType: tyToLean(fn.returnTy),
681
+ requires: fn.requires.map(transformExpr),
682
+ ensures,
683
+ body,
684
+ });
685
+ }
686
+ const base = mod.file.split("/").pop()?.replace(/\.ts$/, "") ?? "module";
687
+ // Types file
688
+ const typesImports = ["LemmaScript"];
689
+ for (const m of usedImports)
690
+ typesImports.push(MODULE_IMPORTS[m] ?? m);
691
+ usedImports.clear();
692
+ let typesFile = null;
693
+ const pureNamespace = pureDefs.length > 0
694
+ ? [{ kind: "namespace", name: "Pure", decls: pureDefs }]
695
+ : [];
696
+ if (typeDecls.length > 0 || pureDefs.length > 0) {
697
+ typesFile = {
698
+ comment: " Generated by lsc — Lean types and pure function mirrors.",
699
+ imports: typesImports,
700
+ options: [],
701
+ decls: [...typeDecls, ...pureNamespace],
702
+ };
703
+ }
704
+ // Def file: Velvet methods
705
+ // Pure functions get a thin wrapper that calls Pure.fnName
706
+ const pureDefNames = new Set(pureDefs.map(d => d.name));
707
+ const methods = mod.functions.map(fn => {
708
+ const ensures = [];
709
+ for (const e of fn.ensures) {
710
+ const m = ensuresToMatch(e, mod.typeDecls);
711
+ if (m)
712
+ ensures.push(m);
713
+ else
714
+ ensures.push(transformExpr(e));
715
+ }
716
+ const body = pureDefNames.has(fn.name)
717
+ ? [{ kind: "return", value: { kind: "app", fn: `Pure.${fn.name}`, args: fn.params.map(p => ({ kind: "var", name: p.name })) } }]
718
+ : transformStmts(fn.body, mod.typeDecls);
719
+ return {
720
+ kind: "method",
721
+ name: fn.name,
722
+ params: fn.params.map(p => ({ name: p.name, type: tyToLean(p.ty) })),
723
+ returnType: tyToLean(fn.returnTy),
724
+ requires: fn.requires.map(transformExpr),
725
+ ensures,
726
+ body,
727
+ };
728
+ });
729
+ const defImport = specImport ?? (typesFile ? `«${base}.types»` : null);
730
+ 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
+ const defFile = {
736
+ comment: " Generated by lsc from " + (mod.file.split("/").pop() ?? "") + "\n Do not edit — re-run `lsc gen` to regenerate.",
737
+ imports: defBaseImports,
738
+ options: [
739
+ { key: "loom.semantics.termination", value: '"total"' },
740
+ { key: "loom.semantics.choice", value: '"demonic"' },
741
+ ],
742
+ decls: methods,
743
+ };
744
+ return { typesFile, defFile };
745
+ }