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/README.md +25 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +47 -53
- package/tools/dist/dafny-emit.js +495 -93
- package/tools/dist/extract.js +847 -46
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +397 -0
- package/tools/dist/lsc.js +62 -44
- package/tools/dist/resolve.js +500 -34
- package/tools/dist/specparser.js +66 -9
- package/tools/dist/transform.js +813 -202
- package/tools/dist/types.js +59 -13
package/tools/dist/ir.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
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
|
|
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
|
|
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 {
|
|
14
|
-
import {
|
|
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.
|
|
21
|
-
|
|
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
|
-
|
|
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
|
|
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,
|
|
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
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
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
|
-
|
|
89
|
-
console.log(`Generated: ${defPath}`);
|
|
124
|
+
leanGen(typesPath, defPath, typesText, defText);
|
|
90
125
|
return;
|
|
91
126
|
}
|
|
92
127
|
if (cmd === "check") {
|
|
93
|
-
|
|
94
|
-
|
|
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}`);
|