lemmascript 0.5.11 → 0.5.13
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 +9 -5
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +7 -4
- package/tools/dist/dafny-emit.js +58 -20
- package/tools/dist/extract.js +6 -2
- package/tools/dist/ir.js +20 -0
- package/tools/dist/lean-emit.js +16 -4
- package/tools/dist/lsc.js +106 -16
- package/tools/dist/names.js +47 -0
- package/tools/dist/narrow.js +11 -9
- package/tools/dist/resolve.js +6 -3
- package/tools/dist/transform.js +69 -15
package/README.md
CHANGED
|
@@ -45,7 +45,7 @@ See the external case studies:
|
|
|
45
45
|
**Install from npm:**
|
|
46
46
|
|
|
47
47
|
```sh
|
|
48
|
-
npm install lemmascript
|
|
48
|
+
npm install -g lemmascript
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
**Or from source:**
|
|
@@ -67,17 +67,21 @@ git clone https://github.com/namin/velvet.git -b lemma ../velvet
|
|
|
67
67
|
### Dafny backend
|
|
68
68
|
|
|
69
69
|
```sh
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
70
|
+
lsc gen --backend=dafny src/myModule.ts
|
|
71
|
+
lsc check --backend=dafny src/myModule.ts
|
|
72
|
+
lsc regen --backend=dafny src/myModule.ts
|
|
73
73
|
```
|
|
74
74
|
|
|
75
|
+
With no file argument, `lsc check` batches over `LemmaScript-files.txt` (one `filepath [timeout] [extra dafny flags…]` per line — the list `tools/check.sh` runs in CI).
|
|
76
|
+
|
|
77
|
+
From a sibling source checkout, the equivalent of `lsc` is `npx tsx ../LemmaScript/tools/src/lsc.ts` — no build step, toolchain edits apply immediately.
|
|
78
|
+
|
|
75
79
|
The Dafny backend generates two files per TS source: `foo.dfy.gen` (always regeneratable) and `foo.dfy` (source of truth, with LLM/user proof additions). The diff between them must be additions-only.
|
|
76
80
|
|
|
77
81
|
### Lean backend
|
|
78
82
|
|
|
79
83
|
```sh
|
|
80
|
-
|
|
84
|
+
lsc gen --backend=lean src/myModule.ts
|
|
81
85
|
lake build
|
|
82
86
|
```
|
|
83
87
|
|
package/package.json
CHANGED
|
@@ -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 } 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) {
|
|
@@ -59,12 +84,19 @@ function escapeName(name) {
|
|
|
59
84
|
// as the current method's out-parameter name.
|
|
60
85
|
if (name === "\\result")
|
|
61
86
|
return _resultName;
|
|
87
|
+
let out = name;
|
|
62
88
|
if (DAFNY_KEYWORDS.has(name))
|
|
63
|
-
|
|
89
|
+
out = `${name}_`;
|
|
64
90
|
// Dafny doesn't allow identifiers starting with _
|
|
65
|
-
if (name.startsWith("_"))
|
|
66
|
-
|
|
67
|
-
|
|
91
|
+
else if (name.startsWith("_"))
|
|
92
|
+
out = `i${name}`;
|
|
93
|
+
else
|
|
94
|
+
return name;
|
|
95
|
+
// Mangling must stay injective: the mangled form may itself be a name the
|
|
96
|
+
// user wrote (`match` → `match_` beside a real `match_`, `_x` → `i_x` beside
|
|
97
|
+
// a real `i_x`), silently merging two distinct variables. `freshName` primes
|
|
98
|
+
// it clear of user-name space (a prime can't occur in a TS identifier).
|
|
99
|
+
return freshName(out);
|
|
68
100
|
}
|
|
69
101
|
/** Format a typed parameter list for Dafny: "x: int, y: seq<int>" */
|
|
70
102
|
function paramList(params) {
|
|
@@ -73,17 +105,18 @@ function paramList(params) {
|
|
|
73
105
|
/** Format a method signature header, omitting `returns` for void methods.
|
|
74
106
|
* Dafny's definite-assignment rule rejects unassigned out-parameters, so a
|
|
75
107
|
* `returns (res: ())` on a void method fails verification. */
|
|
76
|
-
function methodHeader(prefix, params, returnType) {
|
|
108
|
+
function methodHeader(prefix, params, returnType, scope) {
|
|
77
109
|
const sig = `${prefix}(${paramList(params)})`;
|
|
78
110
|
if (returnType.kind === "void")
|
|
79
111
|
return sig;
|
|
80
|
-
// The out-parameter is `res` by default, but a
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
112
|
+
// The out-parameter is `res` by default, but a param (an Express handler's
|
|
113
|
+
// `(req, res)`), body local, or callee named `res` would shadow it. Check only
|
|
114
|
+
// *this method's own* signature and body — `res` is common module-wide (fields,
|
|
115
|
+
// unrelated params), so a module-wide check would prime spuriously. The primed
|
|
116
|
+
// name is recorded so `\result` references resolve to it.
|
|
117
|
+
const taken = (n) => params.some(p => escapeName(p.name) === n) ||
|
|
118
|
+
(scope !== undefined && usesNameInDecl(scope.requires, scope.ensures, scope.body, n));
|
|
119
|
+
const resName = freshName("res", taken);
|
|
87
120
|
_resultName = resName;
|
|
88
121
|
return `${sig} returns (${resName}: ${tyToDafny(returnType)})`;
|
|
89
122
|
}
|
|
@@ -222,8 +255,8 @@ function emitExpr(e) {
|
|
|
222
255
|
const ret = lam.body[0];
|
|
223
256
|
if (ret.kind !== "return")
|
|
224
257
|
throw new Error("unreachable");
|
|
225
|
-
const p =
|
|
226
|
-
const body = emitExpr(
|
|
258
|
+
const { binder: p, body: v } = comprehensionBinder(lam, ret.value, e.obj);
|
|
259
|
+
const body = emitExpr(v);
|
|
227
260
|
return `(exists ${p} :: ${p} in ${obj} && ${body})`;
|
|
228
261
|
}
|
|
229
262
|
}
|
|
@@ -300,8 +333,13 @@ function emitExpr(e) {
|
|
|
300
333
|
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
301
334
|
if (e.method === "has")
|
|
302
335
|
return `(${args[0]} in ${obj})`;
|
|
303
|
-
if (e.method === "delete")
|
|
304
|
-
|
|
336
|
+
if (e.method === "delete") {
|
|
337
|
+
// Minted comprehension binder: freshen so a user variable `k` in the
|
|
338
|
+
// receiver or key isn't captured (`k != k` would delete nothing).
|
|
339
|
+
// Local check — only this comprehension's own operands can collide.
|
|
340
|
+
const k = freshBinder("k", e.obj, e.args[0]);
|
|
341
|
+
return `(map ${k} | ${k} in ${obj} && ${k} != ${args[0]} :: ${obj}[${k}])`;
|
|
342
|
+
}
|
|
305
343
|
}
|
|
306
344
|
// Set methods
|
|
307
345
|
if (ty === "set") {
|
|
@@ -321,8 +359,8 @@ function emitExpr(e) {
|
|
|
321
359
|
const ret = lam.body[0];
|
|
322
360
|
if (ret.kind !== "return")
|
|
323
361
|
throw new Error("unreachable");
|
|
324
|
-
const p =
|
|
325
|
-
const body = emitExpr(
|
|
362
|
+
const { binder: p, body: v } = comprehensionBinder(lam, ret.value, e.obj);
|
|
363
|
+
const body = emitExpr(v);
|
|
326
364
|
return `(set ${p} | ${p} in ${obj} && ${body})`;
|
|
327
365
|
}
|
|
328
366
|
}
|
|
@@ -736,7 +774,7 @@ function emitDecl(d) {
|
|
|
736
774
|
}
|
|
737
775
|
case "method": {
|
|
738
776
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
739
|
-
const lines = [methodHeader(`method ${d.name}${tp}`, d.params, d.returnType)];
|
|
777
|
+
const lines = [methodHeader(`method ${d.name}${tp}`, d.params, d.returnType, d)];
|
|
740
778
|
for (const r of d.requires)
|
|
741
779
|
lines.push(` requires ${emitExpr(r)}`);
|
|
742
780
|
for (const e of d.ensures)
|
|
@@ -756,7 +794,7 @@ function emitDecl(d) {
|
|
|
756
794
|
if (d.fields.length > 0 && d.methods.length > 0)
|
|
757
795
|
lines.push("");
|
|
758
796
|
for (const m of d.methods) {
|
|
759
|
-
lines.push(` ${methodHeader(`method ${m.name}`, m.params, m.returnType)}`);
|
|
797
|
+
lines.push(` ${methodHeader(`method ${m.name}`, m.params, m.returnType, m)}`);
|
|
760
798
|
for (const r of m.requires)
|
|
761
799
|
lines.push(` requires ${emitExpr(r)}`);
|
|
762
800
|
for (const e of m.ensures)
|
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
|
@@ -60,3 +60,23 @@ export function anyExprInStmt(s, pred) {
|
|
|
60
60
|
export function anyExprInStmts(stmts, pred) {
|
|
61
61
|
return stmts.some(s => anyExprInStmt(s, pred));
|
|
62
62
|
}
|
|
63
|
+
// A name is "used" — such that a synthesized binder of the same name would
|
|
64
|
+
// capture or shadow it — iff it appears as a variable reference or a called
|
|
65
|
+
// function. These drive the *local* freshness checks for user-facing binders
|
|
66
|
+
// (the result out-parameter, comprehension binders): a binder is checked only
|
|
67
|
+
// against the expressions/scope it actually wraps, not the whole module.
|
|
68
|
+
const _refsName = (name) => e => (e.kind === "var" && e.name === name) ||
|
|
69
|
+
(e.kind === "app" && e.fn === name) ||
|
|
70
|
+
(e.kind === "constructor" && e.name === name) ||
|
|
71
|
+
(e.kind === "match" && typeof e.scrutinee === "string" && e.scrutinee === name);
|
|
72
|
+
export function usesName(e, name) {
|
|
73
|
+
return anyExpr(e, _refsName(name));
|
|
74
|
+
}
|
|
75
|
+
export function usesNameInStmts(stmts, name) {
|
|
76
|
+
return anyExprInStmts(stmts, _refsName(name));
|
|
77
|
+
}
|
|
78
|
+
/** Does a declaration's spec (requires/ensures) or body reference `name`? The
|
|
79
|
+
* scope a method's out-parameter binder must dodge, shared by both emitters. */
|
|
80
|
+
export function usesNameInDecl(requires, ensures, body, name) {
|
|
81
|
+
return requires.some(e => usesName(e, name)) || ensures.some(e => usesName(e, name)) || usesNameInStmts(body, name);
|
|
82
|
+
}
|
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 } 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) ──────────────
|
|
@@ -258,6 +263,8 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
|
|
|
258
263
|
return `${obj}.contains ${args[0]}`;
|
|
259
264
|
if (method === "set")
|
|
260
265
|
return `${obj}.insert ${args[0]} ${args[1]}`;
|
|
266
|
+
if (method === "delete")
|
|
267
|
+
return `${obj}.erase ${args[0]}`;
|
|
261
268
|
}
|
|
262
269
|
// Set methods
|
|
263
270
|
if (tyKind === "set") {
|
|
@@ -680,7 +687,12 @@ function emitDecl(d) {
|
|
|
680
687
|
// Spec clauses are Prop; the `do` body is computational (Bool).
|
|
681
688
|
const prevBoolCtx = _boolCtx;
|
|
682
689
|
_boolCtx = false;
|
|
683
|
-
|
|
690
|
+
// Prime the return binder only on a collision within *this method's own*
|
|
691
|
+
// signature/body — `res` is a common identifier module-wide (record
|
|
692
|
+
// fields, unrelated params), so a module-wide check would prime spuriously.
|
|
693
|
+
_resultName = freshName("res", n => d.params.some(p => escapeName(p.name) === n) ||
|
|
694
|
+
usesNameInDecl(d.requires, d.ensures, d.body, n));
|
|
695
|
+
const lines = [`method ${d.name} ${params} return (${_resultName} : ${tyToLean(d.returnType)})`];
|
|
684
696
|
for (const r of d.requires)
|
|
685
697
|
lines.push(` require ${emitExpr(r)}`);
|
|
686
698
|
for (const e of d.ensures)
|
package/tools/dist/lsc.js
CHANGED
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
* Pipeline: extract → resolve → narrow → transform → peephole → emit
|
|
6
6
|
*/
|
|
7
7
|
import { Project, ScriptTarget } from "ts-morph";
|
|
8
|
-
import { existsSync } from "fs";
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
import { execFileSync } from "child_process";
|
|
10
|
+
import { createRequire } from "module";
|
|
9
11
|
import path from "path";
|
|
10
12
|
import { extractModule } from "./extract.js";
|
|
11
13
|
import { resolveModule } from "./resolve.js";
|
|
@@ -20,20 +22,44 @@ import { leanGen, leanCheck } from "./lean-commands.js";
|
|
|
20
22
|
import { runInfo } from "./info-command.js";
|
|
21
23
|
function main() {
|
|
22
24
|
const args = process.argv.slice(2);
|
|
23
|
-
// `lsc claimcheck …` forwards verbatim to the lemmascript-claimcheck
|
|
24
|
-
// (a dependency; its cli reads the rewritten process.argv).
|
|
25
|
+
// `lsc claimcheck <file.ts> …` forwards verbatim to the lemmascript-claimcheck
|
|
26
|
+
// CLI (a dependency; its cli reads the rewritten process.argv). With no
|
|
27
|
+
// leading <file.ts>, batch: one satellite run per LemmaScript-files.txt entry,
|
|
28
|
+
// flags passed through unchanged — the loop is owned here, the satellite
|
|
29
|
+
// stays single-file.
|
|
25
30
|
if (args[0] === "claimcheck") {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
const rest = args.slice(1);
|
|
32
|
+
const missing = () => {
|
|
33
|
+
console.error("`lsc claimcheck` needs lemmascript-claimcheck >= 0.2.0; reinstall with: npm i -g lemmascript");
|
|
34
|
+
process.exit(1);
|
|
35
|
+
};
|
|
36
|
+
if (rest[0] && !rest[0].startsWith("-")) {
|
|
37
|
+
process.argv = [process.argv[0], "lemmascript-claimcheck", ...rest];
|
|
38
|
+
import("lemmascript-claimcheck/cli").catch((err) => {
|
|
39
|
+
const code = err?.code;
|
|
40
|
+
if (code === "ERR_MODULE_NOT_FOUND" || code === "ERR_PACKAGE_PATH_NOT_EXPORTED")
|
|
41
|
+
missing();
|
|
33
42
|
console.error(err instanceof Error ? err.message : String(err));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
});
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let cli;
|
|
48
|
+
try {
|
|
49
|
+
cli = createRequire(import.meta.url).resolve("lemmascript-claimcheck/cli");
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
missing();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const e of readEntries()) {
|
|
56
|
+
try {
|
|
57
|
+
execFileSync(process.execPath, [cli, e.file, ...rest], { stdio: "inherit" });
|
|
34
58
|
}
|
|
35
|
-
|
|
36
|
-
|
|
59
|
+
catch {
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
37
63
|
return;
|
|
38
64
|
}
|
|
39
65
|
const backendIdx = args.findIndex(a => a.startsWith("--backend="));
|
|
@@ -50,7 +76,12 @@ function main() {
|
|
|
50
76
|
const timeLimitIdx = args.findIndex(a => a.startsWith("--time-limit="));
|
|
51
77
|
let timeLimit;
|
|
52
78
|
if (timeLimitIdx >= 0) {
|
|
53
|
-
|
|
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);
|
|
54
85
|
args.splice(timeLimitIdx, 1);
|
|
55
86
|
}
|
|
56
87
|
const extraFlagsIdx = args.findIndex(a => a.startsWith("--extra-flags="));
|
|
@@ -59,12 +90,71 @@ function main() {
|
|
|
59
90
|
extraFlags = args[extraFlagsIdx].split("=").slice(1).join("=");
|
|
60
91
|
args.splice(extraFlagsIdx, 1);
|
|
61
92
|
}
|
|
93
|
+
// --slow (batch mode only): verify every entry with its own timeout instead
|
|
94
|
+
// of degrading slow ones to gen-check.
|
|
95
|
+
let slow = false;
|
|
96
|
+
const slowIdx = args.indexOf("--slow");
|
|
97
|
+
if (slowIdx >= 0) {
|
|
98
|
+
slow = true;
|
|
99
|
+
args.splice(slowIdx, 1);
|
|
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
|
+
}
|
|
62
109
|
const [cmd, filePath] = args;
|
|
63
|
-
if (!cmd
|
|
110
|
+
if (!cmd) {
|
|
64
111
|
console.error("Usage: lsc <gen|check|regen|extract|info> [--backend=lean|dafny] <file.ts>");
|
|
65
|
-
console.error(" lsc
|
|
112
|
+
console.error(" lsc <gen|gen-check|check> [--backend=…] [--slow] (no file: batch over LemmaScript-files.txt)");
|
|
113
|
+
console.error(" lsc claimcheck [<file.ts>] [flags…] (forwards to lemmascript-claimcheck)");
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
if (!filePath) {
|
|
117
|
+
runBatch(cmd, backend, slow);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
runFile(cmd, filePath, backend, timeLimit, extraFlags);
|
|
121
|
+
}
|
|
122
|
+
// LemmaScript-files.txt, parsed: `filepath [timeout_in_seconds] [extra dafny
|
|
123
|
+
// flags…]` per line; no timeout = Dafny default. Exits if the file is absent.
|
|
124
|
+
function readEntries() {
|
|
125
|
+
if (!existsSync("LemmaScript-files.txt")) {
|
|
126
|
+
console.error("No file given and no LemmaScript-files.txt found.");
|
|
66
127
|
process.exit(1);
|
|
67
128
|
}
|
|
129
|
+
return readFileSync("LemmaScript-files.txt", "utf8")
|
|
130
|
+
.split("\n").map(s => s.trim()).filter(Boolean)
|
|
131
|
+
.map(entry => {
|
|
132
|
+
const [file, second, ...rest] = entry.split(/\s+/);
|
|
133
|
+
const timeout = second && /^[1-9]\d*$/.test(second) ? parseInt(second) : undefined;
|
|
134
|
+
const flags = (timeout === undefined ? [second, ...rest] : rest).filter(Boolean).join(" ") || undefined;
|
|
135
|
+
return { file, timeout, flags };
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Batch over LemmaScript-files.txt. `check` entries with a timeout above 60s
|
|
139
|
+
// (the CI limit) are gen-check only, unless --slow. Fail-fast: the first
|
|
140
|
+
// failing entry exits. tools/check.sh drives this from source;
|
|
141
|
+
// installed-package consumers run `lsc check`.
|
|
142
|
+
function runBatch(cmd, backend, slow) {
|
|
143
|
+
if (cmd !== "gen" && cmd !== "gen-check" && cmd !== "check") {
|
|
144
|
+
console.error(`No file given, and batch mode supports gen|gen-check|check (not ${cmd}).`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
for (const e of readEntries()) {
|
|
148
|
+
if (cmd === "check" && backend === "dafny" && !slow && e.timeout !== undefined && e.timeout > 60) {
|
|
149
|
+
console.log(`=== ${path.basename(e.file)} (timeout ${e.timeout}s > 60s, gen-check only) ===`);
|
|
150
|
+
runFile("gen-check", e.file, backend, undefined, undefined);
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
runFile(cmd, e.file, backend, e.timeout, e.flags);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
function runFile(cmd, filePath, backend, timeLimit, extraFlags) {
|
|
68
158
|
const absPath = path.resolve(filePath);
|
|
69
159
|
if (!existsSync(absPath)) {
|
|
70
160
|
console.error(`File not found: ${absPath}`);
|
|
@@ -157,7 +247,7 @@ function main() {
|
|
|
157
247
|
return;
|
|
158
248
|
}
|
|
159
249
|
if (cmd === "regen") {
|
|
160
|
-
dafnyRegen(genPath, dfyPath, basePath, text, dir);
|
|
250
|
+
dafnyRegen(genPath, dfyPath, basePath, text, dir, timeLimit, extraFlags);
|
|
161
251
|
return;
|
|
162
252
|
}
|
|
163
253
|
console.error(`Unknown command: ${cmd}`);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fresh names for toolchain-minted identifiers.
|
|
3
|
+
*
|
|
4
|
+
* Several passes synthesize names — loop counters (`_x_idx`), lift temps
|
|
5
|
+
* (`_t0`), someMatch binders (`_task_val`), quantifier binders. Any such name
|
|
6
|
+
* can collide with a user-written identifier and either capture it (silently
|
|
7
|
+
* changing semantics — the `.delete()` binder bug) or shadow it (a
|
|
8
|
+
* loud duplicate-variable error in the backend).
|
|
9
|
+
*
|
|
10
|
+
* The rule, chosen for zero churn to existing output: a minted name is used
|
|
11
|
+
* verbatim unless the user wrote the same identifier somewhere in the module,
|
|
12
|
+
* in which case a prime (`'`) is appended. TypeScript identifiers cannot
|
|
13
|
+
* contain a prime, while Dafny and Lean both accept them, so one prime always
|
|
14
|
+
* suffices; and priming preserves distinctness among minted names, so the
|
|
15
|
+
* existing counter/suffix schemes keep working unchanged.
|
|
16
|
+
*
|
|
17
|
+
* The module-wide check deliberately over-approximates scope — a colliding
|
|
18
|
+
* name anywhere in the file primes the mint. False positives only affect
|
|
19
|
+
* internal names nobody reads. Names with a user-facing meaning stay exact by
|
|
20
|
+
* different, local means: comprehension binders check the expressions they
|
|
21
|
+
* actually wrap (`usesName` in dafny-emit), and result binders check the
|
|
22
|
+
* signature/body in hand (`methodHeader`). Spec text (`//@` comments) is NOT
|
|
23
|
+
* scanned: specs may legitimately reference minted names (e.g. `_x_idx` loop
|
|
24
|
+
* counters in invariants), so a spec mention is a reference, not a collision
|
|
25
|
+
* — when a source collision does force a prime, spec references are expected
|
|
26
|
+
* to follow it.
|
|
27
|
+
*/
|
|
28
|
+
let _userNames = new Set();
|
|
29
|
+
/** Seeded once per module by extract with every Identifier token in the
|
|
30
|
+
* source — params, locals, fields, callees alike. */
|
|
31
|
+
export function setUserNames(names) {
|
|
32
|
+
_userNames = names;
|
|
33
|
+
}
|
|
34
|
+
export function isUserName(name) {
|
|
35
|
+
return _userNames.has(name);
|
|
36
|
+
}
|
|
37
|
+
/** A toolchain-internal name: `base` verbatim, primed on collision. The one
|
|
38
|
+
* place the priming rule lives. `taken` says what counts as a collision —
|
|
39
|
+
* by default a user-written name anywhere in the module; callers that know
|
|
40
|
+
* the exact scope (e.g. a comprehension binder checking only the expressions
|
|
41
|
+
* it wraps) pass their own predicate. */
|
|
42
|
+
export function freshName(base, taken = isUserName) {
|
|
43
|
+
let name = base;
|
|
44
|
+
while (taken(name))
|
|
45
|
+
name += "'";
|
|
46
|
+
return name;
|
|
47
|
+
}
|
package/tools/dist/narrow.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* (early-return, let-cond) run in `walkStmts` so they can consume the rest
|
|
35
35
|
* of the block.
|
|
36
36
|
*/
|
|
37
|
+
import { freshName } from "./names.js";
|
|
37
38
|
// ── Optional-check detection ────────────────────────────────
|
|
38
39
|
/** Counter for naming optChain binders. Reset per module. */
|
|
39
40
|
let _ocCounter = 0;
|
|
@@ -52,7 +53,7 @@ function parseOptionalCheck(cond) {
|
|
|
52
53
|
const hint = binderHintFor(e);
|
|
53
54
|
if (hint === null)
|
|
54
55
|
return null;
|
|
55
|
-
return { scrutinee: e, innerTy, negated: true, binderHint: hint, truthiness: true };
|
|
56
|
+
return { scrutinee: e, innerTy, negated: true, binderHint: freshName(hint), truthiness: true };
|
|
56
57
|
}
|
|
57
58
|
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
|
|
58
59
|
// Bare optional truthiness: `if (e)` where e: T | undefined — true iff e is
|
|
@@ -61,7 +62,7 @@ function parseOptionalCheck(cond) {
|
|
|
61
62
|
const hint = binderHintFor(cond);
|
|
62
63
|
if (hint === null)
|
|
63
64
|
return null;
|
|
64
|
-
return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: hint, truthiness: true };
|
|
65
|
+
return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: freshName(hint), truthiness: true };
|
|
65
66
|
}
|
|
66
67
|
return null;
|
|
67
68
|
}
|
|
@@ -77,7 +78,7 @@ function parseOptionalCheck(cond) {
|
|
|
77
78
|
const hint = binderHintFor(e);
|
|
78
79
|
if (hint === null)
|
|
79
80
|
return null;
|
|
80
|
-
return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: hint, truthiness: false };
|
|
81
|
+
return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: freshName(hint), truthiness: false };
|
|
81
82
|
}
|
|
82
83
|
function binderHintFor(e) {
|
|
83
84
|
// Pure access paths: var(x) or field(purePath, name).
|
|
@@ -425,7 +426,7 @@ function ruleNullish(e) {
|
|
|
425
426
|
if (e.left.ty.kind !== "optional")
|
|
426
427
|
return null;
|
|
427
428
|
const innerTy = e.left.ty.inner;
|
|
428
|
-
const binder = `_oc${_ocCounter++}_val
|
|
429
|
+
const binder = freshName(`_oc${_ocCounter++}_val`);
|
|
429
430
|
return {
|
|
430
431
|
kind: "someMatch",
|
|
431
432
|
scrutinee: e.left, binder, binderTy: innerTy,
|
|
@@ -499,7 +500,7 @@ function ruleOptChain(e) {
|
|
|
499
500
|
if (e.obj.ty.kind !== "optional")
|
|
500
501
|
return null;
|
|
501
502
|
const innerTy = e.obj.ty.inner;
|
|
502
|
-
const binder = `_oc${_ocCounter++}_val
|
|
503
|
+
const binder = freshName(`_oc${_ocCounter++}_val`);
|
|
503
504
|
let body = { kind: "var", name: binder, ty: innerTy };
|
|
504
505
|
for (const step of e.chain) {
|
|
505
506
|
if (step.kind === "field") {
|
|
@@ -543,9 +544,9 @@ function binderHintForMapAccess(m, k) {
|
|
|
543
544
|
// mHint is `_m_val`, kHint is `_k_val` — stitch into `_m_k_val`.
|
|
544
545
|
const mStem = mHint.replace(/_val$/, "");
|
|
545
546
|
const kStem = kHint.replace(/^_/, "").replace(/_val$/, "");
|
|
546
|
-
return `${mStem}_${kStem}_val
|
|
547
|
+
return freshName(`${mStem}_${kStem}_val`);
|
|
547
548
|
}
|
|
548
|
-
return `_oc${_ocCounter++}_val
|
|
549
|
+
return freshName(`_oc${_ocCounter++}_val`);
|
|
549
550
|
}
|
|
550
551
|
/** Rule (expression): `k in m ? m[k] : default` where m is map-typed.
|
|
551
552
|
* The then-branch must be exactly `m[k]` (same obj, same key). This mirrors
|
|
@@ -592,9 +593,10 @@ function ruleConditionalOptionalTruthy(e) {
|
|
|
592
593
|
return null;
|
|
593
594
|
if (e.cond.ty.kind !== "optional")
|
|
594
595
|
return null;
|
|
595
|
-
const
|
|
596
|
-
if (
|
|
596
|
+
const hint = binderHintFor(e.cond);
|
|
597
|
+
if (hint === null)
|
|
597
598
|
return null;
|
|
599
|
+
const binder = freshName(hint);
|
|
598
600
|
return {
|
|
599
601
|
kind: "someMatch",
|
|
600
602
|
scrutinee: e.cond, binderTy: e.cond.ty.inner,
|
package/tools/dist/resolve.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { isBigInt } from "./typedir.js";
|
|
8
8
|
import { parseTsType, tyToCanonical } from "./types.js";
|
|
9
9
|
import { parseExpr } from "./specparser.js";
|
|
10
|
+
import { freshName } from "./names.js";
|
|
10
11
|
function lookup(env, name) {
|
|
11
12
|
if (!env)
|
|
12
13
|
return undefined;
|
|
@@ -693,7 +694,7 @@ function resolveRecordMerge(base, override, ctx) {
|
|
|
693
694
|
return { name: f.name, value: ovf }; // required: override always provides
|
|
694
695
|
// optional: override field wins iff present, else base's field
|
|
695
696
|
const bvf = { kind: "field", obj: bv, field: f.name, ty: ft };
|
|
696
|
-
const binder = `_m${mergeBinder++}
|
|
697
|
+
const binder = freshName(`_m${mergeBinder++}`);
|
|
697
698
|
// someBody is the unwrapped present value; transform re-wraps each arm in
|
|
698
699
|
// the backend's Some constructor (Dafny `Some`, Lean `Option.some`).
|
|
699
700
|
return { name: f.name, value: {
|
|
@@ -704,7 +705,7 @@ function resolveRecordMerge(base, override, ctx) {
|
|
|
704
705
|
});
|
|
705
706
|
if (tover.ty.kind === "optional") {
|
|
706
707
|
// override may be absent (undefined spreads nothing) → base unchanged
|
|
707
|
-
const binder = `_mo${mergeBinder++}
|
|
708
|
+
const binder = freshName(`_mo${mergeBinder++}`);
|
|
708
709
|
return {
|
|
709
710
|
kind: "someMatch", scrutinee: tover, binder, binderTy: userTy,
|
|
710
711
|
someBody: merged(tbase, { kind: "var", name: binder, ty: userTy }), noneBody: tbase, ty: userTy,
|
|
@@ -1373,7 +1374,9 @@ function resolveStmt(s, ctx) {
|
|
|
1373
1374
|
for (const _ of s.names)
|
|
1374
1375
|
nameTypes.push({ kind: "unknown" });
|
|
1375
1376
|
}
|
|
1376
|
-
|
|
1377
|
+
// Must match the freshened counter transform mints for this loop, so a
|
|
1378
|
+
// spec referencing the loop index resolves to the same name.
|
|
1379
|
+
const idxName = freshName(`_${s.names[0]}_idx`);
|
|
1377
1380
|
env = extend(env, idxName, { kind: "nat" });
|
|
1378
1381
|
for (let j = 0; j < s.names.length; j++) {
|
|
1379
1382
|
env = extend(env, s.names[j], nameTypes[j] ?? { kind: "unknown" });
|
package/tools/dist/transform.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { anyExprInStmts } from "./ir.js";
|
|
8
8
|
import { parseTsType } from "./types.js";
|
|
9
|
+
import { freshName } from "./names.js";
|
|
9
10
|
// ── Generic IR walkers ──────────────────────────────────────
|
|
10
11
|
/**
|
|
11
12
|
* Map over all sub-expressions in an Expr. `f` is called on each node;
|
|
@@ -73,6 +74,54 @@ function mapStmt(s, f) {
|
|
|
73
74
|
case "assert": return { ...s, expr: r(s.expr) };
|
|
74
75
|
}
|
|
75
76
|
}
|
|
77
|
+
/** Binders a match-arm pattern string (`.Ctor a b`, `_x_val`, `_`) introduces. */
|
|
78
|
+
function patternBinders(pattern) {
|
|
79
|
+
const toks = pattern.match(/[A-Za-z_][A-Za-z0-9_']*/g) ?? [];
|
|
80
|
+
return pattern.trimStart().startsWith(".") ? toks.slice(1) : toks;
|
|
81
|
+
}
|
|
82
|
+
/** Rename free occurrences of `from` to `to`, stopping at every construct that
|
|
83
|
+
* rebinds `from` — lambda params, `let`/`let-bind`/`ghostLet` (shadows the
|
|
84
|
+
* rest of the block), `match` arm patterns, `forall`/`exists`, and `for-in`
|
|
85
|
+
* indices. Capture-avoiding: a nested scope that reintroduces `from` keeps its
|
|
86
|
+
* own binding untouched. `mapExpr` doesn't descend into lambda bodies, so this
|
|
87
|
+
* walks them by hand. */
|
|
88
|
+
export function renameFreeVar(e, from, to) {
|
|
89
|
+
const f = (x) => {
|
|
90
|
+
if (x.kind === "var")
|
|
91
|
+
return x.name === from ? { ...x, name: to } : x;
|
|
92
|
+
// let-expression: `value` is in the outer scope (rename), `body` sees the
|
|
93
|
+
// rebound `from` (leave it), so handle the recursion here to stop descent.
|
|
94
|
+
if (x.kind === "let" && x.name === from)
|
|
95
|
+
return { ...x, value: mapExpr(x.value, f) };
|
|
96
|
+
if ((x.kind === "forall" || x.kind === "exists") && x.var === from)
|
|
97
|
+
return x;
|
|
98
|
+
if (x.kind === "match") {
|
|
99
|
+
const scr = typeof x.scrutinee === "string"
|
|
100
|
+
? (x.scrutinee === from ? to : x.scrutinee) : mapExpr(x.scrutinee, f);
|
|
101
|
+
return { ...x, scrutinee: scr, arms: x.arms.map(a => patternBinders(a.pattern).includes(from) ? a : { ...a, body: mapExpr(a.body, f) }) };
|
|
102
|
+
}
|
|
103
|
+
if (x.kind === "lambda") {
|
|
104
|
+
if (x.params.some(p => p.name === from))
|
|
105
|
+
return x; // param shadows `from`
|
|
106
|
+
const body = [];
|
|
107
|
+
let shadowed = false;
|
|
108
|
+
for (const s of x.body) {
|
|
109
|
+
if (shadowed) {
|
|
110
|
+
body.push(s);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
body.push(s.kind === "forin" && s.idx === from
|
|
114
|
+
? { ...s, bound: mapExpr(s.bound, f) } // idx shadows in the loop body
|
|
115
|
+
: mapStmt(s, f));
|
|
116
|
+
if ((s.kind === "let" || s.kind === "let-bind" || s.kind === "ghostLet") && s.name === from)
|
|
117
|
+
shadowed = true;
|
|
118
|
+
}
|
|
119
|
+
return { ...x, body };
|
|
120
|
+
}
|
|
121
|
+
return null;
|
|
122
|
+
};
|
|
123
|
+
return mapExpr(e, f);
|
|
124
|
+
}
|
|
76
125
|
/** Map over all sub-expressions in a TExpr (typed IR). */
|
|
77
126
|
function mapTExpr(e, f) {
|
|
78
127
|
const hit = f(e);
|
|
@@ -145,9 +194,12 @@ let _opts = DAFNY_OPTIONS;
|
|
|
145
194
|
let _typeDecls = [];
|
|
146
195
|
/** Prefix match-bound field names to avoid capturing user variables.
|
|
147
196
|
* When prefix is given (the scrutinee name), include it to avoid
|
|
148
|
-
* collisions in nested matches on different variables.
|
|
197
|
+
* collisions in nested matches on different variables. `freshName` closes
|
|
198
|
+
* the residual gap: a user variable literally named `_value`/`_x_field` in an
|
|
199
|
+
* arm body would still be captured, so prime on any module-wide collision.
|
|
200
|
+
* Deterministic, so the pattern binder and its body substitutions agree. */
|
|
149
201
|
function matchBinder(fieldName, prefix) {
|
|
150
|
-
return prefix ? `_${prefix}_${fieldName}` : `_${fieldName}
|
|
202
|
+
return freshName(prefix ? `_${prefix}_${fieldName}` : `_${fieldName}`);
|
|
151
203
|
}
|
|
152
204
|
/** Build a match arm pattern like `.VariantName _v_field1 _v_field2` from variant info. */
|
|
153
205
|
function buildMatchPattern(variantName, fields, scopePrefix) {
|
|
@@ -351,7 +403,7 @@ function lowerExpr(e, binds) {
|
|
|
351
403
|
// callKind "unknown" and fall through to the regular case below where
|
|
352
404
|
// they become `methodCall`.
|
|
353
405
|
if (binds && e.kind === "call" && e.callKind === "method" && e.fn.kind === "var") {
|
|
354
|
-
const name = `_t${_liftCounter++}
|
|
406
|
+
const name = freshName(`_t${_liftCounter++}`);
|
|
355
407
|
const args = e.args.map(a => lowerExpr(a, binds));
|
|
356
408
|
binds.push({ kind: "let-bind", name, value: { kind: "app", fn: e.fn.name, args } });
|
|
357
409
|
return { kind: "var", name };
|
|
@@ -751,7 +803,7 @@ function lowerExpr(e, binds) {
|
|
|
751
803
|
const result = { kind: "methodCall", obj: recv, objTy: e.fn.obj.ty, method, args, monadic: needsMonadic };
|
|
752
804
|
// Monadic HOF call is itself monadic — lift via binds like a method call
|
|
753
805
|
if (_opts.monadic && needsMonadic && binds) {
|
|
754
|
-
const name = `_t${_liftCounter++}
|
|
806
|
+
const name = freshName(`_t${_liftCounter++}`);
|
|
755
807
|
binds.push({ kind: "let-bind", name, value: result });
|
|
756
808
|
return { kind: "var", name };
|
|
757
809
|
}
|
|
@@ -911,7 +963,7 @@ function lowerExpr(e, binds) {
|
|
|
911
963
|
case "havoc":
|
|
912
964
|
// Dafny's * only works in var/assign positions — lift to own declaration
|
|
913
965
|
if (binds) {
|
|
914
|
-
const name = `_t${_liftCounter++}
|
|
966
|
+
const name = freshName(`_t${_liftCounter++}`);
|
|
915
967
|
binds.push({ kind: "let", name, type: e.ty, mutable: false, value: { kind: "havoc", type: e.ty } });
|
|
916
968
|
return { kind: "var", name };
|
|
917
969
|
}
|
|
@@ -1357,11 +1409,11 @@ function transformStmts(stmts, typeDecls) {
|
|
|
1357
1409
|
const count = _forofCounters.get(keyName) ?? 0;
|
|
1358
1410
|
_forofCounters.set(keyName, count + 1);
|
|
1359
1411
|
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
1360
|
-
const keysSeqName = `_${keyName}_keys${suffix}
|
|
1412
|
+
const keysSeqName = freshName(`_${keyName}_keys${suffix}`);
|
|
1361
1413
|
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
1362
1414
|
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
1363
1415
|
const keysVar = { kind: "var", name: keysSeqName };
|
|
1364
|
-
const idxName = `_${keyName}_idx${suffix}
|
|
1416
|
+
const idxName = freshName(`_${keyName}_idx${suffix}`);
|
|
1365
1417
|
const idx = { kind: "var", name: idxName };
|
|
1366
1418
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
1367
1419
|
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
@@ -1383,11 +1435,11 @@ function transformStmts(stmts, typeDecls) {
|
|
|
1383
1435
|
const count = _forofCounters.get(keyName) ?? 0;
|
|
1384
1436
|
_forofCounters.set(keyName, count + 1);
|
|
1385
1437
|
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
1386
|
-
const keysSeqName = `_${keyName}_keys${suffix}
|
|
1438
|
+
const keysSeqName = freshName(`_${keyName}_keys${suffix}`);
|
|
1387
1439
|
const convExpr = { kind: "app", fn: "SetToSeq", args: [{ kind: "field", obj: iterExpr, field: "keys" }] };
|
|
1388
1440
|
result.push({ kind: "let", name: keysSeqName, type: { kind: "array", elem: keyTy }, mutable: false, value: convExpr });
|
|
1389
1441
|
const keysVar = { kind: "var", name: keysSeqName };
|
|
1390
|
-
const idxName = `_${keyName}_idx${suffix}
|
|
1442
|
+
const idxName = freshName(`_${keyName}_idx${suffix}`);
|
|
1391
1443
|
const idx = { kind: "var", name: idxName };
|
|
1392
1444
|
const arrSize = { kind: "field", obj: keysVar, field: "size" };
|
|
1393
1445
|
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
@@ -1405,7 +1457,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
1405
1457
|
}
|
|
1406
1458
|
// Sets aren't indexable — bind SetToSeq to a variable for iteration
|
|
1407
1459
|
if (s.iterable.ty.kind === "set") {
|
|
1408
|
-
const seqName = `_${varName}_seq
|
|
1460
|
+
const seqName = freshName(`_${varName}_seq`);
|
|
1409
1461
|
const convExpr = { kind: "app", fn: "SetToSeq", args: [iterExpr] };
|
|
1410
1462
|
const elemTy = varTy.kind !== "unknown" ? varTy : { kind: "string" };
|
|
1411
1463
|
result.push({ kind: "let", name: seqName, type: { kind: "array", elem: elemTy }, mutable: false, value: convExpr });
|
|
@@ -1414,7 +1466,7 @@ function transformStmts(stmts, typeDecls) {
|
|
|
1414
1466
|
const count = _forofCounters.get(varName) ?? 0;
|
|
1415
1467
|
_forofCounters.set(varName, count + 1);
|
|
1416
1468
|
const suffix = count === 0 ? "" : `${count + 1}`;
|
|
1417
|
-
const idxName = `_${varName}_idx${suffix}
|
|
1469
|
+
const idxName = freshName(`_${varName}_idx${suffix}`);
|
|
1418
1470
|
const idx = { kind: "var", name: idxName };
|
|
1419
1471
|
const arrSize = { kind: "field", obj: iterExpr, field: "size" };
|
|
1420
1472
|
const bodyStmts = eliminateTopLevelContinue(transformStmts(s.body, typeDecls));
|
|
@@ -1489,15 +1541,17 @@ function transformStmt(s, typeDecls) {
|
|
|
1489
1541
|
const arrIR = transformExpr(arrExpr);
|
|
1490
1542
|
const arrTy = arrExpr.ty;
|
|
1491
1543
|
const elemTy = arrTy.kind === "array" ? arrTy.elem : { kind: "unknown" };
|
|
1492
|
-
const idxName = `_${param}_idx
|
|
1544
|
+
const idxName = freshName(`_${param}_idx`);
|
|
1493
1545
|
const idx = { kind: "var", name: idxName };
|
|
1494
1546
|
const arrSize = { kind: "field", obj: arrIR, field: "size" };
|
|
1495
1547
|
const elemVar = { kind: "var", name: param };
|
|
1496
1548
|
const keyIR = transformExpr(keyExpr);
|
|
1497
1549
|
const valIR = transformExpr(valExpr);
|
|
1498
1550
|
const mapSet = { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "set", args: [keyIR, valIR], monadic: false };
|
|
1499
|
-
// Auto-invariant: all processed elements' keys are in the map
|
|
1500
|
-
|
|
1551
|
+
// Auto-invariant: all processed elements' keys are in the map. The
|
|
1552
|
+
// quantifier wraps user expressions, so its binder must be fresh.
|
|
1553
|
+
const kiName = freshName("ki");
|
|
1554
|
+
const kVar = { kind: "var", name: kiName };
|
|
1501
1555
|
const mapHasKey = {
|
|
1502
1556
|
kind: "implies",
|
|
1503
1557
|
premises: [
|
|
@@ -1506,7 +1560,7 @@ function transformStmt(s, typeDecls) {
|
|
|
1506
1560
|
],
|
|
1507
1561
|
conclusion: { kind: "methodCall", obj: { kind: "var", name: s.name }, objTy: s.ty, method: "has", args: [keyIR.kind === "field" ? { kind: "field", obj: { kind: "index", arr: arrIR, idx: kVar }, field: keyIR.field } : keyIR], monadic: false },
|
|
1508
1562
|
};
|
|
1509
|
-
const autoInv = { kind: "forall", var:
|
|
1563
|
+
const autoInv = { kind: "forall", var: kiName, type: { kind: "int" }, body: mapHasKey };
|
|
1510
1564
|
const stmts = [
|
|
1511
1565
|
{ kind: "let", name: s.name, type: s.ty, mutable: true, value: { kind: "emptyMap" } },
|
|
1512
1566
|
{ kind: "forin", idx: idxName, bound: arrSize,
|