lemmascript 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/tools/dist/ir.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
- * Lean IR — the intermediate representation between transform and emit.
2
+ * IR — the intermediate representation between transform and emit.
3
3
  *
4
4
  * The transform phase produces these types.
5
- * The emit phase pretty-prints them to Lean syntax.
5
+ * The emit phase pretty-prints them to backend syntax (Lean or Dafny).
6
6
  */
7
7
  export {};
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Lean backend commands: gen, check.
3
+ */
4
+ import { existsSync, writeFileSync } from "fs";
5
+ import { execSync } from "child_process";
6
+ import path from "path";
7
+ export function leanGen(typesPath, defPath, typesText, defText) {
8
+ if (typesPath && typesText) {
9
+ writeFileSync(typesPath, typesText);
10
+ console.log(`Generated: ${typesPath}`);
11
+ }
12
+ writeFileSync(defPath, defText);
13
+ console.log(`Generated: ${defPath}`);
14
+ }
15
+ export function leanCheck(dir, base) {
16
+ let lakeDir = dir;
17
+ while (lakeDir !== path.dirname(lakeDir)) {
18
+ if (existsSync(path.join(lakeDir, "lakefile.lean")))
19
+ break;
20
+ lakeDir = path.dirname(lakeDir);
21
+ }
22
+ const proofPath = path.join(dir, `${base}.proof.lean`);
23
+ if (!existsSync(proofPath)) {
24
+ console.error(`No proof file: ${proofPath}`);
25
+ return false;
26
+ }
27
+ console.log("Running lake build...");
28
+ try {
29
+ execSync(`lake build`, { cwd: lakeDir, stdio: "inherit" });
30
+ return true;
31
+ }
32
+ catch {
33
+ return false;
34
+ }
35
+ }
@@ -0,0 +1,397 @@
1
+ /**
2
+ * Lean emitter — IR → Lean text.
3
+ * No logic, no type decisions — just serialization.
4
+ */
5
+ // ── Ty → Lean type string ──────────────────────────────────
6
+ function tyToLean(ty) {
7
+ switch (ty.kind) {
8
+ case "nat": return "Nat";
9
+ case "int": return "Int";
10
+ case "real": return "Float"; // Lean doesn't have exact reals; Float is approximate
11
+ case "bool": return "Bool";
12
+ case "string": return "String";
13
+ case "void": return "Unit";
14
+ case "array": {
15
+ const elem = tyToLean(ty.elem);
16
+ return elem.includes(" ") ? `Array (${elem})` : `Array ${elem}`;
17
+ }
18
+ case "map": {
19
+ const k = tyToLean(ty.key);
20
+ const v = tyToLean(ty.value);
21
+ const kStr = k.includes(" ") ? `(${k})` : k;
22
+ const vStr = v.includes(" ") ? `(${v})` : v;
23
+ return `Std.HashMap ${kStr} ${vStr}`;
24
+ }
25
+ case "set": {
26
+ const elem = tyToLean(ty.elem);
27
+ return elem.includes(" ") ? `Std.HashSet (${elem})` : `Std.HashSet ${elem}`;
28
+ }
29
+ case "optional": {
30
+ const inner = tyToLean(ty.inner);
31
+ return inner.includes(" ") ? `Option (${inner})` : `Option ${inner}`;
32
+ }
33
+ case "user": return ty.name;
34
+ case "unknown": return "_";
35
+ }
36
+ }
37
+ // ── Lean keyword escaping ────────────────────────────────────
38
+ const LEAN_KEYWORDS = new Set([
39
+ "def", "theorem", "lemma", "example", "structure", "class", "instance",
40
+ "inductive", "where", "match", "with", "if", "then", "else", "do",
41
+ "let", "mut", "return", "for", "in", "while", "break", "continue",
42
+ "import", "open", "section", "namespace", "end", "set_option",
43
+ "variable", "axiom", "constant", "private", "protected", "noncomputable",
44
+ "partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
45
+ "at", "from", "to", "deriving", "extends", "true", "false",
46
+ ]);
47
+ function escapeName(name) {
48
+ return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
49
+ }
50
+ // ── Operator precedence (for parenthesization) ──────────────
51
+ const PREC = {
52
+ "→": 1, "∨": 2, "∧": 3,
53
+ "=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
54
+ "+": 5, "-": 5, "++": 5, "arrayConcat": 5, "*": 6, "/": 6, "%": 6,
55
+ };
56
+ function prec(op) { return PREC[op] ?? 10; }
57
+ // ── Method call → Lean syntax ───────────────────────────────
58
+ let _needsJSString = false;
59
+ function emitMethodCall(tyKind, method, monadic, obj, args) {
60
+ // Array methods
61
+ if (tyKind === "array") {
62
+ if (method === "map")
63
+ return `${obj}.${monadic ? "mapM" : "map"} ${args[0]}`;
64
+ if (method === "filter")
65
+ return `${obj}.${monadic ? "filterM" : "filter"} ${args[0]}`;
66
+ if (method === "every")
67
+ return `${obj}.${monadic ? "allM" : "all"} ${args[0]}`;
68
+ if (method === "some")
69
+ return `${obj}.${monadic ? "anyM" : "any"} ${args[0]}`;
70
+ if (method === "includes")
71
+ return `${obj}.contains ${args[0]}`;
72
+ if (method === "find")
73
+ return `${obj}.find? ${args[0]}`;
74
+ if (method === "with")
75
+ return `${obj}.set! ${args[0]} ${args[1]}`;
76
+ if (method === "push")
77
+ return `Array.push ${obj} ${args[0]}`;
78
+ }
79
+ // String methods
80
+ if (tyKind === "string") {
81
+ _needsJSString = true;
82
+ if (method === "indexOf")
83
+ return `JSString.indexOf ${obj} ${args[0]}`;
84
+ if (method === "slice")
85
+ return `JSString.slice ${obj} ${args[0]} ${args[1]}`;
86
+ }
87
+ // Map methods
88
+ if (tyKind === "map") {
89
+ if (method === "get")
90
+ return `${obj}.get? ${args[0]}`;
91
+ if (method === "getDirect")
92
+ return `${obj}.get! ${args[0]}`;
93
+ if (method === "has")
94
+ return `${obj}.contains ${args[0]}`;
95
+ if (method === "set")
96
+ return `${obj}.insert ${args[0]} ${args[1]}`;
97
+ }
98
+ // Set methods
99
+ if (tyKind === "set") {
100
+ if (method === "has")
101
+ return `${obj}.contains ${args[0]}`;
102
+ if (method === "add")
103
+ return `${obj}.insert ${args[0]}`;
104
+ if (method === "delete")
105
+ return `${obj}.erase ${args[0]}`;
106
+ }
107
+ throw new Error(`Unsupported Lean method call: .${method}() on ${tyKind}`);
108
+ }
109
+ // ── Expression emission ─────────────────────────────────────
110
+ function emitExpr(e, parentPrec) {
111
+ switch (e.kind) {
112
+ case "var": return escapeName(e.name);
113
+ case "num": return `${e.value}`;
114
+ case "bool": return e.value ? "true" : "false";
115
+ case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
116
+ case "constructor": return `.${e.name}`;
117
+ case "arrayLiteral":
118
+ if (e.elems.length === 0)
119
+ return `#[]`;
120
+ return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
121
+ case "emptyMap": return `Std.HashMap.empty`;
122
+ case "emptySet": return `Std.HashSet.empty`;
123
+ case "methodCall": {
124
+ const obj = emitExpr(e.obj);
125
+ const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "methodCall";
126
+ const receiver = wrap ? `(${obj})` : obj;
127
+ const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall") ? `(${emitExpr(a)})` : emitExpr(a));
128
+ return emitMethodCall(e.objTy.kind, e.method, e.monadic, receiver, args);
129
+ }
130
+ case "lambda": {
131
+ const params = e.params.map(p => p.name).join(" ");
132
+ // Single return statement → expression lambda
133
+ if (e.body.length === 1 && e.body[0].kind === "return") {
134
+ return `(fun ${params} => ${emitExpr(e.body[0].value)})`;
135
+ }
136
+ // Multi-statement → do block
137
+ return `(fun ${params} => do\n${emitStmts(e.body, 2)})`;
138
+ }
139
+ case "unop":
140
+ if (e.op === "¬")
141
+ return `¬(${emitExpr(e.expr)})`;
142
+ if (e.op === "-" && e.expr.kind === "num")
143
+ return `-${e.expr.value}`;
144
+ return `(-${emitExpr(e.expr)})`;
145
+ case "binop": {
146
+ const op = e.op === "arrayConcat" ? "++" : e.op;
147
+ const s = `${emitExpr(e.left, prec(e.op))} ${op} ${emitExpr(e.right, prec(e.op))}`;
148
+ return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
149
+ }
150
+ case "implies": {
151
+ const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
152
+ const s = parts.join(" → ");
153
+ return parentPrec !== undefined ? `(${s})` : s;
154
+ }
155
+ case "app": {
156
+ const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app" || a.kind === "methodCall") ? `(${emitExpr(a)})` : emitExpr(a));
157
+ // SetToSeq → .toArray for Lean (HashSet has native toArray)
158
+ if (e.fn === "SetToSeq" && args.length === 1)
159
+ return `${args[0]}.toArray`;
160
+ return `${e.fn} ${args.join(" ")}`;
161
+ }
162
+ case "field": {
163
+ const obj = emitExpr(e.obj);
164
+ if (e.field === "collectionSize")
165
+ return `${obj}.size`;
166
+ const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
167
+ return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
168
+ }
169
+ case "toNat": {
170
+ const inner = emitExpr(e.expr);
171
+ const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
172
+ return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
173
+ }
174
+ case "index":
175
+ return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
176
+ case "record": {
177
+ const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
178
+ if (e.spread)
179
+ return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
180
+ return `{ ${fields.join(", ")} }`;
181
+ }
182
+ case "if":
183
+ return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
184
+ case "match": {
185
+ const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
186
+ return `match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with ${arms.join(" ")}`;
187
+ }
188
+ case "forall": return `∀ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
189
+ case "exists": return `∃ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
190
+ case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
191
+ default: throw new Error(`Unsupported Lean expression: ${e.kind}`);
192
+ }
193
+ }
194
+ // ── Statement emission ──────────────────────────────────────
195
+ function emitStmts(stmts, indent) {
196
+ const pad = " ".repeat(indent);
197
+ return stmts.map(s => emitStmt(s, indent)).join("\n");
198
+ }
199
+ function emitStmt(s, indent) {
200
+ const pad = " ".repeat(indent);
201
+ switch (s.kind) {
202
+ case "let":
203
+ return s.mutable
204
+ ? `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`
205
+ : `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
206
+ case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
207
+ case "ghostLet":
208
+ return `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`;
209
+ case "ghostAssign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
210
+ case "assert": return `${pad}assertGadget (${emitExpr(s.expr)})`;
211
+ case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
212
+ case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
213
+ case "return": return `${pad}return ${emitExpr(s.value)}`;
214
+ case "break": return `${pad}break`;
215
+ case "continue": return `${pad}continue`;
216
+ case "if": {
217
+ let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
218
+ if (s.else.length > 0) {
219
+ if (s.else.length === 1 && s.else[0].kind === "if") {
220
+ const ei = s.else[0];
221
+ out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
222
+ if (ei.else.length > 0)
223
+ out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
224
+ }
225
+ else {
226
+ out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
227
+ }
228
+ }
229
+ return out;
230
+ }
231
+ case "match": {
232
+ const scrut = typeof s.scrutinee === "string" ? s.scrutinee : emitExpr(s.scrutinee);
233
+ // Option match (.some/.none) → emit as if/let for WPGen.if compatibility
234
+ if (s.arms.length === 2) {
235
+ const someArm = s.arms.find(a => a.pattern.startsWith(".some "));
236
+ const noneArm = s.arms.find(a => a.pattern === ".none");
237
+ if (someArm && noneArm) {
238
+ const boundVar = someArm.pattern.slice(6); // strip ".some "
239
+ const hName = `h_${scrut.replace(/[^a-zA-Z0-9_]/g, "_")}`;
240
+ const lines = [
241
+ `${pad}if ${hName} : (${scrut}).isSome = true then`,
242
+ `${pad} let ${boundVar} := (${scrut}).get ${hName}`,
243
+ ];
244
+ if (someArm.body.length === 0) {
245
+ lines.push(`${pad} pure ()`);
246
+ }
247
+ else {
248
+ lines.push(emitStmts(someArm.body, indent + 1));
249
+ }
250
+ lines.push(`${pad}else`);
251
+ if (noneArm.body.length === 0) {
252
+ lines.push(`${pad} pure ()`);
253
+ }
254
+ else {
255
+ lines.push(emitStmts(noneArm.body, indent + 1));
256
+ }
257
+ return lines.join("\n");
258
+ }
259
+ }
260
+ // General match
261
+ const lines = [`${pad}match ${scrut} with`];
262
+ for (const arm of s.arms) {
263
+ lines.push(`${pad}| ${arm.pattern} =>`);
264
+ if (arm.body.length === 0) {
265
+ lines.push(`${pad} pure ()`);
266
+ }
267
+ else {
268
+ lines.push(emitStmts(arm.body, indent + 1));
269
+ }
270
+ }
271
+ return lines.join("\n");
272
+ }
273
+ case "while": {
274
+ const lines = [`${pad}while ${emitExpr(s.cond)}`];
275
+ for (const inv of s.invariants)
276
+ lines.push(`${pad} invariant ${emitExpr(inv)}`);
277
+ if (s.doneWith)
278
+ lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
279
+ if (s.decreasing)
280
+ lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
281
+ lines.push(`${pad}do`);
282
+ lines.push(emitStmts(s.body, indent + 1));
283
+ return lines.join("\n");
284
+ }
285
+ case "forin": {
286
+ const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
287
+ for (const inv of s.invariants)
288
+ lines.push(`${pad} invariant ${emitExpr(inv)}`);
289
+ lines.push(`${pad}do`);
290
+ lines.push(emitStmts(s.body, indent + 1));
291
+ return lines.join("\n");
292
+ }
293
+ }
294
+ }
295
+ // ── Declaration emission ─────────────────────────────────────
296
+ function emitDecl(d) {
297
+ switch (d.kind) {
298
+ case "inductive": {
299
+ const lines = [`inductive ${d.name} where`];
300
+ for (const c of d.constructors) {
301
+ if (c.fields.length === 0) {
302
+ lines.push(` | ${c.name} : ${d.name}`);
303
+ }
304
+ else {
305
+ const params = c.fields.map(f => `(${escapeName(f.name)} : ${tyToLean(f.type)})`).join(" ");
306
+ lines.push(` | ${c.name} ${params} : ${d.name}`);
307
+ }
308
+ }
309
+ if (d.deriving.length > 0)
310
+ lines.push(`deriving ${d.deriving.join(", ")}`);
311
+ return lines.join("\n");
312
+ }
313
+ case "structure": {
314
+ const lines = [`structure ${d.name} where`];
315
+ for (const f of d.fields)
316
+ lines.push(` ${escapeName(f.name)} : ${tyToLean(f.type)}`);
317
+ if (d.deriving.length > 0)
318
+ lines.push(`deriving ${d.deriving.join(", ")}`);
319
+ return lines.join("\n");
320
+ }
321
+ case "type-alias": {
322
+ return `abbrev ${d.name} := ${tyToLean(d.target)}`;
323
+ }
324
+ case "def": {
325
+ const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
326
+ return `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
327
+ }
328
+ case "method": {
329
+ const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
330
+ const lines = [`method ${d.name} ${params} return (res : ${tyToLean(d.returnType)})`];
331
+ for (const r of d.requires)
332
+ lines.push(` require ${emitExpr(r)}`);
333
+ for (const e of d.ensures)
334
+ lines.push(` ensures ${emitExpr(e)}`);
335
+ lines.push(" do");
336
+ lines.push(emitStmts(d.body, 2));
337
+ return lines.join("\n");
338
+ }
339
+ case "namespace": {
340
+ const lines = [`namespace ${d.name}`];
341
+ for (const inner of d.decls)
342
+ lines.push("", emitDecl(inner));
343
+ lines.push("", `end ${d.name}`);
344
+ return lines.join("\n");
345
+ }
346
+ case "class":
347
+ throw new Error(`Lean class support not yet implemented: ${d.name}`);
348
+ case "const":
349
+ return `def ${escapeName(d.name)} : ${tyToLean(d.type)} := ${emitExpr(d.value)}`;
350
+ }
351
+ }
352
+ /** Emit a pure expression with indented if/match blocks. */
353
+ function emitPureExpr(e, indent) {
354
+ const pad = " ".repeat(indent);
355
+ switch (e.kind) {
356
+ case "if":
357
+ return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
358
+ case "match": {
359
+ const lines = [`${pad}match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with`];
360
+ for (const arm of e.arms) {
361
+ lines.push(`${pad}| ${arm.pattern} =>`);
362
+ lines.push(emitPureExpr(arm.body, indent + 1));
363
+ }
364
+ return lines.join("\n");
365
+ }
366
+ case "let":
367
+ return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
368
+ default:
369
+ return `${pad}${emitExpr(e)}`;
370
+ }
371
+ }
372
+ // ── File emission ────────────────────────────────────────────
373
+ export function emitLeanFile(file) {
374
+ _needsJSString = false;
375
+ // Emit declarations first so _needsJSString is set
376
+ const declLines = [];
377
+ for (const decl of file.decls) {
378
+ declLines.push("");
379
+ declLines.push(emitDecl(decl));
380
+ }
381
+ const lines = [];
382
+ if (file.comment) {
383
+ lines.push("/-");
384
+ lines.push(file.comment);
385
+ lines.push("-/");
386
+ }
387
+ for (const imp of file.imports)
388
+ lines.push(`import ${imp}`);
389
+ if (_needsJSString && !file.imports.includes("LemmaScript.JSString"))
390
+ lines.push("import LemmaScript.JSString");
391
+ if (file.options.length > 0)
392
+ lines.push("");
393
+ for (const opt of file.options)
394
+ lines.push(`set_option ${opt.key} ${opt.value}`);
395
+ lines.push(...declLines);
396
+ return lines.join("\n") + "\n";
397
+ }
package/tools/dist/lsc.js CHANGED
@@ -4,26 +4,38 @@
4
4
  *
