lemmascript 0.3.3 → 0.5.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 +20 -13
- package/package.json +4 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +302 -17
- package/tools/dist/extract.js +1087 -181
- package/tools/dist/info-command.js +38 -0
- package/tools/dist/lean-emit.js +81 -5
- package/tools/dist/lsc.js +29 -9
- package/tools/dist/narrow.js +932 -0
- package/tools/dist/peephole.js +451 -0
- package/tools/dist/resolve.js +680 -258
- package/tools/dist/specparser.js +18 -2
- package/tools/dist/transform.js +597 -441
- package/tools/dist/types.js +128 -69
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `lsc info` — emit a JSON summary of verified functions in a TS file.
|
|
3
|
+
*
|
|
4
|
+
* Pipeline: extract only (no resolve/transform/emit). Walks Raw IR for
|
|
5
|
+
* top-level functions and class methods, preserving the original `//@ `
|
|
6
|
+
* source text for clauses (no specparser round-trip).
|
|
7
|
+
*
|
|
8
|
+
* Output: `foo.ts.json` next to `foo.ts`, with shape:
|
|
9
|
+
* { method: { sig, requires, ensures, decreases }, ... }
|
|
10
|
+
* Class methods key as `ClassName.method`.
|
|
11
|
+
*/
|
|
12
|
+
import { writeFileSync } from "fs";
|
|
13
|
+
import { parseTsType, tyToCanonical } from "./types.js";
|
|
14
|
+
function renderSig(fn) {
|
|
15
|
+
const params = fn.params.map(p => `${p.name}: ${tyToCanonical(parseTsType(p.tsType))}`).join(", ");
|
|
16
|
+
return `(${params}): ${tyToCanonical(parseTsType(fn.returnType))}`;
|
|
17
|
+
}
|
|
18
|
+
function fnToInfo(fn) {
|
|
19
|
+
return {
|
|
20
|
+
sig: renderSig(fn),
|
|
21
|
+
requires: fn.requires,
|
|
22
|
+
ensures: fn.ensures,
|
|
23
|
+
decreases: fn.decreases,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
export function runInfo(raw, outPath) {
|
|
27
|
+
const out = {};
|
|
28
|
+
for (const fn of raw.functions) {
|
|
29
|
+
out[fn.name] = fnToInfo(fn);
|
|
30
|
+
}
|
|
31
|
+
for (const cls of raw.classes) {
|
|
32
|
+
for (const m of cls.methods) {
|
|
33
|
+
out[`${cls.name}.${m.name}`] = fnToInfo(m);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
writeFileSync(outPath, JSON.stringify(out, null, 2) + "\n");
|
|
37
|
+
console.log(`Wrote ${outPath}`);
|
|
38
|
+
}
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -31,6 +31,15 @@ function tyToLean(ty) {
|
|
|
31
31
|
return inner.includes(" ") ? `Option (${inner})` : `Option ${inner}`;
|
|
32
32
|
}
|
|
33
33
|
case "user": return ty.name;
|
|
34
|
+
case "fn": {
|
|
35
|
+
const params = ty.params.map(p => {
|
|
36
|
+
const s = tyToLean(p);
|
|
37
|
+
return s.includes(" ") ? `(${s})` : s;
|
|
38
|
+
});
|
|
39
|
+
const ret = tyToLean(ty.result);
|
|
40
|
+
const retStr = ret.includes(" ") ? `(${ret})` : ret;
|
|
41
|
+
return [...params, retStr].join(" → ");
|
|
42
|
+
}
|
|
34
43
|
case "unknown": return "_";
|
|
35
44
|
}
|
|
36
45
|
}
|
|
@@ -45,6 +54,10 @@ const LEAN_KEYWORDS = new Set([
|
|
|
45
54
|
"at", "from", "to", "deriving", "extends", "true", "false",
|
|
46
55
|
]);
|
|
47
56
|
function escapeName(name) {
|
|
57
|
+
// \result is carried through the IR as the var name "\\result"; render it
|
|
58
|
+
// as Lean's canonical return-value identifier (matches `return (res : T)`).
|
|
59
|
+
if (name === "\\result")
|
|
60
|
+
return "res";
|
|
48
61
|
return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
|
|
49
62
|
}
|
|
50
63
|
// ── Operator precedence (for parenthesization) ──────────────
|
|
@@ -75,6 +88,8 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
75
88
|
return `${obj}.set! ${args[0]} ${args[1]}`;
|
|
76
89
|
if (method === "push")
|
|
77
90
|
return `Array.push ${obj} ${args[0]}`;
|
|
91
|
+
if (method === "concat")
|
|
92
|
+
return `Array.push ${obj} ${args[0]}`;
|
|
78
93
|
}
|
|
79
94
|
// String methods
|
|
80
95
|
if (tyKind === "string") {
|
|
@@ -107,13 +122,30 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
107
122
|
throw new Error(`Unsupported Lean method call: .${method}() on ${tyKind}`);
|
|
108
123
|
}
|
|
109
124
|
// ── Expression emission ─────────────────────────────────────
|
|
125
|
+
// Lean's `∀`/`∃` body extends as far as possible. So `(∃ x, P) <op> Q`
|
|
126
|
+
// (or `∃ x, P → Q`) would parse with the operator absorbed into the body.
|
|
127
|
+
// Wrap a quantifier in parens to terminate its body before the operator.
|
|
128
|
+
function wrapQuantifier(sub, parentPrec) {
|
|
129
|
+
const inner = emitExpr(sub, parentPrec);
|
|
130
|
+
return (sub.kind === "forall" || sub.kind === "exists") ? `(${inner})` : inner;
|
|
131
|
+
}
|
|
110
132
|
function emitExpr(e, parentPrec) {
|
|
111
133
|
switch (e.kind) {
|
|
112
134
|
case "var": return escapeName(e.name);
|
|
113
135
|
case "num": return `${e.value}`;
|
|
114
136
|
case "bool": return e.value ? "true" : "false";
|
|
115
137
|
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
116
|
-
case "constructor":
|
|
138
|
+
case "constructor": {
|
|
139
|
+
// With type: emit `Type.name` (unambiguous; needed in expression positions
|
|
140
|
+
// like `match ... | .none => Type.some x` where elaboration can't infer).
|
|
141
|
+
// Without type: emit `.name` (dotted form; works in pattern positions
|
|
142
|
+
// and where the expected type is clear from context).
|
|
143
|
+
const head = e.type ? `${e.type}.${e.name}` : `.${e.name}`;
|
|
144
|
+
if (!e.args || e.args.length === 0)
|
|
145
|
+
return head;
|
|
146
|
+
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));
|
|
147
|
+
return `${head} ${args.join(" ")}`;
|
|
148
|
+
}
|
|
117
149
|
case "arrayLiteral":
|
|
118
150
|
if (e.elems.length === 0)
|
|
119
151
|
return `#[]`;
|
|
@@ -143,12 +175,19 @@ function emitExpr(e, parentPrec) {
|
|
|
143
175
|
return `-${e.expr.value}`;
|
|
144
176
|
return `(-${emitExpr(e.expr)})`;
|
|
145
177
|
case "binop": {
|
|
178
|
+
// `k in m` (map/set membership) → `m.contains k` in Lean. Dafny has
|
|
179
|
+
// native `in`; Lean uses the method form for HashMap/HashSet.
|
|
180
|
+
if (e.op === "in") {
|
|
181
|
+
const recv = emitExpr(e.right);
|
|
182
|
+
const wrap = e.right.kind === "binop" || e.right.kind === "app" || e.right.kind === "methodCall";
|
|
183
|
+
return `${wrap ? `(${recv})` : recv}.contains ${emitExpr(e.left)}`;
|
|
184
|
+
}
|
|
146
185
|
const op = e.op === "arrayConcat" ? "++" : e.op;
|
|
147
|
-
const s = `${
|
|
186
|
+
const s = `${wrapQuantifier(e.left, prec(e.op))} ${op} ${emitExpr(e.right, prec(e.op))}`;
|
|
148
187
|
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
149
188
|
}
|
|
150
189
|
case "implies": {
|
|
151
|
-
const parts = [...e.premises.map(p =>
|
|
190
|
+
const parts = [...e.premises.map(p => wrapQuantifier(p)), emitExpr(e.conclusion)];
|
|
152
191
|
const s = parts.join(" → ");
|
|
153
192
|
return parentPrec !== undefined ? `(${s})` : s;
|
|
154
193
|
}
|
|
@@ -182,8 +221,12 @@ function emitExpr(e, parentPrec) {
|
|
|
182
221
|
case "if":
|
|
183
222
|
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
184
223
|
case "match": {
|
|
224
|
+
// Always parenthesize inline matches — Lean parses alternatives greedily,
|
|
225
|
+
// so any token after an arm body (`→`, another match's `|`, etc.) would
|
|
226
|
+
// bleed into the last `.none` case without explicit bracketing.
|
|
185
227
|
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
186
|
-
|
|
228
|
+
const scrut = typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee);
|
|
229
|
+
return `(match ${scrut} with ${arms.join(" ")})`;
|
|
187
230
|
}
|
|
188
231
|
case "forall": return `∀ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
|
|
189
232
|
case "exists": return `∃ ${e.var} : ${tyToLean(e.type)}, ${emitExpr(e.body)}`;
|
|
@@ -191,6 +234,24 @@ function emitExpr(e, parentPrec) {
|
|
|
191
234
|
default: throw new Error(`Unsupported Lean expression: ${e.kind}`);
|
|
192
235
|
}
|
|
193
236
|
}
|
|
237
|
+
/** True if the IR expression emits to a Prop-valued Lean term. `transformExpr`
|
|
238
|
+
* routes TS comparisons/logicals through `OP_MAP` (`===`→`=`, `&&`→`∧`, `!`→`¬`)
|
|
239
|
+
* so these top-level ops land in Prop. `in` stays Bool (emits `.contains`), and
|
|
240
|
+
* bare method calls / vars / field accesses remain at their declared type. */
|
|
241
|
+
function isPropValued(e) {
|
|
242
|
+
switch (e.kind) {
|
|
243
|
+
case "binop":
|
|
244
|
+
return ["=", "≠", "≥", "≤", ">", "<", "∧", "∨"].includes(e.op);
|
|
245
|
+
case "unop":
|
|
246
|
+
return e.op === "¬";
|
|
247
|
+
case "implies":
|
|
248
|
+
case "forall":
|
|
249
|
+
case "exists":
|
|
250
|
+
return true;
|
|
251
|
+
default:
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
194
255
|
// ── Statement emission ──────────────────────────────────────
|
|
195
256
|
function emitStmts(stmts, indent) {
|
|
196
257
|
const pad = " ".repeat(indent);
|
|
@@ -207,7 +268,18 @@ function emitStmt(s, indent) {
|
|
|
207
268
|
case "ghostLet":
|
|
208
269
|
return `${pad}let mut ${escapeName(s.name)} : ${tyToLean(s.type)} := ${emitExpr(s.value)}`;
|
|
209
270
|
case "ghostAssign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
210
|
-
case "assert":
|
|
271
|
+
case "assert": {
|
|
272
|
+
if (s.assumed)
|
|
273
|
+
throw new Error("//@ assume: not supported in Lean backend.");
|
|
274
|
+
// WPGen.assert needs a Prop; bare Bool expressions (`k in m` → `.contains`,
|
|
275
|
+
// method calls, vars) lack a matching WPGen instance and silently fall back
|
|
276
|
+
// to WPGen.default, which drops the assertion. Coerce to Prop via `= true`.
|
|
277
|
+
// Top-level Prop constructs (`=`, `<`, `∧`, `¬`, `∀`, `∃`, `→`) already
|
|
278
|
+
// land in Prop — Lean auto-coerces inner Bools there.
|
|
279
|
+
const inner = emitExpr(s.expr);
|
|
280
|
+
const wrapped = isPropValued(s.expr) ? inner : `(${inner}) = true`;
|
|
281
|
+
return `${pad}assertGadget (${wrapped})`;
|
|
282
|
+
}
|
|
211
283
|
case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
|
|
212
284
|
case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
|
|
213
285
|
case "return": return `${pad}return ${emitExpr(s.value)}`;
|
|
@@ -349,6 +421,10 @@ function emitDecl(d) {
|
|
|
349
421
|
throw new Error(`Lean class support not yet implemented: ${d.name}`);
|
|
350
422
|
case "const":
|
|
351
423
|
return `def ${escapeName(d.name)} : ${tyToLean(d.type)} := ${emitExpr(d.value)}`;
|
|
424
|
+
case "extern":
|
|
425
|
+
// Lean: emit an opaque function declaration. The user is expected to
|
|
426
|
+
// provide an axiomatic body or a stub in the companion spec file.
|
|
427
|
+
throw new Error(`Lean extern support not yet implemented: ${d.name}`);
|
|
352
428
|
}
|
|
353
429
|
}
|
|
354
430
|
/** Emit a pure expression with indented if/match blocks. */
|
package/tools/dist/lsc.js
CHANGED
|
@@ -2,18 +2,21 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* lsc — LemmaScript compiler CLI
|
|
4
4
|
*
|
|
5
|
-
* Pipeline: extract → resolve → transform → emit
|
|
5
|
+
* Pipeline: extract → resolve → narrow → transform → peephole → emit
|
|
6
6
|
*/
|
|
7
7
|
import { Project, ScriptTarget } from "ts-morph";
|
|
8
8
|
import { existsSync } from "fs";
|
|
9
9
|
import path from "path";
|
|
10
10
|
import { extractModule } from "./extract.js";
|
|
11
11
|
import { resolveModule } from "./resolve.js";
|
|
12
|
+
import { narrowModule } from "./narrow.js";
|
|
12
13
|
import { transformModuleLean, transformModuleDafny } from "./transform.js";
|
|
14
|
+
import { peepholeModule } from "./peephole.js";
|
|
13
15
|
import { emitLeanFile } from "./lean-emit.js";
|
|
14
16
|
import { emitDafnyFile } from "./dafny-emit.js";
|
|
15
17
|
import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
|
|
16
18
|
import { leanGen, leanCheck } from "./lean-commands.js";
|
|
19
|
+
import { runInfo } from "./info-command.js";
|
|
17
20
|
function main() {
|
|
18
21
|
const args = process.argv.slice(2);
|
|
19
22
|
const backendIdx = args.findIndex(a => a.startsWith("--backend="));
|
|
@@ -41,7 +44,7 @@ function main() {
|
|
|
41
44
|
}
|
|
42
45
|
const [cmd, filePath] = args;
|
|
43
46
|
if (!cmd || !filePath) {
|
|
44
|
-
console.error("Usage: lsc <gen|check|regen|extract> [--backend=lean|dafny] <file.ts>");
|
|
47
|
+
console.error("Usage: lsc <gen|check|regen|extract|info> [--backend=lean|dafny] <file.ts>");
|
|
45
48
|
process.exit(1);
|
|
46
49
|
}
|
|
47
50
|
const absPath = path.resolve(filePath);
|
|
@@ -68,28 +71,42 @@ function main() {
|
|
|
68
71
|
: new Project({ compilerOptions: { strict: true, target: ScriptTarget.ESNext, lib: ["lib.esnext.d.ts"] } });
|
|
69
72
|
const sourceFile = project.addSourceFileAtPath(absPath);
|
|
70
73
|
project.resolveSourceFileDependencies();
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
const fullText = sourceFile.getFullText();
|
|
75
|
+
// Check //@ backend directive — skip if backend doesn't match.
|
|
76
|
+
// `extract` and `info` are backend-neutral and always run.
|
|
77
|
+
const backendDirective = fullText.match(/\/\/@ backend (\w+)/);
|
|
78
|
+
if (cmd !== "extract" && cmd !== "info" && backendDirective && backendDirective[1] !== backend) {
|
|
74
79
|
console.log(`Skipped: ${path.basename(filePath)} (//@ backend ${backendDirective[1]}, current: ${backend})`);
|
|
75
80
|
return;
|
|
76
81
|
}
|
|
82
|
+
// File-level directives consumed by the Dafny emitter.
|
|
83
|
+
const safeSlice = /\/\/@ safe-slice\b/.test(fullText);
|
|
77
84
|
// Extract: ts-morph → Raw IR
|
|
78
85
|
const raw = extractModule(sourceFile);
|
|
79
86
|
if (cmd === "extract") {
|
|
80
87
|
console.log(JSON.stringify(raw, null, 2));
|
|
81
88
|
return;
|
|
82
89
|
}
|
|
90
|
+
if (cmd === "info") {
|
|
91
|
+
const outPath = path.join(path.dirname(absPath), `${path.basename(filePath, ".ts")}.ts.json`);
|
|
92
|
+
runInfo(raw, outPath);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
83
95
|
// Resolve: Raw IR → Typed IR
|
|
84
|
-
const
|
|
96
|
+
const resolved = resolveModule(raw);
|
|
97
|
+
// Narrow: Typed IR → Typed IR (rewrites optional-narrowing patterns to someMatch)
|
|
98
|
+
const typed = narrowModule(resolved);
|
|
85
99
|
const dir = path.dirname(absPath);
|
|
86
100
|
const base = path.basename(filePath, ".ts");
|
|
87
101
|
// ── Dafny backend ─────────────────────────────────────────
|
|
88
102
|
if (backend === "dafny") {
|
|
89
|
-
|
|
103
|
+
let { typesFile, defFile } = transformModuleDafny(typed);
|
|
104
|
+
if (typesFile)
|
|
105
|
+
typesFile = peepholeModule(typesFile, "dafny");
|
|
106
|
+
defFile = peepholeModule(defFile, "dafny");
|
|
90
107
|
const allDecls = [...(typesFile?.decls ?? []), ...defFile.decls];
|
|
91
108
|
const merged = { ...defFile, decls: allDecls };
|
|
92
|
-
const text = emitDafnyFile(merged, path.basename(filePath));
|
|
109
|
+
const text = emitDafnyFile(merged, path.basename(filePath), { safeSlice });
|
|
93
110
|
const genPath = path.join(dir, `${base}.dfy.gen`);
|
|
94
111
|
const dfyPath = path.join(dir, `${base}.dfy`);
|
|
95
112
|
const basePath = path.join(dir, `${base}.dfy.base`);
|
|
@@ -121,7 +138,10 @@ function main() {
|
|
|
121
138
|
// ── Lean backend ──────────────────────────────────────────
|
|
122
139
|
const specPath = path.join(dir, `${base}.spec.lean`);
|
|
123
140
|
const specImport = existsSync(specPath) ? `«${base}.spec»` : undefined;
|
|
124
|
-
|
|
141
|
+
let { typesFile, defFile } = transformModuleLean(typed, specImport);
|
|
142
|
+
if (typesFile)
|
|
143
|
+
typesFile = peepholeModule(typesFile, "lean");
|
|
144
|
+
defFile = peepholeModule(defFile, "lean");
|
|
125
145
|
const typesPath = typesFile ? path.join(dir, `${base}.types.lean`) : null;
|
|
126
146
|
const typesText = typesFile ? emitLeanFile(typesFile) : null;
|
|
127
147
|
const defPath = path.join(dir, `${base}.def.lean`);
|