lemmascript 0.2.0 → 0.3.1
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 +3 -2
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +4 -3
- package/tools/dist/dafny-emit.js +206 -161
- package/tools/dist/extract.js +483 -51
- package/tools/dist/lean-emit.js +6 -2
- package/tools/dist/lsc.js +28 -5
- package/tools/dist/resolve.js +353 -113
- package/tools/dist/specparser.js +5 -2
- package/tools/dist/transform.js +452 -117
- package/tools/dist/types.js +14 -1
package/README.md
CHANGED
|
@@ -16,8 +16,9 @@ See the external case studies:
|
|
|
16
16
|
- **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
|
|
17
17
|
- **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
|
|
18
18
|
- **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
|
|
19
|
-
- **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. Cookie verification done in-place. Dafny only.
|
|
20
|
-
- **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** —
|
|
19
|
+
- **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Two CVEs verified: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas. [Cookie verification done **in-place**](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/src/utils/cookie.ts#L79). Dafny only.
|
|
20
|
+
- **[charmchat](https://github.com/CHARM-BDF/charmchat/tree/lemma)** — brownfield verification of an AI agent orchestration backend. `isEmptyResult` (string emptiness predicate, 8 postconditions, <1s) and `topologicalSort` (Kahn's algorithm — memory safety, output bounds, completeness via acyclicity ranking witness, termination; 5 helper lemmas, 28 loop invariants). Dafny only.
|
|
21
|
+
- **[xyflow-lemmascript](https://github.com/midspiral/xyflow-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [xyflow](https://github.com/xyflow/xyflow)'s core edge and geometry utilities. 9 functions verified: `addEdge` (dedup — never loses edges, adds at most one), `reconnectEdge` (replace — bounded length), `connectionExists`, `getEdgeCenter` (midpoint correctness), `clamp` (bounds), `rectToBox`/`boxToRect` (field arithmetic), `getBoundsOfBoxes` (enclosure), `getOverlappingArea` (non-negative), `areSetsEqual` (subset + same size). 14 Dafny proof obligations. Dafny only.
|
|
21
22
|
|
|
22
23
|
## Setup
|
|
23
24
|
|
package/package.json
CHANGED
|
@@ -31,13 +31,14 @@ export function dafnyCheckDiff(genPath, dfyPath) {
|
|
|
31
31
|
catch { /* diff not available */ }
|
|
32
32
|
return true;
|
|
33
33
|
}
|
|
34
|
-
export function dafnyVerify(dfyPath, dir, timeLimit) {
|
|
34
|
+
export function dafnyVerify(dfyPath, dir, timeLimit, extraFlags) {
|
|
35
35
|
console.log("Running dafny verify...");
|
|
36
36
|
try {
|
|
37
37
|
const content = readFileSync(dfyPath, "utf-8");
|
|
38
|
-
const stdLibFlag = content.includes("
|
|
38
|
+
const stdLibFlag = content.includes("Std.") ? " --standard-libraries" : "";
|
|
39
39
|
const timeLimitFlag = timeLimit ? ` --verification-time-limit ${timeLimit}` : "";
|
|
40
|
-
|
|
40
|
+
const extra = extraFlags ? ` ${extraFlags}` : "";
|
|
41
|
+
execSync(`dafny verify${stdLibFlag}${timeLimitFlag}${extra} "${dfyPath}"`, { cwd: dir, stdio: "inherit" });
|
|
41
42
|
return true;
|
|
42
43
|
}
|
|
43
44
|
catch {
|
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -14,7 +14,7 @@ function tyToDafny(ty) {
|
|
|
14
14
|
case "map": return `map<${tyToDafny(ty.key)}, ${tyToDafny(ty.value)}>`;
|
|
15
15
|
case "set": return `set<${tyToDafny(ty.elem)}>`;
|
|
16
16
|
case "optional": {
|
|
17
|
-
|
|
17
|
+
needPreamble("OptionType");
|
|
18
18
|
return `Option<${tyToDafny(ty.inner)}>`;
|
|
19
19
|
}
|
|
20
20
|
case "user": return ty.name;
|
|
@@ -51,12 +51,29 @@ function paramList(params) {
|
|
|
51
51
|
const OP_MAP = {
|
|
52
52
|
"=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
|
|
53
53
|
"∧": "&&", "∨": "||", "¬": "!",
|
|
54
|
+
"arrayConcat": "+",
|
|
54
55
|
};
|
|
55
56
|
function mapOp(op) { return OP_MAP[op] ?? op; }
|
|
56
57
|
// ── Expression emission ─────────────────────────────────────
|
|
58
|
+
/** Emit a match scrutinee — either a variable name (string) or an expression. */
|
|
59
|
+
function emitScrutinee(s) {
|
|
60
|
+
return typeof s === "string" ? escapeName(s) : emitExpr(s);
|
|
61
|
+
}
|
|
62
|
+
/** Collapse nested forall/exists into a single quantifier with multiple bound vars. */
|
|
63
|
+
function emitQuantifier(e, keyword) {
|
|
64
|
+
const vars = [];
|
|
65
|
+
let body = e;
|
|
66
|
+
while (body.kind === e.kind) {
|
|
67
|
+
const dty = tyToDafny(body.type);
|
|
68
|
+
const ann = dty === "string" ? "" : `: ${dty}`;
|
|
69
|
+
vars.push(`${body.var}${ann}`);
|
|
70
|
+
body = body.body;
|
|
71
|
+
}
|
|
72
|
+
return `${keyword} ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
73
|
+
}
|
|
57
74
|
function emitExpr(e) {
|
|
58
75
|
switch (e.kind) {
|
|
59
|
-
case "var": return escapeName(e.name);
|
|
76
|
+
case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
|
|
60
77
|
case "num": return `${e.value}`;
|
|
61
78
|
case "bool": return e.value ? "true" : "false";
|
|
62
79
|
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
@@ -79,20 +96,18 @@ function emitExpr(e) {
|
|
|
79
96
|
return `(${args[0]} in ${obj})`;
|
|
80
97
|
if (e.method === "push")
|
|
81
98
|
return `(${obj} + [${args[0]}])`;
|
|
82
|
-
if (e.method === "
|
|
99
|
+
if (e.method === "concat")
|
|
100
|
+
return `(${obj} + [${args[0]}])`;
|
|
101
|
+
if (e.method === "slice" && args.length === 1)
|
|
83
102
|
return `${obj}[${args[0]}..]`;
|
|
84
|
-
if (e.method === "
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (e.method === "filter")
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
if (e.method === "every") {
|
|
93
|
-
needsStdCollections = true;
|
|
94
|
-
return `Seq.All(${obj}, ${args[0]})`;
|
|
95
|
-
}
|
|
103
|
+
if (e.method === "slice" && args.length === 2)
|
|
104
|
+
return `${obj}[${args[0]}..${args[1]}]`;
|
|
105
|
+
if (e.method === "map")
|
|
106
|
+
return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
|
|
107
|
+
if (e.method === "filter")
|
|
108
|
+
return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
|
|
109
|
+
if (e.method === "every")
|
|
110
|
+
return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
|
|
96
111
|
if (e.method === "some" && e.args[0].kind === "lambda" &&
|
|
97
112
|
e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
|
|
98
113
|
const lam = e.args[0];
|
|
@@ -107,25 +122,25 @@ function emitExpr(e) {
|
|
|
107
122
|
// String methods
|
|
108
123
|
if (ty === "string") {
|
|
109
124
|
if (e.method === "indexOf") {
|
|
110
|
-
|
|
125
|
+
needPreamble("StringIndexOf");
|
|
111
126
|
return `StringIndexOf(${obj}, ${args[0]})`;
|
|
112
127
|
}
|
|
113
128
|
if (e.method === "slice")
|
|
114
129
|
return `${obj}[${args[0]}..${args[1]}]`;
|
|
115
130
|
if (e.method === "trim") {
|
|
116
|
-
|
|
131
|
+
needPreamble("StringTrim");
|
|
117
132
|
return `StringTrim(${obj})`;
|
|
118
133
|
}
|
|
119
134
|
if (e.method === "toLowerCase") {
|
|
120
|
-
|
|
135
|
+
needPreamble("StringToLower");
|
|
121
136
|
return `StringToLower(${obj})`;
|
|
122
137
|
}
|
|
123
138
|
if (e.method === "toUpperCase") {
|
|
124
|
-
|
|
139
|
+
needPreamble("StringToUpper");
|
|
125
140
|
return `StringToUpper(${obj})`;
|
|
126
141
|
}
|
|
127
142
|
if (e.method === "includes") {
|
|
128
|
-
|
|
143
|
+
needPreamble("StringIndexOf");
|
|
129
144
|
return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
|
|
130
145
|
}
|
|
131
146
|
if (e.method === "charCodeAt")
|
|
@@ -136,13 +151,15 @@ function emitExpr(e) {
|
|
|
136
151
|
if (e.method === "getDirect")
|
|
137
152
|
return `${obj}[${args[0]}]`;
|
|
138
153
|
if (e.method === "get") {
|
|
139
|
-
|
|
154
|
+
needPreamble("OptionType");
|
|
140
155
|
return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
|
|
141
156
|
}
|
|
142
157
|
if (e.method === "set")
|
|
143
158
|
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
144
159
|
if (e.method === "has")
|
|
145
160
|
return `(${args[0]} in ${obj})`;
|
|
161
|
+
if (e.method === "delete")
|
|
162
|
+
return `(map k | k in ${obj} && k != ${args[0]} :: ${obj}[k])`;
|
|
146
163
|
}
|
|
147
164
|
// Set methods
|
|
148
165
|
if (ty === "set") {
|
|
@@ -186,14 +203,14 @@ function emitExpr(e) {
|
|
|
186
203
|
if (e.right.kind === "num") {
|
|
187
204
|
return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
|
|
188
205
|
}
|
|
189
|
-
|
|
206
|
+
needPreamble("Pow2");
|
|
190
207
|
return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
|
|
191
208
|
}
|
|
192
209
|
if (e.op === "<<") {
|
|
193
210
|
if (e.right.kind === "num") {
|
|
194
211
|
return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
|
|
195
212
|
}
|
|
196
|
-
|
|
213
|
+
needPreamble("Pow2");
|
|
197
214
|
return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
|
|
198
215
|
}
|
|
199
216
|
// x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
|
|
@@ -205,7 +222,7 @@ function emitExpr(e) {
|
|
|
205
222
|
return `(${emitExpr(e.left)} % ${modulus})`;
|
|
206
223
|
}
|
|
207
224
|
}
|
|
208
|
-
|
|
225
|
+
needPreamble("BitAnd");
|
|
209
226
|
return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
|
|
210
227
|
}
|
|
211
228
|
// int * real coercion: wrap int side with "as real"
|
|
@@ -227,18 +244,29 @@ function emitExpr(e) {
|
|
|
227
244
|
case "app": {
|
|
228
245
|
const args = e.args.map(emitExpr);
|
|
229
246
|
if (e.fn === "SetToSeq") {
|
|
230
|
-
|
|
247
|
+
needPreamble("SetToSeq");
|
|
231
248
|
return `SetToSeq(${args.join(", ")})`;
|
|
232
249
|
}
|
|
233
250
|
if (e.fn === "BigInt" || e.fn === "Number")
|
|
234
251
|
return args[0]; // identity: both map to int
|
|
252
|
+
// Set literal: {a, b, c}
|
|
253
|
+
if (e.fn === "SetLiteral")
|
|
254
|
+
return `{${args.join(", ")}}`;
|
|
235
255
|
if (e.fn === "JSFloorDiv")
|
|
236
|
-
|
|
256
|
+
needPreamble("JSFloorDiv");
|
|
237
257
|
if (e.fn === "CeilReal")
|
|
238
|
-
|
|
258
|
+
needPreamble("CeilReal");
|
|
239
259
|
if (e.fn === "FloorReal")
|
|
240
|
-
|
|
241
|
-
|
|
260
|
+
needPreamble("FloorReal");
|
|
261
|
+
if (e.fn === "NatToString")
|
|
262
|
+
needPreamble("NatToString");
|
|
263
|
+
if (e.fn === "MathAbs")
|
|
264
|
+
needPreamble("MathAbs");
|
|
265
|
+
if (e.fn === "MathMin")
|
|
266
|
+
needPreamble("MathMin");
|
|
267
|
+
if (e.fn === "MathMax")
|
|
268
|
+
needPreamble("MathMax");
|
|
269
|
+
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
242
270
|
}
|
|
243
271
|
case "field": {
|
|
244
272
|
const obj = emitExpr(e.obj);
|
|
@@ -260,43 +288,51 @@ function emitExpr(e) {
|
|
|
260
288
|
const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
261
289
|
return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
|
|
262
290
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
if (
|
|
291
|
+
// Match constructor by field names — prefer exact match over first-field heuristic
|
|
292
|
+
let ctorName;
|
|
293
|
+
if (e.fields.length > 0) {
|
|
294
|
+
const fieldNames = new Set(e.fields.map(f => f.name));
|
|
295
|
+
for (const [name, fields] of _structureDecls) {
|
|
296
|
+
if (fields.length >= e.fields.length && fields.every(f => fieldNames.has(f.name) || f.type.kind === "optional")) {
|
|
297
|
+
ctorName = name;
|
|
298
|
+
break;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
if (!ctorName)
|
|
302
|
+
ctorName = _recordCtors.get(e.fields[0].name);
|
|
303
|
+
}
|
|
304
|
+
if (ctorName) {
|
|
305
|
+
const structFields = _structureDecls.get(ctorName);
|
|
306
|
+
if (structFields && e.fields.length < structFields.length) {
|
|
307
|
+
// Pad missing fields: match by name, fill None for optional
|
|
308
|
+
const provided = new Map(e.fields.map(f => [f.name, f]));
|
|
309
|
+
const vals = structFields.map(sf => {
|
|
310
|
+
const f = provided.get(sf.name);
|
|
311
|
+
if (f)
|
|
312
|
+
return emitExpr(f.value);
|
|
313
|
+
if (sf.type.kind === "optional") {
|
|
314
|
+
needPreamble("OptionType");
|
|
315
|
+
return "None";
|
|
316
|
+
}
|
|
317
|
+
return `/* missing: ${sf.name} */`;
|
|
318
|
+
});
|
|
319
|
+
return `${ctorName}(${vals.join(", ")})`;
|
|
320
|
+
}
|
|
321
|
+
const vals = e.fields.map(f => emitExpr(f.value));
|
|
266
322
|
return `${ctorName}(${vals.join(", ")})`;
|
|
323
|
+
}
|
|
324
|
+
const vals = e.fields.map(f => emitExpr(f.value));
|
|
267
325
|
return `(${vals.join(", ")})`;
|
|
268
326
|
}
|
|
269
327
|
case "if":
|
|
270
328
|
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
271
329
|
case "match": {
|
|
272
|
-
const scrut =
|
|
330
|
+
const scrut = emitScrutinee(e.scrutinee);
|
|
273
331
|
const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
|
|
274
332
|
return `(match ${scrut} { ${arms.join(" ")} })`;
|
|
275
333
|
}
|
|
276
|
-
case "forall":
|
|
277
|
-
|
|
278
|
-
const vars = [];
|
|
279
|
-
let body = e;
|
|
280
|
-
while (body.kind === "forall") {
|
|
281
|
-
const dty = tyToDafny(body.type);
|
|
282
|
-
const ann = dty === "string" ? "" : `: ${dty}`;
|
|
283
|
-
vars.push(`${body.var}${ann}`);
|
|
284
|
-
body = body.body;
|
|
285
|
-
}
|
|
286
|
-
return `forall ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
287
|
-
}
|
|
288
|
-
case "exists": {
|
|
289
|
-
// Collapse nested exists: exists x :: exists y :: P → exists x, y :: P
|
|
290
|
-
const vars = [];
|
|
291
|
-
let body = e;
|
|
292
|
-
while (body.kind === "exists") {
|
|
293
|
-
const dty = tyToDafny(body.type);
|
|
294
|
-
const ann = dty === "string" ? "" : `: ${dty}`;
|
|
295
|
-
vars.push(`${body.var}${ann}`);
|
|
296
|
-
body = body.body;
|
|
297
|
-
}
|
|
298
|
-
return `exists ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
299
|
-
}
|
|
334
|
+
case "forall": return emitQuantifier(e, "forall");
|
|
335
|
+
case "exists": return emitQuantifier(e, "exists");
|
|
300
336
|
case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
|
|
301
337
|
case "havoc": return "*";
|
|
302
338
|
}
|
|
@@ -308,7 +344,7 @@ function emitPureExpr(e, indent) {
|
|
|
308
344
|
case "if":
|
|
309
345
|
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
310
346
|
case "match": {
|
|
311
|
-
const scrut =
|
|
347
|
+
const scrut = emitScrutinee(e.scrutinee);
|
|
312
348
|
const lines = [`${pad}match ${scrut} {`];
|
|
313
349
|
for (const arm of e.arms) {
|
|
314
350
|
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
@@ -331,7 +367,13 @@ function emitStmt(s, indent) {
|
|
|
331
367
|
const pad = " ".repeat(indent);
|
|
332
368
|
switch (s.kind) {
|
|
333
369
|
case "let":
|
|
334
|
-
|
|
370
|
+
// Record literal assigned to map type → emit as map[k := v, ...]
|
|
371
|
+
if (s.type.kind === "map" && s.value.kind === "record" && !s.value.spread) {
|
|
372
|
+
const entries = s.value.fields.map(f => `${emitExpr({ kind: "str", value: f.name })} := ${emitExpr(f.value)}`);
|
|
373
|
+
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(resolveTy(s.type))} := map[${entries.join(", ")}];`;
|
|
374
|
+
}
|
|
375
|
+
if (s.value.kind === "havoc" || s.value.kind === "emptyMap" || s.value.kind === "emptySet" ||
|
|
376
|
+
(s.value.kind === "arrayLiteral" && s.value.elems.length === 0))
|
|
335
377
|
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
336
378
|
return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
|
|
337
379
|
case "assign":
|
|
@@ -367,7 +409,7 @@ function emitStmt(s, indent) {
|
|
|
367
409
|
return out;
|
|
368
410
|
}
|
|
369
411
|
case "match": {
|
|
370
|
-
const scrut =
|
|
412
|
+
const scrut = emitScrutinee(s.scrutinee);
|
|
371
413
|
const lines = [`${pad}match ${scrut} {`];
|
|
372
414
|
for (const arm of s.arms) {
|
|
373
415
|
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
@@ -408,18 +450,23 @@ function emitStmt(s, indent) {
|
|
|
408
450
|
function emitDecl(d) {
|
|
409
451
|
switch (d.kind) {
|
|
410
452
|
case "inductive": {
|
|
453
|
+
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
411
454
|
const ctors = d.constructors.map(c => {
|
|
412
455
|
if (c.fields.length === 0)
|
|
413
456
|
return escapeName(c.name);
|
|
414
457
|
return `${escapeName(c.name)}(${paramList(c.fields)})`;
|
|
415
458
|
});
|
|
416
|
-
return `datatype ${d.name} = ${ctors.join(" | ")}`;
|
|
459
|
+
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
417
460
|
}
|
|
418
461
|
case "structure": {
|
|
419
462
|
return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
|
|
420
463
|
}
|
|
464
|
+
case "type-alias": {
|
|
465
|
+
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
466
|
+
}
|
|
421
467
|
case "def": {
|
|
422
|
-
const
|
|
468
|
+
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
469
|
+
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
423
470
|
for (const r of d.requires)
|
|
424
471
|
lines.push(` requires ${emitExpr(r)}`);
|
|
425
472
|
lines.push(`{`);
|
|
@@ -427,8 +474,10 @@ function emitDecl(d) {
|
|
|
427
474
|
lines.push(`}`);
|
|
428
475
|
// Companion lemma for ensures (proof target for LLM)
|
|
429
476
|
if (d.ensures.length > 0) {
|
|
477
|
+
// Strip constraints like (==) from type params — ghost lemmas don't need them
|
|
478
|
+
const lemmaTP = d.typeParams.length > 0 ? `<${d.typeParams.map(t => t.replace(/\(.*\)/, '')).join(", ")}>` : "";
|
|
430
479
|
lines.push("");
|
|
431
|
-
lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
|
|
480
|
+
lines.push(`lemma ${d.name}_ensures${lemmaTP}(${paramList(d.params)})`);
|
|
432
481
|
for (const r of d.requires)
|
|
433
482
|
lines.push(` requires ${emitExpr(r)}`);
|
|
434
483
|
for (const e of d.ensures)
|
|
@@ -439,7 +488,8 @@ function emitDecl(d) {
|
|
|
439
488
|
return lines.join("\n");
|
|
440
489
|
}
|
|
441
490
|
case "method": {
|
|
442
|
-
const
|
|
491
|
+
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
492
|
+
const lines = [`method ${d.name}${tp}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
|
|
443
493
|
for (const r of d.requires)
|
|
444
494
|
lines.push(` requires ${emitExpr(r)}`);
|
|
445
495
|
for (const e of d.ensures)
|
|
@@ -480,18 +530,9 @@ function emitDecl(d) {
|
|
|
480
530
|
}
|
|
481
531
|
// ── File emission ───────────────────────────────────────────
|
|
482
532
|
// ── Preamble helpers ────────────────────────────────────────
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
let needsStringToUpper = false;
|
|
487
|
-
let needsJSFloorDiv = false;
|
|
488
|
-
let needsCeilReal = false;
|
|
489
|
-
let needsFloorReal = false;
|
|
490
|
-
let needsStdCollections = false;
|
|
491
|
-
let needsOptionType = false;
|
|
492
|
-
let needsSetToSeq = false;
|
|
493
|
-
let needsBitAnd = false;
|
|
494
|
-
let needsPow2 = false;
|
|
533
|
+
/** Preamble tracking — emitters add keys via `needPreamble(key)`, emitDafnyFile emits them. */
|
|
534
|
+
const _neededPreambles = new Set();
|
|
535
|
+
function needPreamble(key) { _neededPreambles.add(key); }
|
|
495
536
|
const POW2 = `function Pow2(n: int): int
|
|
496
537
|
requires n >= 0
|
|
497
538
|
decreases n
|
|
@@ -579,25 +620,99 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
|
|
|
579
620
|
var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
|
|
580
621
|
[upper] + StringToUpper(s[1..])
|
|
581
622
|
}`;
|
|
623
|
+
const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
|
|
624
|
+
const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
|
|
625
|
+
const NAT_TO_STRING = `function NatToString(n: nat): string
|
|
626
|
+
decreases n
|
|
627
|
+
{
|
|
628
|
+
var digit := ('0' as int + n % 10) as char;
|
|
629
|
+
if n < 10 then [digit]
|
|
630
|
+
else NatToString(n / 10) + [digit]
|
|
631
|
+
}`;
|
|
632
|
+
const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
|
|
633
|
+
const SET_TO_SEQ = `method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
|
|
634
|
+
ensures forall x :: x in s <==> x in res
|
|
635
|
+
ensures |res| == |s|
|
|
636
|
+
{
|
|
637
|
+
var remaining := s;
|
|
638
|
+
res := [];
|
|
639
|
+
while remaining != {}
|
|
640
|
+
invariant remaining <= s
|
|
641
|
+
invariant forall x :: x in res <==> (x in s && x !in remaining)
|
|
642
|
+
invariant |res| + |remaining| == |s|
|
|
643
|
+
decreases remaining
|
|
644
|
+
{
|
|
645
|
+
var x :| x in remaining;
|
|
646
|
+
res := res + [x];
|
|
647
|
+
remaining := remaining - {x};
|
|
648
|
+
}
|
|
649
|
+
}`;
|
|
650
|
+
/** Preamble code keyed by name. Emitted in this order when needed. */
|
|
651
|
+
const PREAMBLE_CODE = [
|
|
652
|
+
["OptionType", "datatype Option<T> = None | Some(value: T)"],
|
|
653
|
+
["SetToSeq", SET_TO_SEQ],
|
|
654
|
+
["Pow2", POW2],
|
|
655
|
+
["BitAnd", BIT_AND],
|
|
656
|
+
["JSFloorDiv", JS_FLOOR_DIV],
|
|
657
|
+
["CeilReal", CEIL_REAL],
|
|
658
|
+
["FloorReal", FLOOR_REAL],
|
|
659
|
+
["StringIndexOf", STRING_INDEX_OF],
|
|
660
|
+
["StringTrim", STRING_TRIM],
|
|
661
|
+
["StringToLower", STRING_TO_LOWER],
|
|
662
|
+
["StringToUpper", STRING_TO_UPPER],
|
|
663
|
+
["NatToString", NAT_TO_STRING],
|
|
664
|
+
["MathAbs", MATH_ABS],
|
|
665
|
+
["MathMin", MATH_MIN],
|
|
666
|
+
["MathMax", MATH_MAX],
|
|
667
|
+
];
|
|
582
668
|
// ── Constructor and record helpers ───────────────────────────
|
|
583
669
|
let _recordCtors = new Map();
|
|
670
|
+
let _structureDecls = new Map();
|
|
671
|
+
let _declaredTypes = new Set();
|
|
584
672
|
function buildRecordCtorMap(decls) {
|
|
585
673
|
_recordCtors = new Map();
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
674
|
+
_structureDecls = new Map();
|
|
675
|
+
_declaredTypes = new Set();
|
|
676
|
+
function collectDecl(d) {
|
|
677
|
+
if (d.kind === "structure") {
|
|
678
|
+
_declaredTypes.add(d.name);
|
|
679
|
+
_structureDecls.set(d.name, d.fields);
|
|
680
|
+
if (d.fields.length > 0)
|
|
681
|
+
_recordCtors.set(d.fields[0].name, d.name);
|
|
682
|
+
}
|
|
683
|
+
if (d.kind === "inductive")
|
|
684
|
+
_declaredTypes.add(d.name);
|
|
685
|
+
if (d.kind === "type-alias")
|
|
686
|
+
_declaredTypes.add(d.name);
|
|
687
|
+
if (d.kind === "def")
|
|
688
|
+
_declaredTypes.add(d.name);
|
|
589
689
|
if (d.kind === "namespace")
|
|
590
|
-
for (const inner of d.decls)
|
|
591
|
-
|
|
592
|
-
_recordCtors.set(inner.fields[0].name, inner.name);
|
|
593
|
-
}
|
|
690
|
+
for (const inner of d.decls)
|
|
691
|
+
collectDecl(inner);
|
|
594
692
|
}
|
|
693
|
+
for (const d of decls)
|
|
694
|
+
collectDecl(d);
|
|
695
|
+
}
|
|
696
|
+
/** Resolve a Ty to a Dafny-safe type, falling back to string for undeclared user types. */
|
|
697
|
+
function resolveTy(ty) {
|
|
698
|
+
if (ty.kind === "user" && !_declaredTypes.has(ty.name))
|
|
699
|
+
return { kind: "string" };
|
|
700
|
+
if (ty.kind === "optional")
|
|
701
|
+
return { kind: "optional", inner: resolveTy(ty.inner) };
|
|
702
|
+
if (ty.kind === "array")
|
|
703
|
+
return { kind: "array", elem: resolveTy(ty.elem) };
|
|
704
|
+
if (ty.kind === "map")
|
|
705
|
+
return { kind: "map", key: resolveTy(ty.key), value: resolveTy(ty.value) };
|
|
706
|
+
if (ty.kind === "set")
|
|
707
|
+
return { kind: "set", elem: resolveTy(ty.elem) };
|
|
708
|
+
return ty;
|
|
595
709
|
}
|
|
596
710
|
function qualifyCtor(name, type) {
|
|
597
711
|
const rawName = name.replace(/^\./, "");
|
|
712
|
+
const mapped = CTOR_MAP[rawName] ?? escapeName(rawName);
|
|
598
713
|
if (type)
|
|
599
|
-
return `${type}.${
|
|
600
|
-
return
|
|
714
|
+
return `${type}.${mapped}`;
|
|
715
|
+
return mapped;
|
|
601
716
|
}
|
|
602
717
|
/** Translate a Lean match pattern to Dafny syntax.
|
|
603
718
|
* ".ctorName field1 field2" → "ctorName(field1, field2)"
|
|
@@ -618,23 +733,9 @@ function translatePattern(pattern) {
|
|
|
618
733
|
const fieldNames = fields.split(/\s+/).map(escapeName);
|
|
619
734
|
return `${ctorName}(${fieldNames.join(", ")})`;
|
|
620
735
|
}
|
|
621
|
-
const PREAMBLES = {
|
|
622
|
-
StringIndexOf: STRING_INDEX_OF,
|
|
623
|
-
};
|
|
624
736
|
export function emitDafnyFile(file, tsFileName) {
|
|
625
737
|
buildRecordCtorMap(file.decls);
|
|
626
|
-
|
|
627
|
-
needsStringTrim = false;
|
|
628
|
-
needsStringToLower = false;
|
|
629
|
-
needsStringToUpper = false;
|
|
630
|
-
needsJSFloorDiv = false;
|
|
631
|
-
needsCeilReal = false;
|
|
632
|
-
needsFloorReal = false;
|
|
633
|
-
needsStdCollections = false;
|
|
634
|
-
needsOptionType = false;
|
|
635
|
-
needsSetToSeq = false;
|
|
636
|
-
needsBitAnd = false;
|
|
637
|
-
needsPow2 = false;
|
|
738
|
+
_neededPreambles.clear();
|
|
638
739
|
// Collect pure def names so we can skip their method wrappers
|
|
639
740
|
const pureDefs = new Set();
|
|
640
741
|
for (const d of file.decls) {
|
|
@@ -671,67 +772,11 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
671
772
|
const lines = [];
|
|
672
773
|
if (tsFileName)
|
|
673
774
|
lines.push(`// Generated by lsc from ${tsFileName}`);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
}
|
|
680
|
-
if (needsSetToSeq) {
|
|
681
|
-
lines.push("");
|
|
682
|
-
lines.push(`method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
|
|
683
|
-
ensures forall x :: x in s <==> x in res
|
|
684
|
-
ensures |res| == |s|
|
|
685
|
-
{
|
|
686
|
-
var remaining := s;
|
|
687
|
-
res := [];
|
|
688
|
-
while remaining != {}
|
|
689
|
-
invariant remaining <= s
|
|
690
|
-
invariant forall x :: x in res <==> (x in s && x !in remaining)
|
|
691
|
-
invariant |res| + |remaining| == |s|
|
|
692
|
-
decreases remaining
|
|
693
|
-
{
|
|
694
|
-
var x :| x in remaining;
|
|
695
|
-
res := res + [x];
|
|
696
|
-
remaining := remaining - {x};
|
|
697
|
-
}
|
|
698
|
-
}`);
|
|
699
|
-
}
|
|
700
|
-
if (needsPow2) {
|
|
701
|
-
lines.push("");
|
|
702
|
-
lines.push(POW2);
|
|
703
|
-
}
|
|
704
|
-
if (needsBitAnd) {
|
|
705
|
-
lines.push("");
|
|
706
|
-
lines.push(BIT_AND);
|
|
707
|
-
}
|
|
708
|
-
if (needsJSFloorDiv) {
|
|
709
|
-
lines.push("");
|
|
710
|
-
lines.push(JS_FLOOR_DIV);
|
|
711
|
-
}
|
|
712
|
-
if (needsCeilReal) {
|
|
713
|
-
lines.push("");
|
|
714
|
-
lines.push(CEIL_REAL);
|
|
715
|
-
}
|
|
716
|
-
if (needsFloorReal) {
|
|
717
|
-
lines.push("");
|
|
718
|
-
lines.push(FLOOR_REAL);
|
|
719
|
-
}
|
|
720
|
-
if (needsStringIndexOf) {
|
|
721
|
-
lines.push("");
|
|
722
|
-
lines.push(PREAMBLES.StringIndexOf);
|
|
723
|
-
}
|
|
724
|
-
if (needsStringTrim) {
|
|
725
|
-
lines.push("");
|
|
726
|
-
lines.push(STRING_TRIM);
|
|
727
|
-
}
|
|
728
|
-
if (needsStringToLower) {
|
|
729
|
-
lines.push("");
|
|
730
|
-
lines.push(STRING_TO_LOWER);
|
|
731
|
-
}
|
|
732
|
-
if (needsStringToUpper) {
|
|
733
|
-
lines.push("");
|
|
734
|
-
lines.push(STRING_TO_UPPER);
|
|
775
|
+
for (const [key, code] of PREAMBLE_CODE) {
|
|
776
|
+
if (_neededPreambles.has(key)) {
|
|
777
|
+
lines.push("");
|
|
778
|
+
lines.push(code);
|
|
779
|
+
}
|
|
735
780
|
}
|
|
736
781
|
lines.push(...declLines);
|
|
737
782
|
return lines.join("\n") + "\n";
|