lemmascript 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +101 -3
- package/package.json +30 -20
- package/tools/dist/dafny-commands.js +104 -0
- package/tools/dist/dafny-emit.js +449 -0
- package/tools/dist/emit.js +253 -0
- package/tools/dist/extract.js +435 -0
- package/tools/dist/ir.js +7 -0
- package/tools/dist/lsc.js +118 -0
- package/tools/dist/rawir.js +10 -0
- package/tools/dist/resolve.js +451 -0
- package/tools/dist/specparser.js +251 -0
- package/tools/dist/transform.js +745 -0
- package/tools/dist/typedir.js +7 -0
- package/tools/dist/types.js +38 -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,118 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* lsc — LemmaScript compiler CLI
|
|
4
|
+
*
|
|
5
|
+
* Pipeline: extract → resolve → transform → emit
|
|
6
|
+
*/
|
|
7
|
+
import { Project } from "ts-morph";
|
|
8
|
+
import { existsSync, writeFileSync } from "fs";
|
|
9
|
+
import { execSync } from "child_process";
|
|
10
|
+
import path from "path";
|
|
11
|
+
import { extractModule } from "./extract.js";
|
|
12
|
+
import { resolveModule } from "./resolve.js";
|
|
13
|
+
import { transformModule } from "./transform.js";
|
|
14
|
+
import { emitFile } from "./emit.js";
|
|
15
|
+
import { transformModuleDafny } from "./transform.js";
|
|
16
|
+
import { emitDafnyFile } from "./dafny-emit.js";
|
|
17
|
+
import { dafnyGen, dafnyCheckDiff, dafnyVerify, dafnyRegen } from "./dafny-commands.js";
|
|
18
|
+
function main() {
|
|
19
|
+
const args = process.argv.slice(2);
|
|
20
|
+
const backendIdx = args.indexOf("--backend=dafny");
|
|
21
|
+
const backend = backendIdx >= 0 ? "dafny" : "lean";
|
|
22
|
+
if (backendIdx >= 0)
|
|
23
|
+
args.splice(backendIdx, 1);
|
|
24
|
+
const [cmd, filePath] = args;
|
|
25
|
+
if (!cmd || !filePath) {
|
|
26
|
+
console.error("Usage: lsc <gen|check|regen|extract> [--backend=dafny] <file.ts>");
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const absPath = path.resolve(filePath);
|
|
30
|
+
if (!existsSync(absPath)) {
|
|
31
|
+
console.error(`File not found: ${absPath}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const project = new Project({ compilerOptions: { strict: true } });
|
|
35
|
+
const sourceFile = project.addSourceFileAtPath(absPath);
|
|
36
|
+
// Extract: ts-morph → Raw IR
|
|
37
|
+
const raw = extractModule(sourceFile);
|
|
38
|
+
if (cmd === "extract") {
|
|
39
|
+
console.log(JSON.stringify(raw, null, 2));
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
// Resolve: Raw IR → Typed IR
|
|
43
|
+
const typed = resolveModule(raw);
|
|
44
|
+
const dir = path.dirname(absPath);
|
|
45
|
+
const base = path.basename(filePath, ".ts");
|
|
46
|
+
// ── Dafny backend ─────────────────────────────────────────
|
|
47
|
+
if (backend === "dafny") {
|
|
48
|
+
const { typesFile, defFile } = transformModuleDafny(typed);
|
|
49
|
+
// Emit types + def into a single Dafny file
|
|
50
|
+
const allDecls = [...(typesFile?.decls ?? []), ...defFile.decls];
|
|
51
|
+
const merged = { ...defFile, decls: allDecls };
|
|
52
|
+
const text = emitDafnyFile(merged, path.basename(filePath));
|
|
53
|
+
const genPath = path.join(dir, `${base}.dfy.gen`);
|
|
54
|
+
const dfyPath = path.join(dir, `${base}.dfy`);
|
|
55
|
+
const patchPath = path.join(dir, `${base}.dfy.patch`);
|
|
56
|
+
if (cmd === "gen") {
|
|
57
|
+
dafnyGen(genPath, dfyPath, text);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (cmd === "check") {
|
|
61
|
+
dafnyGen(genPath, dfyPath, text);
|
|
62
|
+
if (!dafnyCheckDiff(genPath, dfyPath))
|
|
63
|
+
process.exit(1);
|
|
64
|
+
if (!dafnyVerify(dfyPath, dir))
|
|
65
|
+
process.exit(1);
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (cmd === "regen") {
|
|
69
|
+
dafnyRegen(genPath, dfyPath, patchPath, text, dir);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
console.error(`Unknown command: ${cmd}`);
|
|
73
|
+
process.exit(1);
|
|
74
|
+
}
|
|
75
|
+
// ── Lean backend ──────────────────────────────────────────
|
|
76
|
+
const specPath = path.join(dir, `${base}.spec.lean`);
|
|
77
|
+
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
|
+
}
|
|
86
|
+
const defPath = path.join(dir, `${base}.def.lean`);
|
|
87
|
+
if (cmd === "gen") {
|
|
88
|
+
writeFileSync(defPath, emitFile(defFile));
|
|
89
|
+
console.log(`Generated: ${defPath}`);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
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}`);
|
|
104
|
+
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
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
console.error(`Unknown command: ${cmd}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
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 {};
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve — Raw IR → Typed IR.
|
|
3
|
+
*
|
|
4
|
+
* Uses linked environments (Scheme-style) for lexical scoping.
|
|
5
|
+
* No mutation — each let extends the chain, lookup walks it.
|
|
6
|
+
*/
|
|
7
|
+
import { parseTsType } from "./types.js";
|
|
8
|
+
import { parseExpr } from "./specparser.js";
|
|
9
|
+
function lookup(env, name) {
|
|
10
|
+
if (!env)
|
|
11
|
+
return undefined;
|
|
12
|
+
return env.name === name ? env.ty : lookup(env.parent, name);
|
|
13
|
+
}
|
|
14
|
+
function extend(env, name, ty) {
|
|
15
|
+
return { name, ty, parent: env };
|
|
16
|
+
}
|
|
17
|
+
function withEnv(ctx, env) {
|
|
18
|
+
return { ...ctx, env };
|
|
19
|
+
}
|
|
20
|
+
// ── TS type → Ty ─────────────────────────────────────────────
|
|
21
|
+
function resolveTsType(tsType, overrides, varName) {
|
|
22
|
+
if (varName) {
|
|
23
|
+
const o = overrides.get(varName);
|
|
24
|
+
if (o)
|
|
25
|
+
return parseTsType(o);
|
|
26
|
+
}
|
|
27
|
+
return parseTsType(tsType);
|
|
28
|
+
}
|
|
29
|
+
/** If expr is a string literal and targetTy is a user type, coerce the literal's type. */
|
|
30
|
+
function coerceStr(expr, targetTy) {
|
|
31
|
+
if (expr.kind === "str" && targetTy.kind === "user")
|
|
32
|
+
return { ...expr, ty: targetTy };
|
|
33
|
+
return expr;
|
|
34
|
+
}
|
|
35
|
+
// ── Helpers ──────────────────────────────────────────────────
|
|
36
|
+
function findDecl(ctx, name) {
|
|
37
|
+
return ctx.typeDecls.find(d => d.name === name);
|
|
38
|
+
}
|
|
39
|
+
function getDiscriminant(ctx, typeName) {
|
|
40
|
+
return findDecl(ctx, typeName)?.discriminant;
|
|
41
|
+
}
|
|
42
|
+
function classifyCall(fn, ctx) {
|
|
43
|
+
if (fn.kind === "field" && fn.obj.kind === "var" && fn.obj.name === "Math")
|
|
44
|
+
return "pure";
|
|
45
|
+
if (fn.kind === "var" && (ctx.inSpec || ctx.inLambda) && ctx.pureFns.has(fn.name))
|
|
46
|
+
return "spec-pure";
|
|
47
|
+
if (fn.kind === "var" && ctx.inSpec) {
|
|
48
|
+
// Not a known pure function — could be external (Lean-defined spec helper).
|
|
49
|
+
// Pass through as "pure" and let Lean catch any errors.
|
|
50
|
+
return "pure";
|
|
51
|
+
}
|
|
52
|
+
if (fn.kind === "var")
|
|
53
|
+
return "method";
|
|
54
|
+
return "unknown";
|
|
55
|
+
}
|
|
56
|
+
// ── Resolve expressions ──────────────────────────────────────
|
|
57
|
+
function resolveExpr(e, ctx) {
|
|
58
|
+
switch (e.kind) {
|
|
59
|
+
case "var":
|
|
60
|
+
return { kind: "var", name: e.name, ty: lookup(ctx.env, e.name) ?? { kind: "unknown" } };
|
|
61
|
+
case "num":
|
|
62
|
+
return { kind: "num", value: e.value, ty: e.value >= 0 ? { kind: "nat" } : { kind: "int" } };
|
|
63
|
+
case "str":
|
|
64
|
+
return { kind: "str", value: e.value, ty: { kind: "string" } };
|
|
65
|
+
case "bool":
|
|
66
|
+
return { kind: "bool", value: e.value, ty: { kind: "bool" } };
|
|
67
|
+
case "binop": {
|
|
68
|
+
let left = resolveExpr(e.left, ctx);
|
|
69
|
+
let right = resolveExpr(e.right, ctx);
|
|
70
|
+
if (e.op === "===" || e.op === "!==") {
|
|
71
|
+
left = coerceStr(left, right.ty);
|
|
72
|
+
right = coerceStr(right, left.ty);
|
|
73
|
+
}
|
|
74
|
+
let ty = { kind: "unknown" };
|
|
75
|
+
if (["===", "!==", ">=", "<=", ">", "<", "&&", "||"].includes(e.op))
|
|
76
|
+
ty = { kind: "bool" };
|
|
77
|
+
else if (["+", "-", "*", "/", "%"].includes(e.op))
|
|
78
|
+
ty = left.ty;
|
|
79
|
+
return { kind: "binop", op: e.op, left, right, ty };
|
|
80
|
+
}
|
|
81
|
+
case "unop": {
|
|
82
|
+
const expr = resolveExpr(e.expr, ctx);
|
|
83
|
+
return { kind: "unop", op: e.op, expr, ty: e.op === "!" ? { kind: "bool" } : expr.ty };
|
|
84
|
+
}
|
|
85
|
+
case "call":
|
|
86
|
+
return { kind: "call", fn: resolveExpr(e.fn, ctx), args: e.args.map(a => resolveExpr(a, ctx)), ty: { kind: "unknown" }, callKind: classifyCall(e.fn, ctx) };
|
|
87
|
+
case "index": {
|
|
88
|
+
const obj = resolveExpr(e.obj, ctx);
|
|
89
|
+
const idx = resolveExpr(e.idx, ctx);
|
|
90
|
+
return { kind: "index", obj, idx, ty: obj.ty.kind === "array" ? obj.ty.elem : { kind: "unknown" } };
|
|
91
|
+
}
|
|
92
|
+
case "field": {
|
|
93
|
+
const obj = resolveExpr(e.obj, ctx);
|
|
94
|
+
let isDiscriminant = false;
|
|
95
|
+
let ty = { kind: "unknown" };
|
|
96
|
+
if (e.field === "length" && (obj.ty.kind === "array" || obj.ty.kind === "string")) {
|
|
97
|
+
ty = { kind: "nat" };
|
|
98
|
+
}
|
|
99
|
+
else if (obj.ty.kind === "user") {
|
|
100
|
+
if (getDiscriminant(ctx, obj.ty.name) === e.field)
|
|
101
|
+
isDiscriminant = true;
|
|
102
|
+
const decl = findDecl(ctx, obj.ty.name);
|
|
103
|
+
if (decl?.kind === "record") {
|
|
104
|
+
const f = decl.fields?.find(f => f.name === e.field);
|
|
105
|
+
if (f)
|
|
106
|
+
ty = resolveTsType(f.tsType, ctx.overrides);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return { kind: "field", obj, field: e.field, ty, isDiscriminant };
|
|
110
|
+
}
|
|
111
|
+
case "record": {
|
|
112
|
+
const spread = e.spread ? resolveExpr(e.spread, ctx) : null;
|
|
113
|
+
const ty = spread ? spread.ty : { kind: "unknown" };
|
|
114
|
+
// Infer record type: from spread, or from return type context
|
|
115
|
+
const recordTy = ty.kind === "user" ? ty : ctx.returnTy.kind === "user" ? ctx.returnTy : null;
|
|
116
|
+
const decl = recordTy ? ctx.typeDecls.find(d => d.name === recordTy.name && d.kind === "record") : undefined;
|
|
117
|
+
const fields = e.fields.map(f => {
|
|
118
|
+
let value = resolveExpr(f.value, ctx);
|
|
119
|
+
const fieldDecl = decl?.fields?.find(df => df.name === f.name);
|
|
120
|
+
if (fieldDecl)
|
|
121
|
+
value = coerceStr(value, parseTsType(fieldDecl.tsType));
|
|
122
|
+
return { name: f.name, value };
|
|
123
|
+
});
|
|
124
|
+
return { kind: "record", spread, fields, ty: recordTy ?? ty };
|
|
125
|
+
}
|
|
126
|
+
case "result":
|
|
127
|
+
if (!ctx.allowResult)
|
|
128
|
+
throw new Error("\\result is only valid in ensures");
|
|
129
|
+
return { kind: "result", ty: ctx.returnTy };
|
|
130
|
+
case "forall": {
|
|
131
|
+
const varTy = e.varType === "nat" ? { kind: "nat" } : { kind: "int" };
|
|
132
|
+
return { kind: "forall", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
133
|
+
}
|
|
134
|
+
case "exists": {
|
|
135
|
+
const varTy = e.varType === "nat" ? { kind: "nat" } : { kind: "int" };
|
|
136
|
+
return { kind: "exists", var: e.var, varTy, body: resolveExpr(e.body, withEnv(ctx, extend(ctx.env, e.var, varTy))), ty: { kind: "bool" } };
|
|
137
|
+
}
|
|
138
|
+
case "arrayLiteral": {
|
|
139
|
+
const elems = e.elems.map(el => resolveExpr(el, ctx));
|
|
140
|
+
const elemTy = elems.length > 0 ? elems[0].ty : { kind: "unknown" };
|
|
141
|
+
return { kind: "arrayLiteral", elems, ty: { kind: "array", elem: elemTy } };
|
|
142
|
+
}
|
|
143
|
+
case "lambda": {
|
|
144
|
+
// Resolve lambda params — types from explicit annotation or unknown
|
|
145
|
+
const params = e.params.map(p => ({
|
|
146
|
+
name: p.name,
|
|
147
|
+
ty: p.tsType ? parseTsType(p.tsType) : { kind: "unknown" },
|
|
148
|
+
}));
|
|
149
|
+
// Extend env with lambda params
|
|
150
|
+
let lambdaEnv = ctx.env;
|
|
151
|
+
for (const p of params)
|
|
152
|
+
lambdaEnv = extend(lambdaEnv, p.name, p.ty);
|
|
153
|
+
const lambdaCtx = { ...withEnv(ctx, lambdaEnv), inLambda: true };
|
|
154
|
+
// Body: expression (wrap in return stmt) or statement block
|
|
155
|
+
const body = Array.isArray(e.body)
|
|
156
|
+
? resolveBlock(e.body, lambdaCtx)
|
|
157
|
+
: [{ kind: "return", value: resolveExpr(e.body, lambdaCtx) }];
|
|
158
|
+
return { kind: "lambda", params, body, ty: { kind: "unknown" } };
|
|
159
|
+
}
|
|
160
|
+
case "conditional": {
|
|
161
|
+
const cond = resolveExpr(e.cond, ctx);
|
|
162
|
+
let then_ = resolveExpr(e.then, ctx);
|
|
163
|
+
let else_ = resolveExpr(e.else, ctx);
|
|
164
|
+
then_ = coerceStr(then_, else_.ty);
|
|
165
|
+
else_ = coerceStr(else_, then_.ty);
|
|
166
|
+
const ty = then_.ty.kind !== "unknown" ? then_.ty : else_.ty;
|
|
167
|
+
return { kind: "conditional", cond, then: then_, else: else_, ty };
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
// ── Resolve specs ────────────────────────────────────────────
|
|
172
|
+
function resolveSpec(spec, ctx) {
|
|
173
|
+
return resolveExpr(parseExpr(spec), ctx);
|
|
174
|
+
}
|
|
175
|
+
function resolveSpecs(specs, ctx) {
|
|
176
|
+
const result = [];
|
|
177
|
+
for (const spec of specs) {
|
|
178
|
+
for (const clause of splitConj(parseExpr(spec))) {
|
|
179
|
+
result.push(resolveExpr(clause, ctx));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return result;
|
|
183
|
+
}
|
|
184
|
+
function splitConj(e) {
|
|
185
|
+
if (e.kind === "binop" && e.op === "&&")
|
|
186
|
+
return [...splitConj(e.left), ...splitConj(e.right)];
|
|
187
|
+
return [e];
|
|
188
|
+
}
|
|
189
|
+
// ── Resolve statements ───────────────────────────────────────
|
|
190
|
+
function resolveBlock(stmts, ctx) {
|
|
191
|
+
const result = [];
|
|
192
|
+
let env = ctx.env;
|
|
193
|
+
for (const s of stmts) {
|
|
194
|
+
const [typed, nextEnv] = resolveStmt(s, withEnv(ctx, env));
|
|
195
|
+
result.push(typed);
|
|
196
|
+
env = nextEnv;
|
|
197
|
+
}
|
|
198
|
+
return result;
|
|
199
|
+
}
|
|
200
|
+
function resolveStmt(s, ctx) {
|
|
201
|
+
switch (s.kind) {
|
|
202
|
+
case "let": {
|
|
203
|
+
const ty = resolveTsType(s.tsType, ctx.overrides, s.name);
|
|
204
|
+
const init = coerceStr(resolveExpr(s.init, ctx), ty);
|
|
205
|
+
return [{ kind: "let", name: s.name, ty, mutable: s.mutable, init }, extend(ctx.env, s.name, ty)];
|
|
206
|
+
}
|
|
207
|
+
case "assign": {
|
|
208
|
+
const targetTy = lookup(ctx.env, s.target) ?? { kind: "unknown" };
|
|
209
|
+
return [{ kind: "assign", target: s.target, value: coerceStr(resolveExpr(s.value, ctx), targetTy) }, ctx.env];
|
|
210
|
+
}
|
|
211
|
+
case "return":
|
|
212
|
+
return [{ kind: "return", value: coerceStr(resolveExpr(s.value, ctx), ctx.returnTy) }, ctx.env];
|
|
213
|
+
case "break":
|
|
214
|
+
return [{ kind: "break" }, ctx.env];
|
|
215
|
+
case "continue":
|
|
216
|
+
return [{ kind: "continue" }, ctx.env];
|
|
217
|
+
case "expr":
|
|
218
|
+
return [{ kind: "expr", expr: resolveExpr(s.expr, ctx) }, ctx.env];
|
|
219
|
+
case "if":
|
|
220
|
+
return [{ kind: "if", cond: resolveExpr(s.cond, ctx), then: resolveBlock(s.then, ctx), else: resolveBlock(s.else, ctx) }, ctx.env];
|
|
221
|
+
case "while": {
|
|
222
|
+
const whileSpecCtx = { ...ctx, inSpec: true };
|
|
223
|
+
return [{
|
|
224
|
+
kind: "while",
|
|
225
|
+
cond: resolveExpr(s.cond, ctx),
|
|
226
|
+
invariants: resolveSpecs(s.invariants, whileSpecCtx),
|
|
227
|
+
decreases: s.decreases ? resolveSpec(s.decreases, whileSpecCtx) : null,
|
|
228
|
+
doneWith: s.doneWith ? resolveSpec(s.doneWith, whileSpecCtx) : null,
|
|
229
|
+
body: resolveBlock(s.body, ctx),
|
|
230
|
+
}, ctx.env];
|
|
231
|
+
}
|
|
232
|
+
case "forof": {
|
|
233
|
+
const iterable = resolveExpr(s.iterable, ctx);
|
|
234
|
+
const elemTy = iterable.ty.kind === "array" ? iterable.ty.elem : { kind: "unknown" };
|
|
235
|
+
const idxName = `_${s.varName}_idx`;
|
|
236
|
+
const withIdx = extend(ctx.env, idxName, { kind: "nat" });
|
|
237
|
+
const withElem = extend(withIdx, s.varName, elemTy);
|
|
238
|
+
const bodyCtx = withEnv(ctx, withElem);
|
|
239
|
+
return [{
|
|
240
|
+
kind: "forof", varName: s.varName, varTy: elemTy, iterable,
|
|
241
|
+
invariants: resolveSpecs(s.invariants, { ...bodyCtx, inSpec: true }),
|
|
242
|
+
doneWith: s.doneWith ? resolveSpec(s.doneWith, { ...bodyCtx, inSpec: true }) : null,
|
|
243
|
+
body: resolveBlock(s.body, bodyCtx),
|
|
244
|
+
}, ctx.env];
|
|
245
|
+
}
|
|
246
|
+
case "switch":
|
|
247
|
+
return [{
|
|
248
|
+
kind: "switch", expr: resolveExpr(s.expr, ctx), discriminant: s.discriminant,
|
|
249
|
+
cases: s.cases.map(c => ({ label: c.label, body: resolveBlock(c.body, ctx) })),
|
|
250
|
+
defaultBody: resolveBlock(s.defaultBody, ctx),
|
|
251
|
+
}, ctx.env];
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
// ── Pure / return-in-loop detection ──────────────────────────
|
|
255
|
+
/** Syntactic purity: no while, no for-of, no mutable let. */
|
|
256
|
+
function isSyntacticallyPure(stmts) {
|
|
257
|
+
for (const s of stmts) {
|
|
258
|
+
switch (s.kind) {
|
|
259
|
+
case "while":
|
|
260
|
+
case "forof": return false;
|
|
261
|
+
case "let":
|
|
262
|
+
if (s.mutable)
|
|
263
|
+
return false;
|
|
264
|
+
break;
|
|
265
|
+
case "if":
|
|
266
|
+
if (!isSyntacticallyPure(s.then) || !isSyntacticallyPure(s.else))
|
|
267
|
+
return false;
|
|
268
|
+
break;
|
|
269
|
+
case "switch":
|
|
270
|
+
if (!s.cases.every(c => isSyntacticallyPure(c.body)) || !isSyntacticallyPure(s.defaultBody))
|
|
271
|
+
return false;
|
|
272
|
+
break;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
// ── Call graph ──────────────────────────────────────────────
|
|
278
|
+
/** Collect all same-file function calls from expressions (including inside lambdas). */
|
|
279
|
+
function collectCallsExpr(e, fns, out) {
|
|
280
|
+
switch (e.kind) {
|
|
281
|
+
case "call":
|
|
282
|
+
if (e.fn.kind === "var" && fns.has(e.fn.name))
|
|
283
|
+
out.add(e.fn.name);
|
|
284
|
+
collectCallsExpr(e.fn, fns, out);
|
|
285
|
+
for (const a of e.args)
|
|
286
|
+
collectCallsExpr(a, fns, out);
|
|
287
|
+
return;
|
|
288
|
+
case "binop":
|
|
289
|
+
collectCallsExpr(e.left, fns, out);
|
|
290
|
+
collectCallsExpr(e.right, fns, out);
|
|
291
|
+
return;
|
|
292
|
+
case "unop":
|
|
293
|
+
collectCallsExpr(e.expr, fns, out);
|
|
294
|
+
return;
|
|
295
|
+
case "field":
|
|
296
|
+
collectCallsExpr(e.obj, fns, out);
|
|
297
|
+
return;
|
|
298
|
+
case "index":
|
|
299
|
+
collectCallsExpr(e.obj, fns, out);
|
|
300
|
+
collectCallsExpr(e.idx, fns, out);
|
|
301
|
+
return;
|
|
302
|
+
case "record":
|
|
303
|
+
if (e.spread)
|
|
304
|
+
collectCallsExpr(e.spread, fns, out);
|
|
305
|
+
for (const f of e.fields)
|
|
306
|
+
collectCallsExpr(f.value, fns, out);
|
|
307
|
+
return;
|
|
308
|
+
case "arrayLiteral":
|
|
309
|
+
for (const el of e.elems)
|
|
310
|
+
collectCallsExpr(el, fns, out);
|
|
311
|
+
return;
|
|
312
|
+
case "lambda":
|
|
313
|
+
if (Array.isArray(e.body))
|
|
314
|
+
collectCallsStmts(e.body, fns, out);
|
|
315
|
+
else
|
|
316
|
+
collectCallsExpr(e.body, fns, out);
|
|
317
|
+
return;
|
|
318
|
+
case "forall":
|
|
319
|
+
case "exists":
|
|
320
|
+
collectCallsExpr(e.body, fns, out);
|
|
321
|
+
return;
|
|
322
|
+
case "conditional":
|
|
323
|
+
collectCallsExpr(e.cond, fns, out);
|
|
324
|
+
collectCallsExpr(e.then, fns, out);
|
|
325
|
+
collectCallsExpr(e.else, fns, out);
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
function collectCallsStmts(stmts, fns, out) {
|
|
330
|
+
for (const s of stmts) {
|
|
331
|
+
switch (s.kind) {
|
|
332
|
+
case "let":
|
|
333
|
+
collectCallsExpr(s.init, fns, out);
|
|
334
|
+
break;
|
|
335
|
+
case "assign":
|
|
336
|
+
collectCallsExpr(s.value, fns, out);
|
|
337
|
+
break;
|
|
338
|
+
case "return":
|
|
339
|
+
collectCallsExpr(s.value, fns, out);
|
|
340
|
+
break;
|
|
341
|
+
case "expr":
|
|
342
|
+
collectCallsExpr(s.expr, fns, out);
|
|
343
|
+
break;
|
|
344
|
+
case "if":
|
|
345
|
+
collectCallsExpr(s.cond, fns, out);
|
|
346
|
+
collectCallsStmts(s.then, fns, out);
|
|
347
|
+
collectCallsStmts(s.else, fns, out);
|
|
348
|
+
break;
|
|
349
|
+
case "while":
|
|
350
|
+
collectCallsExpr(s.cond, fns, out);
|
|
351
|
+
collectCallsStmts(s.body, fns, out);
|
|
352
|
+
break;
|
|
353
|
+
case "forof":
|
|
354
|
+
collectCallsExpr(s.iterable, fns, out);
|
|
355
|
+
collectCallsStmts(s.body, fns, out);
|
|
356
|
+
break;
|
|
357
|
+
case "switch":
|
|
358
|
+
collectCallsExpr(s.expr, fns, out);
|
|
359
|
+
for (const c of s.cases)
|
|
360
|
+
collectCallsStmts(c.body, fns, out);
|
|
361
|
+
collectCallsStmts(s.defaultBody, fns, out);
|
|
362
|
+
break;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function computePureFns(functions) {
|
|
367
|
+
const allFnNames = new Set(functions.map(fn => fn.name));
|
|
368
|
+
// Build call graph: fn → set of same-file functions it calls
|
|
369
|
+
const callGraph = new Map();
|
|
370
|
+
for (const fn of functions) {
|
|
371
|
+
const calls = new Set();
|
|
372
|
+
collectCallsStmts(fn.body, allFnNames, calls);
|
|
373
|
+
callGraph.set(fn.name, calls);
|
|
374
|
+
}
|
|
375
|
+
// Seed: syntactically non-pure functions
|
|
376
|
+
const nonPure = new Set(functions.filter(fn => !isSyntacticallyPure(fn.body)).map(fn => fn.name));
|
|
377
|
+
// Build reverse graph: fn → set of functions that call it
|
|
378
|
+
const callers = new Map();
|
|
379
|
+
for (const name of allFnNames)
|
|
380
|
+
callers.set(name, new Set());
|
|
381
|
+
for (const [caller, callees] of callGraph) {
|
|
382
|
+
for (const callee of callees)
|
|
383
|
+
callers.get(callee).add(caller);
|
|
384
|
+
}
|
|
385
|
+
// Propagate impurity through reverse call graph
|
|
386
|
+
const worklist = [...nonPure];
|
|
387
|
+
while (worklist.length > 0) {
|
|
388
|
+
const fn = worklist.pop();
|
|
389
|
+
for (const caller of callers.get(fn) ?? []) {
|
|
390
|
+
if (!nonPure.has(caller)) {
|
|
391
|
+
nonPure.add(caller);
|
|
392
|
+
worklist.push(caller);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
return new Set(functions.map(fn => fn.name).filter(name => !nonPure.has(name)));
|
|
397
|
+
}
|
|
398
|
+
function hasReturnInLoop(stmts) {
|
|
399
|
+
for (const s of stmts) {
|
|
400
|
+
if ((s.kind === "while" || s.kind === "forof") && containsReturn(s.body))
|
|
401
|
+
return true;
|
|
402
|
+
if (s.kind === "if" && (hasReturnInLoop(s.then) || hasReturnInLoop(s.else)))
|
|
403
|
+
return true;
|
|
404
|
+
if (s.kind === "switch" && (s.cases.some(c => hasReturnInLoop(c.body)) || hasReturnInLoop(s.defaultBody)))
|
|
405
|
+
return true;
|
|
406
|
+
}
|
|
407
|
+
return false;
|
|
408
|
+
}
|
|
409
|
+
function containsReturn(stmts) {
|
|
410
|
+
for (const s of stmts) {
|
|
411
|
+
if (s.kind === "return")
|
|
412
|
+
return true;
|
|
413
|
+
if (s.kind === "if" && (containsReturn(s.then) || containsReturn(s.else)))
|
|
414
|
+
return true;
|
|
415
|
+
if ((s.kind === "while" || s.kind === "forof") && containsReturn(s.body))
|
|
416
|
+
return true;
|
|
417
|
+
if (s.kind === "switch" && (s.cases.some(c => containsReturn(c.body)) || containsReturn(s.defaultBody)))
|
|
418
|
+
return true;
|
|
419
|
+
}
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
// ── Resolve function / module ────────────────────────────────
|
|
423
|
+
function resolveFunction(fn, typeDecls, pureFns) {
|
|
424
|
+
if (hasReturnInLoop(fn.body)) {
|
|
425
|
+
throw new Error(`${fn.name}: return inside a loop is not supported.`);
|
|
426
|
+
}
|
|
427
|
+
const overrides = new Map(fn.typeAnnotations.map(a => [a.name, a.type]));
|
|
428
|
+
const params = fn.params.map(p => ({ name: p.name, ty: resolveTsType(p.tsType, overrides, p.name) }));
|
|
429
|
+
const returnTy = resolveTsType(fn.returnType, overrides, "\\result");
|
|
430
|
+
let env = null;
|
|
431
|
+
for (const p of params)
|
|
432
|
+
env = extend(env, p.name, p.ty);
|
|
433
|
+
const baseCtx = { env, typeDecls, overrides, allowResult: false, returnTy, pureFns, inSpec: false, inLambda: false };
|
|
434
|
+
const requiresCtx = { ...baseCtx, inSpec: true };
|
|
435
|
+
const ensuresCtx = { ...baseCtx, allowResult: true, inSpec: true };
|
|
436
|
+
return {
|
|
437
|
+
name: fn.name, params, returnTy,
|
|
438
|
+
requires: resolveSpecs(fn.requires, requiresCtx),
|
|
439
|
+
ensures: resolveSpecs(fn.ensures, ensuresCtx),
|
|
440
|
+
isPure: pureFns.has(fn.name),
|
|
441
|
+
body: resolveBlock(fn.body, baseCtx),
|
|
442
|
+
};
|
|
443
|
+
}
|
|
444
|
+
export function resolveModule(raw) {
|
|
445
|
+
const pureFns = computePureFns(raw.functions);
|
|
446
|
+
return {
|
|
447
|
+
file: raw.file,
|
|
448
|
+
typeDecls: raw.typeDecls,
|
|
449
|
+
functions: raw.functions.map(fn => resolveFunction(fn, raw.typeDecls, pureFns)),
|
|
450
|
+
};
|
|
451
|
+
}
|