5
5
  * Pipeline: extract → resolve → transform → emit
6
6
  */
7
- import { Project } from "ts-morph";
8
- import { existsSync, writeFileSync } from "fs";
9
- import { execSync } from "child_process";
7
+ import { Project, ScriptTarget } from "ts-morph";
8
+ import { existsSync } from "fs";
10
9
  import path from "path";
11
10
  import { extractModule } from "./extract.js";
12
11
  import { resolveModule } from "./resolve.js";
13
- import { transformModule } from "./transform.js";
14
- import { emitFile } from "./emit.js";
15
- import { transformModuleDafny } from "./transform.js";
12
+ import { transformModuleLean, transformModuleDafny } from "./transform.js";
13
+ import { emitLeanFile } from "./lean-emit.js";
16
14
  import { emitDafnyFile } from "./dafny-emit.js";
17
15
  import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
16
+ import { leanGen, leanCheck } from "./lean-commands.js";
18
17
  function main() {
19
18
  const args = process.argv.slice(2);
20
- const backendIdx = args.indexOf("--backend=dafny");
21
- const backend = backendIdx >= 0 ? "dafny" : "lean";
22
- if (backendIdx >= 0)
19
+ const backendIdx = args.findIndex(a => a.startsWith("--backend="));
20
+ let backend = "dafny";
21
+ if (backendIdx >= 0) {
22
+ const val = args[backendIdx].split("=")[1];
23
+ if (val !== "lean" && val !== "dafny") {
24
+ console.error(`Unknown backend: ${val}. Use --backend=lean or --backend=dafny`);
25
+ process.exit(1);
26
+ }
27
+ backend = val;
23
28
  args.splice(backendIdx, 1);
29
+ }
30
+ const timeLimitIdx = args.findIndex(a => a.startsWith("--time-limit="));
31
+ let timeLimit;
32
+ if (timeLimitIdx >= 0) {
33
+ timeLimit = parseInt(args[timeLimitIdx].split("=")[1]);
34
+ args.splice(timeLimitIdx, 1);
35
+ }
24
36
  const [cmd, filePath] = args;
25
37
  if (!cmd || !filePath) {
26
- console.error("Usage: lsc <gen|check|regen|extract> [--backend=dafny] <file.ts>");
38
+ console.error("Usage: lsc <gen|check|regen|extract> [--backend=lean|dafny] <file.ts>");
27
39
  process.exit(1);
28
40
  }
29
41
  const absPath = path.resolve(filePath);
@@ -31,8 +43,31 @@ function main() {
31
43
  console.error(`File not found: ${absPath}`);
32
44
  process.exit(1);
33
45
  }
34
- const project = new Project({ compilerOptions: { strict: true } });
46
+ // Find nearest tsconfig.json for import resolution; fall back to bare options
47
+ function findTsConfig(from) {
48
+ let dir = path.dirname(from);
49
+ while (true) {
50
+ const candidate = path.join(dir, "tsconfig.json");
51
+ if (existsSync(candidate))
52
+ return candidate;
53
+ const parent = path.dirname(dir);
54
+ if (parent === dir)
55
+ return undefined;
56
+ dir = parent;
57
+ }
58
+ }
59
+ const tsConfigFilePath = findTsConfig(absPath);
60
+ const project = tsConfigFilePath
61
+ ? new Project({ tsConfigFilePath })
62
+ : new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
35
63
  const sourceFile = project.addSourceFileAtPath(absPath);
64
+ project.resolveSourceFileDependencies();
65
+ // Check //@ backend directive — skip if backend doesn't match
66
+ const backendDirective = sourceFile.getFullText().match(/\/\/@ backend (\w+)/);
67
+ if (backendDirective && backendDirective[1] !== backend) {
68
+ console.log(`Skipped: ${path.basename(filePath)} (//@ backend ${backendDirective[1]}, current: ${backend})`);
69
+ return;
70
+ }
36
71
  // Extract: ts-morph → Raw IR
37
72
  const raw = extractModule(sourceFile);
38
73
  if (cmd === "extract") {
@@ -46,27 +81,32 @@ function main() {
46
81
  // ── Dafny backend ─────────────────────────────────────────
47
82
  if (backend === "dafny") {
48
83
  const { typesFile, defFile } = transformModuleDafny(typed);
49
- // Emit types + def into a single Dafny file
50
84
  const allDecls = [...(typesFile?.decls ?? []), ...defFile.decls];
51
85
  const merged = { ...defFile, decls: allDecls };
52
86
  const text = emitDafnyFile(merged, path.basename(filePath));
53
87
  const genPath = path.join(dir, `${base}.dfy.gen`);
54
88
  const dfyPath = path.join(dir, `${base}.dfy`);
55
- const patchPath = path.join(dir, `${base}.dfy.patch`);
89
+ const basePath = path.join(dir, `${base}.dfy.base`);
56
90
  if (cmd === "gen") {
57
91
  dafnyGen(genPath, dfyPath, text);
58
92
  return;
59
93
  }
94
+ if (cmd === "gen-check") {
95
+ dafnyGen(genPath, dfyPath, text);
96
+ if (!dafnyCheckDiff(genPath, dfyPath))
97
+ process.exit(1);
98
+ return;
99
+ }
60
100
  if (cmd === "check") {
61
101
  dafnyGen(genPath, dfyPath, text);
62
102
  if (!dafnyCheckDiff(genPath, dfyPath))
63
103
  process.exit(1);
64
- if (!dafnyVerify(dfyPath, dir))
104
+ if (!dafnyVerify(dfyPath, dir, timeLimit))
65
105
  process.exit(1);
66
106
  return;
67
107
  }
68
108
  if (cmd === "regen") {
69
- dafnyRegen(genPath, dfyPath, patchPath, text, dir);
109
+ dafnyRegen(genPath, dfyPath, basePath, text, dir);
70
110
  return;
71
111
  }
72
112
  console.error(`Unknown command: ${cmd}`);
@@ -75,41 +115,19 @@ function main() {
75
115
  // ── Lean backend ──────────────────────────────────────────
76
116
  const specPath = path.join(dir, `${base}.spec.lean`);
77
117
  const specImport = existsSync(specPath) ? `«${base}.spec»` : undefined;
78
- // Transform: Typed IR Lean IR
79
- const { typesFile, defFile } = transformModule(typed, specImport);
80
- // Emit: Lean IR text
81
- if (typesFile) {
82
- const typesPath = path.join(dir, `${base}.types.lean`);
83
- writeFileSync(typesPath, emitFile(typesFile));
84
- console.log(`Generated: ${typesPath}`);
85
- }
118
+ const { typesFile, defFile } = transformModuleLean(typed, specImport);
119
+ const typesPath = typesFile ? path.join(dir, `${base}.types.lean`) : null;
120
+ const typesText = typesFile ? emitLeanFile(typesFile) : null;
86
121
  const defPath = path.join(dir, `${base}.def.lean`);
122
+ const defText = emitLeanFile(defFile);
87
123
  if (cmd === "gen") {
88
- writeFileSync(defPath, emitFile(defFile));
89
- console.log(`Generated: ${defPath}`);
124
+ leanGen(typesPath, defPath, typesText, defText);
90
125
  return;
91
126
  }
92
127
  if (cmd === "check") {
93
- writeFileSync(defPath, emitFile(defFile));
94
- console.log(`Generated: ${defPath}`);
95
- let lakeDir = dir;
96
- while (lakeDir !== path.dirname(lakeDir)) {
97
- if (existsSync(path.join(lakeDir, "lakefile.lean")))
98
- break;
99
- lakeDir = path.dirname(lakeDir);
100
- }
101
- const proofPath = path.join(dir, `${base}.proof.lean`);
102
- if (!existsSync(proofPath)) {
103
- console.error(`No proof file: ${proofPath}`);
128
+ leanGen(typesPath, defPath, typesText, defText);
129
+ if (!leanCheck(dir, base))
104
130
  process.exit(1);
105
- }
106
- console.log("Running lake build...");
107
- try {
108
- execSync(`lake build`, { cwd: lakeDir, stdio: "inherit" });
109
- }
110
- catch {
111
- process.exit(1);
112
- }
113
131
  return;
114
132
  }
115
133
  console.error(`Unknown command: ${cmd}`);