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.
- package/LICENSE +21 -0
- package/README.md +94 -3
- package/package.json +30 -20
- package/tools/dist/dafny-commands.js +98 -0
- package/tools/dist/dafny-emit.js +738 -0
- package/tools/dist/emit.js +253 -0
- package/tools/dist/extract.js +806 -0
- package/tools/dist/ir.js +7 -0
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +119 -0
- package/tools/dist/rawir.js +10 -0
- package/tools/dist/resolve.js +717 -0
- package/tools/dist/specparser.js +305 -0
- package/tools/dist/transform.js +1091 -0
- package/tools/dist/typedir.js +7 -0
- package/tools/dist/types.js +71 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -14
package/tools/dist/ir.js
ADDED
|
@@ -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,393 @@
|
|
|
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, "*": 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 s = `${emitExpr(e.left, prec(e.op))} ${e.op} ${emitExpr(e.right, prec(e.op))}`;
|
|
147
|
+
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
148
|
+
}
|
|
149
|
+
case "implies": {
|
|
150
|
+
const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
|
|
151
|
+
const s = parts.join(" → ");
|
|
152
|
+
return parentPrec !== undefined ? `(${s})` : s;
|
|
153
|
+
}
|
|
154
|
+
case "app": {
|
|
155
|
+
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));
|
|
156
|
+
// SetToSeq → .toArray for Lean (HashSet has native toArray)
|
|
157
|
+
if (e.fn === "SetToSeq" && args.length === 1)
|
|
158
|
+
return `${args[0]}.toArray`;
|
|
159
|
+
return `${e.fn} ${args.join(" ")}`;
|
|
160
|
+
}
|
|
161
|
+
case "field": {
|
|
162
|
+
const obj = emitExpr(e.obj);
|
|
163
|
+
if (e.field === "collectionSize")
|
|
164
|
+
return `${obj}.size`;
|
|
165
|
+
const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
|
|
166
|
+
return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
|
|
167
|
+
}
|
|
168
|
+
case "toNat": {
|
|
169
|
+
const inner = emitExpr(e.expr);
|
|
170
|
+
const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
|
|
171
|
+
return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
|
|
172
|
+
}
|
|
173
|
+
case "index":
|
|
174
|
+
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
|
|
175
|
+
case "record": {
|
|
176
|
+
const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
177
|
+
if (e.spread)
|
|
178
|
+
return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
|
|
179
|
+
return `{ ${fields.join(", ")} }`;
|
|
180
|
+
}
|
|
181
|
+
case "if":
|
|
182
|
+
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
183
|
+
case "match": {
|
|
184
|
+
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
185
|
+
return `match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with ${arms.join(" ")}`;
|
|
186
|
+
}
|
|
187
|
+
case "forall": return `∀ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
|
|
188
|
+
case "exists": return `∃ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
|
|
189
|
+
case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
|
|
190
|
+
default: throw new Error(`Unsupported Lean expression: ${e.kind}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
// ── Statement emission ──────────────────────────────────────
|
|
194
|
+
function emitStmts(stmts, indent) {
|
|
195
|
+
const pad = " ".repeat(indent);
|
|
196
|
+
return stmts.map(s => emitStmt(s, indent)).join("\n");
|
|
197
|
+
}
|
|
198
|
+
function emitStmt(s, indent) {
|
|
199
|
+
const pad = " ".repeat(indent);
|
|
200
|
+
switch (s.kind) {
|
|
201
|
+
case "let":
|
|
202
|
+
return s.mutable
|
|
203
|
+
? `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`
|
|
204
|
+
: `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
|
|
205
|
+
case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
206
|
+
case "ghostLet":
|
|
207
|
+
return `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`;
|
|
208
|
+
case "ghostAssign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
209
|
+
case "assert": return `${pad}assertGadget (${emitExpr(s.expr)})`;
|
|
210
|
+
case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
|
|
211
|
+
case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
|
|
212
|
+
case "return": return `${pad}return ${emitExpr(s.value)}`;
|
|
213
|
+
case "break": return `${pad}break`;
|
|
214
|
+
case "continue": return `${pad}continue`;
|
|
215
|
+
case "if": {
|
|
216
|
+
let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
|
|
217
|
+
if (s.else.length > 0) {
|
|
218
|
+
if (s.else.length === 1 && s.else[0].kind === "if") {
|
|
219
|
+
const ei = s.else[0];
|
|
220
|
+
out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
|
|
221
|
+
if (ei.else.length > 0)
|
|
222
|
+
out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
|
|
223
|
+
}
|
|
224
|
+
else {
|
|
225
|
+
out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return out;
|
|
229
|
+
}
|
|
230
|
+
case "match": {
|
|
231
|
+
const scrut = typeof s.scrutinee === "string" ? s.scrutinee : emitExpr(s.scrutinee);
|
|
232
|
+
// Option match (.some/.none) → emit as if/let for WPGen.if compatibility
|
|
233
|
+
if (s.arms.length === 2) {
|
|
234
|
+
const someArm = s.arms.find(a => a.pattern.startsWith(".some "));
|
|
235
|
+
const noneArm = s.arms.find(a => a.pattern === ".none");
|
|
236
|
+
if (someArm && noneArm) {
|
|
237
|
+
const boundVar = someArm.pattern.slice(6); // strip ".some "
|
|
238
|
+
const hName = `h_${scrut.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
239
|
+
const lines = [
|
|
240
|
+
`${pad}if ${hName} : (${scrut}).isSome = true then`,
|
|
241
|
+
`${pad} let ${boundVar} := (${scrut}).get ${hName}`,
|
|
242
|
+
];
|
|
243
|
+
if (someArm.body.length === 0) {
|
|
244
|
+
lines.push(`${pad} pure ()`);
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
lines.push(emitStmts(someArm.body, indent + 1));
|
|
248
|
+
}
|
|
249
|
+
lines.push(`${pad}else`);
|
|
250
|
+
if (noneArm.body.length === 0) {
|
|
251
|
+
lines.push(`${pad} pure ()`);
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
lines.push(emitStmts(noneArm.body, indent + 1));
|
|
255
|
+
}
|
|
256
|
+
return lines.join("\n");
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
// General match
|
|
260
|
+
const lines = [`${pad}match ${scrut} with`];
|
|
261
|
+
for (const arm of s.arms) {
|
|
262
|
+
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
263
|
+
if (arm.body.length === 0) {
|
|
264
|
+
lines.push(`${pad} pure ()`);
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
lines.push(emitStmts(arm.body, indent + 1));
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return lines.join("\n");
|
|
271
|
+
}
|
|
272
|
+
case "while": {
|
|
273
|
+
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
274
|
+
for (const inv of s.invariants)
|
|
275
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
276
|
+
if (s.doneWith)
|
|
277
|
+
lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
|
|
278
|
+
if (s.decreasing)
|
|
279
|
+
lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
|
|
280
|
+
lines.push(`${pad}do`);
|
|
281
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
282
|
+
return lines.join("\n");
|
|
283
|
+
}
|
|
284
|
+
case "forin": {
|
|
285
|
+
const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
|
|
286
|
+
for (const inv of s.invariants)
|
|
287
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
288
|
+
lines.push(`${pad}do`);
|
|
289
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
290
|
+
return lines.join("\n");
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// ── Declaration emission ─────────────────────────────────────
|
|
295
|
+
function emitDecl(d) {
|
|
296
|
+
switch (d.kind) {
|
|
297
|
+
case "inductive": {
|
|
298
|
+
const lines = [`inductive ${d.name} where`];
|
|
299
|
+
for (const c of d.constructors) {
|
|
300
|
+
if (c.fields.length === 0) {
|
|
301
|
+
lines.push(` | ${c.name} : ${d.name}`);
|
|
302
|
+
}
|
|
303
|
+
else {
|
|
304
|
+
const params = c.fields.map(f => `(${escapeName(f.name)} : ${tyToLean(f.type)})`).join(" ");
|
|
305
|
+
lines.push(` | ${c.name} ${params} : ${d.name}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
if (d.deriving.length > 0)
|
|
309
|
+
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
310
|
+
return lines.join("\n");
|
|
311
|
+
}
|
|
312
|
+
case "structure": {
|
|
313
|
+
const lines = [`structure ${d.name} where`];
|
|
314
|
+
for (const f of d.fields)
|
|
315
|
+
lines.push(` ${escapeName(f.name)} : ${tyToLean(f.type)}`);
|
|
316
|
+
if (d.deriving.length > 0)
|
|
317
|
+
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
318
|
+
return lines.join("\n");
|
|
319
|
+
}
|
|
320
|
+
case "def": {
|
|
321
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
322
|
+
return `def ${d.name} ${params} : ${tyToLean(d.returnType)} :=\n${emitPureExpr(d.body, 1)}`;
|
|
323
|
+
}
|
|
324
|
+
case "method": {
|
|
325
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${tyToLean(p.type)})`).join(" ");
|
|
326
|
+
const lines = [`method ${d.name} ${params} return (res : ${tyToLean(d.returnType)})`];
|
|
327
|
+
for (const r of d.requires)
|
|
328
|
+
lines.push(` require ${emitExpr(r)}`);
|
|
329
|
+
for (const e of d.ensures)
|
|
330
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
331
|
+
lines.push(" do");
|
|
332
|
+
lines.push(emitStmts(d.body, 2));
|
|
333
|
+
return lines.join("\n");
|
|
334
|
+
}
|
|
335
|
+
case "namespace": {
|
|
336
|
+
const lines = [`namespace ${d.name}`];
|
|
337
|
+
for (const inner of d.decls)
|
|
338
|
+
lines.push("", emitDecl(inner));
|
|
339
|
+
lines.push("", `end ${d.name}`);
|
|
340
|
+
return lines.join("\n");
|
|
341
|
+
}
|
|
342
|
+
case "class":
|
|
343
|
+
throw new Error(`Lean class support not yet implemented: ${d.name}`);
|
|
344
|
+
case "const":
|
|
345
|
+
return `def ${escapeName(d.name)} : ${tyToLean(d.type)} := ${emitExpr(d.value)}`;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
/** Emit a pure expression with indented if/match blocks. */
|
|
349
|
+
function emitPureExpr(e, indent) {
|
|
350
|
+
const pad = " ".repeat(indent);
|
|
351
|
+
switch (e.kind) {
|
|
352
|
+
case "if":
|
|
353
|
+
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
354
|
+
case "match": {
|
|
355
|
+
const lines = [`${pad}match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with`];
|
|
356
|
+
for (const arm of e.arms) {
|
|
357
|
+
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
358
|
+
lines.push(emitPureExpr(arm.body, indent + 1));
|
|
359
|
+
}
|
|
360
|
+
return lines.join("\n");
|
|
361
|
+
}
|
|
362
|
+
case "let":
|
|
363
|
+
return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
|
|
364
|
+
default:
|
|
365
|
+
return `${pad}${emitExpr(e)}`;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
// ── File emission ────────────────────────────────────────────
|
|
369
|
+
export function emitLeanFile(file) {
|
|
370
|
+
_needsJSString = false;
|
|
371
|
+
// Emit declarations first so _needsJSString is set
|
|
372
|
+
const declLines = [];
|
|
373
|
+
for (const decl of file.decls) {
|
|
374
|
+
declLines.push("");
|
|
375
|
+
declLines.push(emitDecl(decl));
|
|
376
|
+
}
|
|
377
|
+
const lines = [];
|
|
378
|
+
if (file.comment) {
|
|
379
|
+
lines.push("/-");
|
|
380
|
+
lines.push(file.comment);
|
|
381
|
+
lines.push("-/");
|
|
382
|
+
}
|
|
383
|
+
for (const imp of file.imports)
|
|
384
|
+
lines.push(`import ${imp}`);
|
|
385
|
+
if (_needsJSString && !file.imports.includes("LemmaScript.JSString"))
|
|
386
|
+
lines.push("import LemmaScript.JSString");
|
|
387
|
+
if (file.options.length > 0)
|
|
388
|
+
lines.push("");
|
|
389
|
+
for (const opt of file.options)
|
|
390
|
+
lines.push(`set_option ${opt.key} ${opt.value}`);
|
|
391
|
+
lines.push(...declLines);
|
|
392
|
+
return lines.join("\n") + "\n";
|
|
393
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lsc — LemmaScript compiler CLI
|
|
4
|
+
*
|
|
5
|
+
* Pipeline: extract → resolve → transform → emit
|
|
6
|
+
*/
|
|
7
|
+
import { Project, ScriptTarget } from "ts-morph";
|
|
8
|
+
import { existsSync } from "fs";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import { extractModule } from "./extract.js";
|
|
11
|
+
import { resolveModule } from "./resolve.js";
|
|
12
|
+
import { transformModule, transformModuleDafny } from "./transform.js";
|
|
13
|
+
import { emitLeanFile } from "./lean-emit.js";
|
|
14
|
+
import { emitDafnyFile } from "./dafny-emit.js";
|
|
15
|
+
import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
|
|
16
|
+
import { leanGen, leanCheck } from "./lean-commands.js";
|
|
17
|
+
function main() {
|
|
18
|
+
const args = process.argv.slice(2);
|
|
19
|
+
const backendIdx = args.findIndex(a => a.startsWith("--backend="));
|
|
20
|
+
let backend = "lean";
|
|
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;
|
|
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
|
+
}
|
|
36
|
+
const [cmd, filePath] = args;
|
|
37
|
+
if (!cmd || !filePath) {
|
|
38
|
+
console.error("Usage: lsc <gen|check|regen|extract> [--backend=lean|dafny] <file.ts>");
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const absPath = path.resolve(filePath);
|
|
42
|
+
if (!existsSync(absPath)) {
|
|
43
|
+
console.error(`File not found: ${absPath}`);
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
const project = new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
|
|
47
|
+
const sourceFile = project.addSourceFileAtPath(absPath);
|
|
48
|
+
// Check //@ backend directive — skip if backend doesn't match
|
|
49
|
+
const backendDirective = sourceFile.getFullText().match(/\/\/@ backend (\w+)/);
|
|
50
|
+
if (backendDirective && backendDirective[1] !== backend) {
|
|
51
|
+
console.log(`Skipped: ${path.basename(filePath)} (//@ backend ${backendDirective[1]}, current: ${backend})`);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Extract: ts-morph → Raw IR
|
|
55
|
+
const raw = extractModule(sourceFile);
|
|
56
|
+
if (cmd === "extract") {
|
|
57
|
+
console.log(JSON.stringify(raw, null, 2));
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
// Resolve: Raw IR → Typed IR
|
|
61
|
+
const typed = resolveModule(raw);
|
|
62
|
+
const dir = path.dirname(absPath);
|
|
63
|
+
const base = path.basename(filePath, ".ts");
|
|
64
|
+
// ── Dafny backend ─────────────────────────────────────────
|
|
65
|
+
if (backend === "dafny") {
|
|
66
|
+
const { typesFile, defFile } = transformModuleDafny(typed);
|
|
67
|
+
const allDecls = [...(typesFile?.decls ?? []), ...defFile.decls];
|
|
68
|
+
const merged = { ...defFile, decls: allDecls };
|
|
69
|
+
const text = emitDafnyFile(merged, path.basename(filePath));
|
|
70
|
+
const genPath = path.join(dir, `${base}.dfy.gen`);
|
|
71
|
+
const dfyPath = path.join(dir, `${base}.dfy`);
|
|
72
|
+
const basePath = path.join(dir, `${base}.dfy.base`);
|
|
73
|
+
if (cmd === "gen") {
|
|
74
|
+
dafnyGen(genPath, dfyPath, text);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (cmd === "gen-check") {
|
|
78
|
+
dafnyGen(genPath, dfyPath, text);
|
|
79
|
+
if (!dafnyCheckDiff(genPath, dfyPath))
|
|
80
|
+
process.exit(1);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
if (cmd === "check") {
|
|
84
|
+
dafnyGen(genPath, dfyPath, text);
|
|
85
|
+
if (!dafnyCheckDiff(genPath, dfyPath))
|
|
86
|
+
process.exit(1);
|
|
87
|
+
if (!dafnyVerify(dfyPath, dir, timeLimit))
|
|
88
|
+
process.exit(1);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (cmd === "regen") {
|
|
92
|
+
dafnyRegen(genPath, dfyPath, basePath, text, dir);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.error(`Unknown command: ${cmd}`);
|
|
96
|
+
process.exit(1);
|
|
97
|
+
}
|
|
98
|
+
// ── Lean backend ──────────────────────────────────────────
|
|
99
|
+
const specPath = path.join(dir, `${base}.spec.lean`);
|
|
100
|
+
const specImport = existsSync(specPath) ? `«${base}.spec»` : undefined;
|
|
101
|
+
const { typesFile, defFile } = transformModule(typed, specImport);
|
|
102
|
+
const typesPath = typesFile ? path.join(dir, `${base}.types.lean`) : null;
|
|
103
|
+
const typesText = typesFile ? emitLeanFile(typesFile) : null;
|
|
104
|
+
const defPath = path.join(dir, `${base}.def.lean`);
|
|
105
|
+
const defText = emitLeanFile(defFile);
|
|
106
|
+
if (cmd === "gen") {
|
|
107
|
+
leanGen(typesPath, defPath, typesText, defText);
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
if (cmd === "check") {
|
|
111
|
+
leanGen(typesPath, defPath, typesText, defText);
|
|
112
|
+
if (!leanCheck(dir, base))
|
|
113
|
+
process.exit(1);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
console.error(`Unknown command: ${cmd}`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
main();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Raw IR — structured AST for expressions and statements.
|
|
3
|
+
*
|
|
4
|
+
* Produced by the extract phase (ts-morph → RawExpr for body expressions)
|
|
5
|
+
* and the specparser (annotation strings → RawExpr for spec expressions).
|
|
6
|
+
*
|
|
7
|
+
* Layer 1: structured (no strings for expressions)
|
|
8
|
+
* Layer 2 (planned): add Ty to each node
|
|
9
|
+
*/
|
|
10
|
+
export {};
|