lemmascript 0.5.12 → 0.5.14
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/package.json +2 -2
- package/tools/dist/dafny-commands.js +7 -4
- package/tools/dist/dafny-emit.js +138 -48
- package/tools/dist/extract.js +6 -2
- package/tools/dist/ir.js +57 -0
- package/tools/dist/lean-emit.js +26 -10
- package/tools/dist/lsc.js +15 -2
- package/tools/dist/names.js +52 -0
- package/tools/dist/narrow.js +60 -80
- package/tools/dist/peephole.js +8 -9
- package/tools/dist/resolve.js +6 -3
- package/tools/dist/transform.js +104 -52
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemmascript",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.14",
|
|
4
4
|
"description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
"type": "git",
|
|
33
33
|
"url": "https://github.com/midspiral/LemmaScript"
|
|
34
34
|
},
|
|
35
|
-
"homepage": "https://lemmascript.
|
|
35
|
+
"homepage": "https://lemmascript.org",
|
|
36
36
|
"keywords": [
|
|
37
37
|
"lemmascript",
|
|
38
38
|
"verification",
|
|
@@ -60,11 +60,14 @@ export function dafnyVerify(dfyPath, dir, timeLimit, extraFlags) {
|
|
|
60
60
|
execFileSync("dafny", args, { cwd: dir, stdio: "inherit" });
|
|
61
61
|
return true;
|
|
62
62
|
}
|
|
63
|
-
catch {
|
|
63
|
+
catch (e) {
|
|
64
|
+
if (e?.code === "ENOENT") {
|
|
65
|
+
console.error("ERROR: `dafny` not found on PATH — verification never ran. Install Dafny 4.x: https://dafny.org/");
|
|
66
|
+
}
|
|
64
67
|
return false;
|
|
65
68
|
}
|
|
66
69
|
}
|
|
67
|
-
export function dafnyRegen(genPath, dfyPath, basePath, text, dir) {
|
|
70
|
+
export function dafnyRegen(genPath, dfyPath, basePath, text, dir, timeLimit, extraFlags) {
|
|
68
71
|
// 1. Read old gen before overwriting (needed for base seeding)
|
|
69
72
|
const oldGen = existsSync(genPath) ? readFileSync(genPath, "utf-8") : "";
|
|
70
73
|
// 2. Always write new gen so user can inspect latest output
|
|
@@ -73,7 +76,7 @@ export function dafnyRegen(genPath, dfyPath, basePath, text, dir) {
|
|
|
73
76
|
if (!existsSync(dfyPath)) {
|
|
74
77
|
writeFileSync(dfyPath, text);
|
|
75
78
|
console.log(`Created: ${path.basename(dfyPath)}`);
|
|
76
|
-
if (!dafnyVerify(dfyPath, dir)) {
|
|
79
|
+
if (!dafnyVerify(dfyPath, dir, timeLimit, extraFlags)) {
|
|
77
80
|
console.error(`FAILED: ${path.basename(dfyPath)} verification failed on first run.`);
|
|
78
81
|
process.exit(1);
|
|
79
82
|
}
|
|
@@ -108,7 +111,7 @@ export function dafnyRegen(genPath, dfyPath, basePath, text, dir) {
|
|
|
108
111
|
process.exit(1);
|
|
109
112
|
}
|
|
110
113
|
// 7. Verify
|
|
111
|
-
if (!dafnyVerify(dfyPath, dir)) {
|
|
114
|
+
if (!dafnyVerify(dfyPath, dir, timeLimit, extraFlags)) {
|
|
112
115
|
console.error(`FAILED: ${path.basename(dfyPath)} verification failed.`);
|
|
113
116
|
process.exit(1);
|
|
114
117
|
}
|
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -1,6 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Dafny emitter — IR → Dafny text.
|
|
3
3
|
*/
|
|
4
|
+
import { usesName, usesNameInDecl } from "./ir.js";
|
|
5
|
+
import { freshName, userNames } from "./names.js";
|
|
6
|
+
import { renameFreeVar } from "./transform.js";
|
|
7
|
+
/** Fresh binder for a comprehension wrapping the given subexpressions: `base`
|
|
8
|
+
* verbatim unless one of them references it, then primed until free. A *local*
|
|
9
|
+
* check — a same-named name elsewhere in the module keeps the plain binder. */
|
|
10
|
+
function freshBinder(base, ...wrapped) {
|
|
11
|
+
return freshName(base, name => wrapped.some(w => usesName(w, name)));
|
|
12
|
+
}
|
|
13
|
+
/** Binder + body for lowering a single-return lambda to a comprehension whose
|
|
14
|
+
* receiver is emitted inside the binder's scope. TS scoping keeps the receiver
|
|
15
|
+
* outside the lambda, so a lambda param sharing a name with anything free in
|
|
16
|
+
* the receiver would capture it — e.g. `mk(n).some(n => …)` naively emitting
|
|
17
|
+
* `exists n :: n in mk(n) && …`. Alpha-rename the param out of the way (and
|
|
18
|
+
* its free uses in the body); the zero-param default must also dodge free
|
|
19
|
+
* names in the body. */
|
|
20
|
+
function comprehensionBinder(lam, value, receiver) {
|
|
21
|
+
const rawName = lam.params[0]?.name;
|
|
22
|
+
if (rawName === undefined)
|
|
23
|
+
return { binder: escapeName(freshBinder("x", receiver, value)), body: value };
|
|
24
|
+
if (!usesName(receiver, rawName))
|
|
25
|
+
return { binder: escapeName(rawName), body: value };
|
|
26
|
+
const fresh = freshBinder(rawName, receiver, value);
|
|
27
|
+
return { binder: escapeName(fresh), body: renameFreeVar(value, rawName, fresh) };
|
|
28
|
+
}
|
|
4
29
|
// ── Ty → Dafny type string ─────────────────────────────────
|
|
5
30
|
function tyToDafny(ty) {
|
|
6
31
|
switch (ty.kind) {
|
|
@@ -17,7 +42,7 @@ function tyToDafny(ty) {
|
|
|
17
42
|
needPreamble("OptionType");
|
|
18
43
|
return `Option<${tyToDafny(ty.inner)}>`;
|
|
19
44
|
}
|
|
20
|
-
case "user": return ty.name;
|
|
45
|
+
case "user": return escapeName(ty.name);
|
|
21
46
|
case "fn": return `(${ty.params.map(tyToDafny).join(", ")}) -> ${tyToDafny(ty.result)}`;
|
|
22
47
|
// Out-of-subset (`any`/`unknown`); opaque so real ops on it fail loudly
|
|
23
48
|
// rather than silently verify as `int`. Mirrors the Lean backend's `_`.
|
|
@@ -50,22 +75,83 @@ const DAFNY_KEYWORDS = new Set([
|
|
|
50
75
|
"copredicate", "inductive",
|
|
51
76
|
]);
|
|
52
77
|
// The Dafny out-parameter name for the method currently being emitted. Default
|
|
53
|
-
// `res`, but bumped (e.g. `
|
|
54
|
-
// methodHeader and reset per decl. `\result` in an ensures must use the
|
|
55
|
-
// name, so escapeName routes it here.
|
|
78
|
+
// `res`, but bumped (e.g. `res'`) when the method's own scope uses `res` — set
|
|
79
|
+
// by methodHeader and reset per decl. `\result` in an ensures must use the
|
|
80
|
+
// *same* name, so escapeName routes it here.
|
|
56
81
|
let _resultName = "res";
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
// ── Dafny name allocation ──────────────────────────────────
|
|
83
|
+
//
|
|
84
|
+
// freshName (names.ts) freshens in the *raw TS* namespace — but that is not the
|
|
85
|
+
// namespace Dafny sees. Escaping maps `_x`→`i_x` and keyword `match`→`match_`,
|
|
86
|
+
// so two raw-distinct names can collapse *after* escaping: a raw-freshened temp
|
|
87
|
+
// `_t0'`→`i_t0'` colliding with a user `_t0` that escaped-and-primed to `i_t0'`.
|
|
88
|
+
// So Dafny hygiene is a second allocator, layered at emission: escape to a base,
|
|
89
|
+
// then freshen against the names already claimed in the Dafny namespace. User
|
|
90
|
+
// names are allocated up front (Dafny-safe ones kept exact); generated names are
|
|
91
|
+
// allocated on first sight and cached so a decl and its references agree. Reset
|
|
92
|
+
// per file. (The raw freshName layer stays — Lean has a different escaping story.)
|
|
93
|
+
function dafnyBaseName(name) {
|
|
62
94
|
if (DAFNY_KEYWORDS.has(name))
|
|
63
95
|
return `${name}_`;
|
|
64
|
-
// Dafny doesn't allow identifiers starting with _
|
|
65
96
|
if (name.startsWith("_"))
|
|
66
|
-
return `i${name}`;
|
|
97
|
+
return `i${name}`; // Dafny forbids leading `_`
|
|
67
98
|
return name;
|
|
68
99
|
}
|
|
100
|
+
let _userDafnyNames = new Map();
|
|
101
|
+
let _generatedDafnyNames = new Map();
|
|
102
|
+
let _takenDafnyNames = new Set();
|
|
103
|
+
/** `base`, primed until free in the Dafny namespace. A prime can't occur in a
|
|
104
|
+
* TS identifier, so priming always leaves user-name space. */
|
|
105
|
+
function freshDafnyName(base) {
|
|
106
|
+
let out = base;
|
|
107
|
+
while (_takenDafnyNames.has(out))
|
|
108
|
+
out += "'";
|
|
109
|
+
return out;
|
|
110
|
+
}
|
|
111
|
+
function resetDafnyNameCache() {
|
|
112
|
+
_userDafnyNames = new Map();
|
|
113
|
+
_generatedDafnyNames = new Map();
|
|
114
|
+
_takenDafnyNames = new Set();
|
|
115
|
+
const raws = [...userNames()].sort();
|
|
116
|
+
// Dafny-safe source names keep their spelling; names that must mangle are then
|
|
117
|
+
// freshened in the emitted namespace (safe-first, sorted → deterministic).
|
|
118
|
+
for (const raw of raws)
|
|
119
|
+
if (dafnyBaseName(raw) === raw) {
|
|
120
|
+
_userDafnyNames.set(raw, raw);
|
|
121
|
+
_takenDafnyNames.add(raw);
|
|
122
|
+
}
|
|
123
|
+
for (const raw of raws)
|
|
124
|
+
if (dafnyBaseName(raw) !== raw) {
|
|
125
|
+
const emitted = freshDafnyName(dafnyBaseName(raw));
|
|
126
|
+
_userDafnyNames.set(raw, emitted);
|
|
127
|
+
_takenDafnyNames.add(emitted);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function escapeName(name) {
|
|
131
|
+
// \result is carried through the IR as var "\\result"; render it as the
|
|
132
|
+
// current method's out-parameter name (chosen locally by methodHeader).
|
|
133
|
+
if (name === "\\result")
|
|
134
|
+
return _resultName;
|
|
135
|
+
const user = _userDafnyNames.get(name);
|
|
136
|
+
if (user !== undefined)
|
|
137
|
+
return user;
|
|
138
|
+
return escapeGeneratedName(name);
|
|
139
|
+
}
|
|
140
|
+
/** Allocate a toolchain-generated name (an ANF temp, a comprehension binder, a
|
|
141
|
+
* companion `_ensures` lemma). Escapes to a base, then freshens in the Dafny
|
|
142
|
+
* namespace so it can't collapse onto an escaped user name. Bypasses the user
|
|
143
|
+
* map on purpose: the raw name is synthesized, so it must be freshened *away
|
|
144
|
+
* from* a same-spelled user name, not aliased onto it. Cached so a declaration
|
|
145
|
+
* and its references render identically. */
|
|
146
|
+
function escapeGeneratedName(name) {
|
|
147
|
+
const cached = _generatedDafnyNames.get(name);
|
|
148
|
+
if (cached !== undefined)
|
|
149
|
+
return cached;
|
|
150
|
+
const emitted = freshDafnyName(dafnyBaseName(name));
|
|
151
|
+
_generatedDafnyNames.set(name, emitted);
|
|
152
|
+
_takenDafnyNames.add(emitted);
|
|
153
|
+
return emitted;
|
|
154
|
+
}
|
|
69
155
|
/** Format a typed parameter list for Dafny: "x: int, y: seq<int>" */
|
|
70
156
|
function paramList(params) {
|
|
71
157
|
return params.map(p => `${escapeName(p.name)}: ${tyToDafny(p.type)}`).join(", ");
|
|
@@ -73,17 +159,18 @@ function paramList(params) {
|
|
|
73
159
|
/** Format a method signature header, omitting `returns` for void methods.
|
|
74
160
|
* Dafny's definite-assignment rule rejects unassigned out-parameters, so a
|
|
75
161
|
* `returns (res: ())` on a void method fails verification. */
|
|
76
|
-
function methodHeader(prefix, params, returnType) {
|
|
162
|
+
function methodHeader(prefix, params, returnType, scope) {
|
|
77
163
|
const sig = `${prefix}(${paramList(params)})`;
|
|
78
164
|
if (returnType.kind === "void")
|
|
79
165
|
return sig;
|
|
80
|
-
// The out-parameter is `res` by default, but a
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
166
|
+
// The out-parameter is `res` by default, but a param (an Express handler's
|
|
167
|
+
// `(req, res)`), body local, or callee named `res` would shadow it. Check only
|
|
168
|
+
// *this method's own* signature and body — `res` is common module-wide (fields,
|
|
169
|
+
// unrelated params), so a module-wide check would prime spuriously. The primed
|
|
170
|
+
// name is recorded so `\result` references resolve to it.
|
|
171
|
+
const taken = (n) => params.some(p => escapeName(p.name) === n) ||
|
|
172
|
+
(scope !== undefined && usesNameInDecl(scope.requires, scope.ensures, scope.body, n));
|
|
173
|
+
const resName = freshName("res", taken);
|
|
87
174
|
_resultName = resName;
|
|
88
175
|
return `${sig} returns (${resName}: ${tyToDafny(returnType)})`;
|
|
89
176
|
}
|
|
@@ -106,7 +193,7 @@ function emitQuantifier(e, keyword) {
|
|
|
106
193
|
while (body.kind === e.kind) {
|
|
107
194
|
const dty = tyToDafny(body.type);
|
|
108
195
|
const ann = dty === "string" ? "" : `: ${dty}`;
|
|
109
|
-
vars.push(`${body.var}${ann}`);
|
|
196
|
+
vars.push(`${escapeName(body.var)}${ann}`);
|
|
110
197
|
body = body.body;
|
|
111
198
|
}
|
|
112
199
|
return `${keyword} ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
@@ -222,8 +309,8 @@ function emitExpr(e) {
|
|
|
222
309
|
const ret = lam.body[0];
|
|
223
310
|
if (ret.kind !== "return")
|
|
224
311
|
throw new Error("unreachable");
|
|
225
|
-
const p =
|
|
226
|
-
const body = emitExpr(
|
|
312
|
+
const { binder: p, body: v } = comprehensionBinder(lam, ret.value, e.obj);
|
|
313
|
+
const body = emitExpr(v);
|
|
227
314
|
return `(exists ${p} :: ${p} in ${obj} && ${body})`;
|
|
228
315
|
}
|
|
229
316
|
}
|
|
@@ -300,8 +387,13 @@ function emitExpr(e) {
|
|
|
300
387
|
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
301
388
|
if (e.method === "has")
|
|
302
389
|
return `(${args[0]} in ${obj})`;
|
|
303
|
-
if (e.method === "delete")
|
|
304
|
-
|
|
390
|
+
if (e.method === "delete") {
|
|
391
|
+
// Minted comprehension binder: freshen so a user variable `k` in the
|
|
392
|
+
// receiver or key isn't captured (`k != k` would delete nothing).
|
|
393
|
+
// Local check — only this comprehension's own operands can collide.
|
|
394
|
+
const k = escapeName(freshBinder("k", e.obj, e.args[0]));
|
|
395
|
+
return `(map ${k} | ${k} in ${obj} && ${k} != ${args[0]} :: ${obj}[${k}])`;
|
|
396
|
+
}
|
|
305
397
|
}
|
|
306
398
|
// Set methods
|
|
307
399
|
if (ty === "set") {
|
|
@@ -321,8 +413,8 @@ function emitExpr(e) {
|
|
|
321
413
|
const ret = lam.body[0];
|
|
322
414
|
if (ret.kind !== "return")
|
|
323
415
|
throw new Error("unreachable");
|
|
324
|
-
const p =
|
|
325
|
-
const body = emitExpr(
|
|
416
|
+
const { binder: p, body: v } = comprehensionBinder(lam, ret.value, e.obj);
|
|
417
|
+
const body = emitExpr(v);
|
|
326
418
|
return `(set ${p} | ${p} in ${obj} && ${body})`;
|
|
327
419
|
}
|
|
328
420
|
}
|
|
@@ -447,9 +539,11 @@ function emitExpr(e) {
|
|
|
447
539
|
}
|
|
448
540
|
case "field": {
|
|
449
541
|
const obj = emitExpr(e.obj);
|
|
450
|
-
|
|
542
|
+
// `size`/`length`/`keys` are collection intrinsics unless the transform
|
|
543
|
+
// proved this is a declared datatype field (then project it).
|
|
544
|
+
if (!e.datatypeField && (e.field === "size" || e.field === "length" || e.field === "collectionSize"))
|
|
451
545
|
return `|${obj}|`;
|
|
452
|
-
if (e.field === "keys")
|
|
546
|
+
if (!e.datatypeField && e.field === "keys")
|
|
453
547
|
return `${obj}.Keys`;
|
|
454
548
|
if (e.field === "toNat")
|
|
455
549
|
return obj;
|
|
@@ -681,23 +775,23 @@ function emitDecl(d) {
|
|
|
681
775
|
const fields = c.fields.map(f => collides.has(f.name) ? { ...f, name: `${f.name}_${c.name}` } : f);
|
|
682
776
|
return `${escapeName(c.name)}(${paramList(fields)})`;
|
|
683
777
|
});
|
|
684
|
-
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
778
|
+
return `datatype ${escapeName(d.name)}${tp} = ${ctors.join(" | ")}`;
|
|
685
779
|
}
|
|
686
780
|
case "structure": {
|
|
687
781
|
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
688
|
-
return `datatype ${d.name}${tp} = ${d.name}(${paramList(d.fields)})`;
|
|
782
|
+
return `datatype ${escapeName(d.name)}${tp} = ${escapeName(d.name)}(${paramList(d.fields)})`;
|
|
689
783
|
}
|
|
690
784
|
case "type-alias": {
|
|
691
|
-
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
785
|
+
return `type ${escapeName(d.name)} = ${tyToDafny(d.target)}`;
|
|
692
786
|
}
|
|
693
787
|
case "opaque-type": {
|
|
694
788
|
// Abstract type — no definition. `(==)` so it can sit inside datatypes
|
|
695
789
|
// that derive structural equality. Never constructed or destructured.
|
|
696
|
-
return `type ${d.name}(==)`;
|
|
790
|
+
return `type ${escapeName(d.name)}(==)`;
|
|
697
791
|
}
|
|
698
792
|
case "def": {
|
|
699
793
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
700
|
-
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
794
|
+
const lines = [`function ${escapeName(d.name)}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
701
795
|
for (const r of d.requires)
|
|
702
796
|
lines.push(` requires ${emitExpr(r)}`);
|
|
703
797
|
if (d.decreases)
|
|
@@ -710,7 +804,7 @@ function emitDecl(d) {
|
|
|
710
804
|
// Strip constraints like (==) from type params — ghost lemmas don't need them
|
|
711
805
|
const lemmaTP = d.typeParams.length > 0 ? `<${d.typeParams.map(t => t.replace(/\(.*\)/, '')).join(", ")}>` : "";
|
|
712
806
|
lines.push("");
|
|
713
|
-
lines.push(`lemma ${d.name}_ensures${lemmaTP}(${paramList(d.params)})`);
|
|
807
|
+
lines.push(`lemma ${escapeGeneratedName(`${d.name}_ensures`)}${lemmaTP}(${paramList(d.params)})`);
|
|
714
808
|
for (const r of d.requires)
|
|
715
809
|
lines.push(` requires ${emitExpr(r)}`);
|
|
716
810
|
for (const e of d.ensures)
|
|
@@ -722,7 +816,7 @@ function emitDecl(d) {
|
|
|
722
816
|
}
|
|
723
817
|
case "def-by-method": {
|
|
724
818
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
725
|
-
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
819
|
+
const lines = [`function ${escapeName(d.name)}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
726
820
|
for (const r of d.requires)
|
|
727
821
|
lines.push(` requires ${emitExpr(r)}`);
|
|
728
822
|
if (d.decreases)
|
|
@@ -736,7 +830,7 @@ function emitDecl(d) {
|
|
|
736
830
|
}
|
|
737
831
|
case "method": {
|
|
738
832
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
739
|
-
const lines = [methodHeader(`method ${d.name}${tp}`, d.params, d.returnType)];
|
|
833
|
+
const lines = [methodHeader(`method ${escapeName(d.name)}${tp}`, d.params, d.returnType, d)];
|
|
740
834
|
for (const r of d.requires)
|
|
741
835
|
lines.push(` requires ${emitExpr(r)}`);
|
|
742
836
|
for (const e of d.ensures)
|
|
@@ -749,14 +843,14 @@ function emitDecl(d) {
|
|
|
749
843
|
return lines.join("\n");
|
|
750
844
|
}
|
|
751
845
|
case "class": {
|
|
752
|
-
const lines = [`class ${d.name} {`];
|
|
846
|
+
const lines = [`class ${escapeName(d.name)} {`];
|
|
753
847
|
for (const f of d.fields) {
|
|
754
848
|
lines.push(` var ${escapeName(f.name)}: ${tyToDafny(f.type)}`);
|
|
755
849
|
}
|
|
756
850
|
if (d.fields.length > 0 && d.methods.length > 0)
|
|
757
851
|
lines.push("");
|
|
758
852
|
for (const m of d.methods) {
|
|
759
|
-
lines.push(` ${methodHeader(`method ${m.name}`, m.params, m.returnType)}`);
|
|
853
|
+
lines.push(` ${methodHeader(`method ${escapeName(m.name)}`, m.params, m.returnType, m)}`);
|
|
760
854
|
for (const r of m.requires)
|
|
761
855
|
lines.push(` requires ${emitExpr(r)}`);
|
|
762
856
|
for (const e of m.ensures)
|
|
@@ -1211,21 +1305,17 @@ function qualifyCtor(name, type) {
|
|
|
1211
1305
|
* "_" → "_"
|
|
1212
1306
|
*/
|
|
1213
1307
|
const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
1214
|
-
function translatePattern(
|
|
1215
|
-
if (
|
|
1308
|
+
function translatePattern(p) {
|
|
1309
|
+
if (p.kind === "wild")
|
|
1216
1310
|
return "_";
|
|
1217
|
-
const
|
|
1218
|
-
if (
|
|
1219
|
-
return pattern;
|
|
1220
|
-
const ctorName = CTOR_MAP[m[1]] ?? escapeName(m[1]);
|
|
1221
|
-
const fields = m[2].trim();
|
|
1222
|
-
if (!fields)
|
|
1311
|
+
const ctorName = CTOR_MAP[p.ctor] ?? escapeName(p.ctor);
|
|
1312
|
+
if (p.binders.length === 0)
|
|
1223
1313
|
return ctorName;
|
|
1224
|
-
|
|
1225
|
-
return `${ctorName}(${fieldNames.join(", ")})`;
|
|
1314
|
+
return `${ctorName}(${p.binders.map(escapeName).join(", ")})`;
|
|
1226
1315
|
}
|
|
1227
1316
|
export function emitDafnyFile(file, tsFileName, opts) {
|
|
1228
1317
|
_useSafeSlice = !!opts?.safeSlice;
|
|
1318
|
+
resetDafnyNameCache();
|
|
1229
1319
|
buildRecordCtorMap(file.decls);
|
|
1230
1320
|
_neededPreambles.clear();
|
|
1231
1321
|
// Track successfully emitted pure defs — method wrappers are only
|
package/tools/dist/extract.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { Project, Node, SyntaxKind, ScriptTarget, ts } from "ts-morph";
|
|
8
8
|
import { initTypeParser } from "./types.js";
|
|
9
|
+
import { setUserNames, freshName } from "./names.js";
|
|
9
10
|
// ── Expression extraction ────────────────────────────────────
|
|
10
11
|
/** When set, calls whose function/method name matches this key are replaced with havoc. */
|
|
11
12
|
let _havocKey = null;
|
|
@@ -1215,7 +1216,7 @@ function extractStmts(stmts) {
|
|
|
1215
1216
|
let initExpr = extractExpr(initializer);
|
|
1216
1217
|
let initVar = initExpr;
|
|
1217
1218
|
if (initExpr.kind !== "var") {
|
|
1218
|
-
const tempName = `_destr${_destrCounter++}
|
|
1219
|
+
const tempName = freshName(`_destr${_destrCounter++}`);
|
|
1219
1220
|
const initTs = _eraseGenerics(typeToString(initializer.getType()));
|
|
1220
1221
|
result.push({ kind: "let", name: tempName, mutable: false, tsType: initTs, init: initExpr, line });
|
|
1221
1222
|
initVar = { kind: "var", name: tempName };
|
|
@@ -1259,7 +1260,7 @@ function extractStmts(stmts) {
|
|
|
1259
1260
|
let initExpr = extractExpr(initializer);
|
|
1260
1261
|
let initVar = initExpr;
|
|
1261
1262
|
if (initExpr.kind !== "var") {
|
|
1262
|
-
const tempName = `_destr${_destrCounter++}
|
|
1263
|
+
const tempName = freshName(`_destr${_destrCounter++}`);
|
|
1263
1264
|
const initTs = _eraseGenerics(typeToString(initializer.getType()));
|
|
1264
1265
|
result.push({ kind: "let", name: tempName, mutable: false, tsType: initTs, init: initExpr, line });
|
|
1265
1266
|
initVar = { kind: "var", name: tempName };
|
|
@@ -1859,6 +1860,9 @@ function extractFunctionInner(fn, parentAnnotations) {
|
|
|
1859
1860
|
}
|
|
1860
1861
|
// ── Module extraction ────────────────────────────────────────
|
|
1861
1862
|
export function extractModule(sourceFile) {
|
|
1863
|
+
// Seed the fresh-name check (names.ts) before anything mints: every
|
|
1864
|
+
// Identifier token in the module, a deliberate over-approximation.
|
|
1865
|
+
setUserNames(new Set(sourceFile.getDescendantsOfKind(SyntaxKind.Identifier).map(i => i.getText())));
|
|
1862
1866
|
const typeDecls = [];
|
|
1863
1867
|
// Cross-file calls are auto-externed: ts-morph resolves the call's symbol;
|
|
1864
1868
|
// if it's defined in a different source file we treat the symbol as opaque
|
package/tools/dist/ir.js
CHANGED
|
@@ -4,6 +4,19 @@
|
|
|
4
4
|
* The transform phase produces these types.
|
|
5
5
|
* The emit phase pretty-prints them to backend syntax (Lean or Dafny).
|
|
6
6
|
*/
|
|
7
|
+
export const pWild = () => ({ kind: "wild" });
|
|
8
|
+
export const pCtor = (ctor, ...binders) => ({ kind: "ctor", ctor, binders });
|
|
9
|
+
/** Binder identifiers a pattern introduces (`[]` for wildcard / nullary ctor). */
|
|
10
|
+
export function patternBinders(p) {
|
|
11
|
+
return p.kind === "ctor" ? p.binders : [];
|
|
12
|
+
}
|
|
13
|
+
export function patternCtor(p) {
|
|
14
|
+
return p.kind === "ctor" ? p.ctor : null;
|
|
15
|
+
}
|
|
16
|
+
/** Does the pattern bind `name`? */
|
|
17
|
+
export function patternBinds(p, name) {
|
|
18
|
+
return patternBinders(p).includes(name);
|
|
19
|
+
}
|
|
7
20
|
export function anyExpr(e, pred) {
|
|
8
21
|
if (pred(e))
|
|
9
22
|
return true;
|
|
@@ -60,3 +73,47 @@ export function anyExprInStmt(s, pred) {
|
|
|
60
73
|
export function anyExprInStmts(stmts, pred) {
|
|
61
74
|
return stmts.some(s => anyExprInStmt(s, pred));
|
|
62
75
|
}
|
|
76
|
+
// A name is "used" — such that a synthesized binder of the same name would
|
|
77
|
+
// capture or shadow it — iff it appears as a variable reference or a called
|
|
78
|
+
// function. These drive the *local* freshness checks for user-facing binders
|
|
79
|
+
// (the result out-parameter, comprehension binders): a binder is checked only
|
|
80
|
+
// against the expressions/scope it actually wraps, not the whole module.
|
|
81
|
+
const _refsName = (name) => e => (e.kind === "var" && e.name === name) ||
|
|
82
|
+
(e.kind === "app" && e.fn === name) ||
|
|
83
|
+
(e.kind === "constructor" && e.name === name) ||
|
|
84
|
+
(e.kind === "match" && typeof e.scrutinee === "string" && e.scrutinee === name);
|
|
85
|
+
export function usesName(e, name) {
|
|
86
|
+
return anyExpr(e, _refsName(name));
|
|
87
|
+
}
|
|
88
|
+
export function usesNameInStmts(stmts, name) {
|
|
89
|
+
return anyExprInStmts(stmts, _refsName(name));
|
|
90
|
+
}
|
|
91
|
+
/** Names a statement tree *binds, targets, or introduces* — `let`/`let-bind`/
|
|
92
|
+
* `ghostLet` names, `assign`/`bind`/`ghostAssign` targets, `for-in` indices,
|
|
93
|
+
* and `match`-arm pattern binders — recursing through nested blocks. Distinct
|
|
94
|
+
* from `usesNameInStmts` (expression references only): an unread or assign-only
|
|
95
|
+
* local still duplicate-declares against a method's out-parameter in Dafny. */
|
|
96
|
+
export function bindsNameInStmts(stmts, name) {
|
|
97
|
+
return stmts.some(s => {
|
|
98
|
+
switch (s.kind) {
|
|
99
|
+
case "let":
|
|
100
|
+
case "let-bind":
|
|
101
|
+
case "ghostLet": return s.name === name;
|
|
102
|
+
case "assign":
|
|
103
|
+
case "bind":
|
|
104
|
+
case "ghostAssign": return s.target === name;
|
|
105
|
+
case "forin": return s.idx === name || bindsNameInStmts(s.body, name);
|
|
106
|
+
case "if": return bindsNameInStmts(s.then, name) || bindsNameInStmts(s.else, name);
|
|
107
|
+
case "while": return bindsNameInStmts(s.body, name);
|
|
108
|
+
case "match": return s.arms.some(a => patternBinds(a.pattern, name) || bindsNameInStmts(a.body, name));
|
|
109
|
+
default: return false;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Every occurrence of `name` a method's out-parameter binder must dodge —
|
|
114
|
+
* referenced in a spec (requires/ensures) or body, *or* bound/targeted anywhere
|
|
115
|
+
* in the body (an unread local still duplicate-declares). Both emitters share it. */
|
|
116
|
+
export function usesNameInDecl(requires, ensures, body, name) {
|
|
117
|
+
return requires.some(e => usesName(e, name)) || ensures.some(e => usesName(e, name))
|
|
118
|
+
|| usesNameInStmts(body, name) || bindsNameInStmts(body, name);
|
|
119
|
+
}
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Lean emitter — IR → Lean text.
|
|
3
3
|
* No logic, no type decisions — just serialization.
|
|
4
4
|
*/
|
|
5
|
-
import { anyExpr } from "./ir.js";
|
|
5
|
+
import { anyExpr, usesNameInDecl, patternBinders } from "./ir.js";
|
|
6
|
+
import { freshName } from "./names.js";
|
|
6
7
|
// ── Ty → Lean type string ──────────────────────────────────
|
|
7
8
|
function tyToLean(ty) {
|
|
8
9
|
switch (ty.kind) {
|
|
@@ -76,11 +77,15 @@ const LEAN_KEYWORDS = new Set([
|
|
|
76
77
|
"partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
|
|
77
78
|
"at", "from", "to", "deriving", "extends", "true", "false",
|
|
78
79
|
]);
|
|
80
|
+
// The return-value identifier for the method currently being emitted. Default
|
|
81
|
+
// `res`, but primed (e.g. `res'`) when a module identifier is named `res` — set
|
|
82
|
+
// by the method case. `\result` in an ensures/body must use the same name.
|
|
83
|
+
let _resultName = "res";
|
|
79
84
|
function escapeName(name) {
|
|
80
85
|
// \result is carried through the IR as the var name "\\result"; render it
|
|
81
|
-
// as
|
|
86
|
+
// as the method's return-value identifier (matches `return (res : T)`).
|
|
82
87
|
if (name === "\\result")
|
|
83
|
-
return
|
|
88
|
+
return _resultName;
|
|
84
89
|
return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
|
|
85
90
|
}
|
|
86
91
|
// ── Operator precedence (for parenthesization) ──────────────
|
|
@@ -173,6 +178,10 @@ let _unknownEmitted = false; // across files in one run — the def file imports
|
|
|
173
178
|
// built only from decidable atoms (comparisons, Bool-returning calls) coerces fine
|
|
174
179
|
// and stays in the more proof-friendly Prop form.
|
|
175
180
|
let _boolCtx = false;
|
|
181
|
+
/** Render a match pattern to Lean syntax: `_`, `.none`, `.some x`, `.syn seq`. */
|
|
182
|
+
function renderLeanPattern(p) {
|
|
183
|
+
return p.kind === "wild" ? "_" : "." + [p.ctor, ...p.binders].join(" ");
|
|
184
|
+
}
|
|
176
185
|
// A Bool-valued atom that does NOT coerce to Prop: an inlined union discriminator
|
|
177
186
|
// (lowered to a match-bool `match x with | .C .. => true | _ => false`) or a raw
|
|
178
187
|
// `match` used as a Bool — neither has a `Decidable` instance Lean can synthesize
|
|
@@ -258,6 +267,8 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
258
267
|
return `${obj}.contains ${args[0]}`;
|
|
259
268
|
if (method === "set")
|
|
260
269
|
return `${obj}.insert ${args[0]} ${args[1]}`;
|
|
270
|
+
if (method === "delete")
|
|
271
|
+
return `${obj}.erase ${args[0]}`;
|
|
261
272
|
}
|
|
262
273
|
// Set methods
|
|
263
274
|
if (tyKind === "set") {
|
|
@@ -458,7 +469,7 @@ function emitExpr(e, parentPrec) {
|
|
|
458
469
|
// Always parenthesize inline matches — Lean parses alternatives greedily,
|
|
459
470
|
// so any token after an arm body (`→`, another match's `|`, etc.) would
|
|
460
471
|
// bleed into the last `.none` case without explicit bracketing.
|
|
461
|
-
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
472
|
+
const arms = e.arms.map(a => `| ${renderLeanPattern(a.pattern)} => ${emitExpr(a.body)}`);
|
|
462
473
|
const scrut = typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee);
|
|
463
474
|
return `(match ${scrut} with ${arms.join(" ")})`;
|
|
464
475
|
}
|
|
@@ -550,10 +561,10 @@ function emitStmt(s, indent) {
|
|
|
550
561
|
const scrut = typeof s.scrutinee === "string" ? s.scrutinee : emitExpr(s.scrutinee);
|
|
551
562
|
// Option match (.some/.none) → emit as if/let for WPGen.if compatibility
|
|
552
563
|
if (s.arms.length === 2) {
|
|
553
|
-
const someArm = s.arms.find(a => a.pattern.
|
|
554
|
-
const noneArm = s.arms.find(a => a.pattern === ".none");
|
|
564
|
+
const someArm = s.arms.find(a => a.pattern.kind === "ctor" && a.pattern.ctor === "some");
|
|
565
|
+
const noneArm = s.arms.find(a => a.pattern.kind === "ctor" && a.pattern.ctor === "none");
|
|
555
566
|
if (someArm && noneArm) {
|
|
556
|
-
const boundVar = someArm.pattern
|
|
567
|
+
const boundVar = patternBinders(someArm.pattern)[0]; // ".some x" ⇒ "x"
|
|
557
568
|
const hName = `h_${scrut.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
558
569
|
const lines = [
|
|
559
570
|
`${pad}if ${hName} : (${scrut}).isSome = true then`,
|
|
@@ -578,7 +589,7 @@ function emitStmt(s, indent) {
|
|
|
578
589
|
// General match
|
|
579
590
|
const lines = [`${pad}match ${scrut} with`];
|
|
580
591
|
for (const arm of s.arms) {
|
|
581
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
592
|
+
lines.push(`${pad}| ${renderLeanPattern(arm.pattern)} =>`);
|
|
582
593
|
if (arm.body.length === 0) {
|
|
583
594
|
lines.push(`${pad} pure ()`);
|
|
584
595
|
}
|
|
@@ -680,7 +691,12 @@ function emitDecl(d) {
|
|
|
680
691
|
// Spec clauses are Prop; the `do` body is computational (Bool).
|
|
681
692
|
const prevBoolCtx = _boolCtx;
|
|
682
693
|
_boolCtx = false;
|
|
683
|
-
|
|
694
|
+
// Prime the return binder only on a collision within *this method's own*
|
|
695
|
+
// signature/body — `res` is a common identifier module-wide (record
|
|
696
|
+
// fields, unrelated params), so a module-wide check would prime spuriously.
|
|
697
|
+
_resultName = freshName("res", n => d.params.some(p => escapeName(p.name) === n) ||
|
|
698
|
+
usesNameInDecl(d.requires, d.ensures, d.body, n));
|
|
699
|
+
const lines = [`method ${d.name} ${params} return (${_resultName} : ${tyToLean(d.returnType)})`];
|
|
684
700
|
for (const r of d.requires)
|
|
685
701
|
lines.push(` require ${emitExpr(r)}`);
|
|
686
702
|
for (const e of d.ensures)
|
|
@@ -732,7 +748,7 @@ function emitPureExpr(e, indent) {
|
|
|
732
748
|
case "match": {
|
|
733
749
|
const lines = [`${pad}match ${typeof e.scrutinee === "string" ? e.scrutinee : emitExpr(e.scrutinee)} with`];
|
|
734
750
|
for (const arm of e.arms) {
|
|
735
|
-
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
751
|
+
lines.push(`${pad}| ${renderLeanPattern(arm.pattern)} =>`);
|
|
736
752
|
lines.push(emitPureExpr(arm.body, indent + 1));
|
|
737
753
|
}
|
|
738
754
|
return lines.join("\n");
|
package/tools/dist/lsc.js
CHANGED
|
@@ -76,7 +76,12 @@ function main() {
|
|
|
76
76
|
const timeLimitIdx = args.findIndex(a => a.startsWith("--time-limit="));
|
|
77
77
|
let timeLimit;
|
|
78
78
|
if (timeLimitIdx >= 0) {
|
|
79
|
-
|
|
79
|
+
const val = args[timeLimitIdx].split("=")[1];
|
|
80
|
+
if (!/^[1-9]\d*$/.test(val)) {
|
|
81
|
+
console.error(`Invalid --time-limit: ${val} (expected seconds as a positive integer)`);
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
timeLimit = parseInt(val);
|
|
80
85
|
args.splice(timeLimitIdx, 1);
|
|
81
86
|
}
|
|
82
87
|
const extraFlagsIdx = args.findIndex(a => a.startsWith("--extra-flags="));
|
|
@@ -93,6 +98,14 @@ function main() {
|
|
|
93
98
|
slow = true;
|
|
94
99
|
args.splice(slowIdx, 1);
|
|
95
100
|
}
|
|
101
|
+
// Anything flag-shaped left over is a typo or a space-separated form
|
|
102
|
+
// (`--backend lean`): reject it rather than let it become a positional arg
|
|
103
|
+
// or be silently ignored (which would e.g. verify with the wrong backend).
|
|
104
|
+
const stray = args.find(a => a.startsWith("-"));
|
|
105
|
+
if (stray) {
|
|
106
|
+
console.error(`Unknown flag: ${stray} (flags take the form --flag=value, e.g. --backend=dafny)`);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
96
109
|
const [cmd, filePath] = args;
|
|
97
110
|
if (!cmd) {
|
|
98
111
|
console.error("Usage: lsc <gen|check|regen|extract|info> [--backend=lean|dafny] <file.ts>");
|
|
@@ -234,7 +247,7 @@ function runFile(cmd, filePath, backend, timeLimit, extraFlags) {
|
|
|
234
247
|
return;
|
|
235
248
|
}
|
|
236
249
|
if (cmd === "regen") {
|
|
237
|
-
dafnyRegen(genPath, dfyPath, basePath, text, dir);
|
|
250
|
+
dafnyRegen(genPath, dfyPath, basePath, text, dir, timeLimit, extraFlags);
|
|
238
251
|
return;
|
|
239
252
|
}
|
|
240
253
|
console.error(`Unknown command: ${cmd}`);
|