lemmascript 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +94 -3
- package/package.json +30 -20
- package/tools/dist/dafny-commands.js +98 -0
- package/tools/dist/dafny-emit.js +738 -0
- package/tools/dist/emit.js +253 -0
- package/tools/dist/extract.js +806 -0
- package/tools/dist/ir.js +7 -0
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +119 -0
- package/tools/dist/rawir.js +10 -0
- package/tools/dist/resolve.js +717 -0
- package/tools/dist/specparser.js +305 -0
- package/tools/dist/transform.js +1091 -0
- package/tools/dist/typedir.js +7 -0
- package/tools/dist/types.js +71 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -4
- package/src/index.ts +0 -1
- package/tsconfig.json +0 -14
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dafny emitter — IR → Dafny text.
|
|
3
|
+
*/
|
|
4
|
+
// ── Ty → Dafny type string ─────────────────────────────────
|
|
5
|
+
function tyToDafny(ty) {
|
|
6
|
+
switch (ty.kind) {
|
|
7
|
+
case "nat": return "nat";
|
|
8
|
+
case "int": return "int";
|
|
9
|
+
case "real": return "real";
|
|
10
|
+
case "bool": return "bool";
|
|
11
|
+
case "string": return "string";
|
|
12
|
+
case "void": return "()";
|
|
13
|
+
case "array": return `seq<${tyToDafny(ty.elem)}>`;
|
|
14
|
+
case "map": return `map<${tyToDafny(ty.key)}, ${tyToDafny(ty.value)}>`;
|
|
15
|
+
case "set": return `set<${tyToDafny(ty.elem)}>`;
|
|
16
|
+
case "optional": {
|
|
17
|
+
needsOptionType = true;
|
|
18
|
+
return `Option<${tyToDafny(ty.inner)}>`;
|
|
19
|
+
}
|
|
20
|
+
case "user": return ty.name;
|
|
21
|
+
case "unknown": return "int";
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ── Dafny keyword escaping ──────────────────────────────────
|
|
25
|
+
const DAFNY_KEYWORDS = new Set([
|
|
26
|
+
"seq", "set", "map", "multiset", "iset", "imap",
|
|
27
|
+
"var", "method", "function", "predicate", "lemma",
|
|
28
|
+
"class", "trait", "module", "import", "export",
|
|
29
|
+
"if", "then", "else", "while", "for", "in",
|
|
30
|
+
"match", "case", "return", "break", "continue",
|
|
31
|
+
"requires", "ensures", "invariant", "decreases",
|
|
32
|
+
"forall", "exists", "old", "fresh", "allocated",
|
|
33
|
+
"true", "false", "null", /*"this",*/ "new",
|
|
34
|
+
"datatype", "type", "const", "ghost", "static",
|
|
35
|
+
"reads", "modifies", "assert", "assume", "print",
|
|
36
|
+
"by", "calc", "reveal",
|
|
37
|
+
]);
|
|
38
|
+
function escapeName(name) {
|
|
39
|
+
if (DAFNY_KEYWORDS.has(name))
|
|
40
|
+
return `${name}_`;
|
|
41
|
+
// Dafny doesn't allow identifiers starting with _
|
|
42
|
+
if (name.startsWith("_"))
|
|
43
|
+
return `i${name}`;
|
|
44
|
+
return name;
|
|
45
|
+
}
|
|
46
|
+
/** Format a typed parameter list for Dafny: "x: int, y: seq<int>" */
|
|
47
|
+
function paramList(params) {
|
|
48
|
+
return params.map(p => `${escapeName(p.name)}: ${tyToDafny(p.type)}`).join(", ");
|
|
49
|
+
}
|
|
50
|
+
// ── Lean op → Dafny op ─────────────────────────────────────
|
|
51
|
+
const OP_MAP = {
|
|
52
|
+
"=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
|
|
53
|
+
"∧": "&&", "∨": "||", "¬": "!",
|
|
54
|
+
};
|
|
55
|
+
function mapOp(op) { return OP_MAP[op] ?? op; }
|
|
56
|
+
// ── Expression emission ─────────────────────────────────────
|
|
57
|
+
function emitExpr(e) {
|
|
58
|
+
switch (e.kind) {
|
|
59
|
+
case "var": return escapeName(e.name);
|
|
60
|
+
case "num": return `${e.value}`;
|
|
61
|
+
case "bool": return e.value ? "true" : "false";
|
|
62
|
+
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
63
|
+
case "constructor": return qualifyCtor(e.name, e.type);
|
|
64
|
+
case "arrayLiteral":
|
|
65
|
+
if (e.elems.length === 0)
|
|
66
|
+
return `[]`;
|
|
67
|
+
return `[${e.elems.map(emitExpr).join(", ")}]`;
|
|
68
|
+
case "emptyMap": return `map[]`;
|
|
69
|
+
case "emptySet": return `{}`;
|
|
70
|
+
case "methodCall": {
|
|
71
|
+
const obj = emitExpr(e.obj);
|
|
72
|
+
const args = e.args.map(emitExpr);
|
|
73
|
+
const ty = e.objTy.kind;
|
|
74
|
+
// Array methods
|
|
75
|
+
if (ty === "array") {
|
|
76
|
+
if (e.method === "with")
|
|
77
|
+
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
78
|
+
if (e.method === "includes")
|
|
79
|
+
return `(${args[0]} in ${obj})`;
|
|
80
|
+
if (e.method === "push")
|
|
81
|
+
return `(${obj} + [${args[0]}])`;
|
|
82
|
+
if (e.method === "slice")
|
|
83
|
+
return `${obj}[${args[0]}..]`;
|
|
84
|
+
if (e.method === "map") {
|
|
85
|
+
needsStdCollections = true;
|
|
86
|
+
return `Seq.Map(${args[0]}, ${obj})`;
|
|
87
|
+
}
|
|
88
|
+
if (e.method === "filter") {
|
|
89
|
+
needsStdCollections = true;
|
|
90
|
+
return `Seq.Filter(${args[0]}, ${obj})`;
|
|
91
|
+
}
|
|
92
|
+
if (e.method === "every") {
|
|
93
|
+
needsStdCollections = true;
|
|
94
|
+
return `Seq.All(${obj}, ${args[0]})`;
|
|
95
|
+
}
|
|
96
|
+
if (e.method === "some" && e.args[0].kind === "lambda" &&
|
|
97
|
+
e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
|
|
98
|
+
const lam = e.args[0];
|
|
99
|
+
const ret = lam.body[0];
|
|
100
|
+
if (ret.kind !== "return")
|
|
101
|
+
throw new Error("unreachable");
|
|
102
|
+
const p = escapeName(lam.params[0]?.name ?? "x");
|
|
103
|
+
const body = emitExpr(ret.value);
|
|
104
|
+
return `(exists ${p} :: ${p} in ${obj} && ${body})`;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// String methods
|
|
108
|
+
if (ty === "string") {
|
|
109
|
+
if (e.method === "indexOf") {
|
|
110
|
+
needsStringIndexOf = true;
|
|
111
|
+
return `StringIndexOf(${obj}, ${args[0]})`;
|
|
112
|
+
}
|
|
113
|
+
if (e.method === "slice")
|
|
114
|
+
return `${obj}[${args[0]}..${args[1]}]`;
|
|
115
|
+
if (e.method === "trim") {
|
|
116
|
+
needsStringTrim = true;
|
|
117
|
+
return `StringTrim(${obj})`;
|
|
118
|
+
}
|
|
119
|
+
if (e.method === "toLowerCase") {
|
|
120
|
+
needsStringToLower = true;
|
|
121
|
+
return `StringToLower(${obj})`;
|
|
122
|
+
}
|
|
123
|
+
if (e.method === "toUpperCase") {
|
|
124
|
+
needsStringToUpper = true;
|
|
125
|
+
return `StringToUpper(${obj})`;
|
|
126
|
+
}
|
|
127
|
+
if (e.method === "includes") {
|
|
128
|
+
needsStringIndexOf = true;
|
|
129
|
+
return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
|
|
130
|
+
}
|
|
131
|
+
if (e.method === "charCodeAt")
|
|
132
|
+
return `(${obj}[${args[0]}] as int)`;
|
|
133
|
+
}
|
|
134
|
+
// Map methods
|
|
135
|
+
if (ty === "map") {
|
|
136
|
+
if (e.method === "getDirect")
|
|
137
|
+
return `${obj}[${args[0]}]`;
|
|
138
|
+
if (e.method === "get") {
|
|
139
|
+
needsOptionType = true;
|
|
140
|
+
return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
|
|
141
|
+
}
|
|
142
|
+
if (e.method === "set")
|
|
143
|
+
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
144
|
+
if (e.method === "has")
|
|
145
|
+
return `(${args[0]} in ${obj})`;
|
|
146
|
+
}
|
|
147
|
+
// Set methods
|
|
148
|
+
if (ty === "set") {
|
|
149
|
+
if (e.method === "has")
|
|
150
|
+
return `(${args[0]} in ${obj})`;
|
|
151
|
+
if (e.method === "add")
|
|
152
|
+
return `(${obj} + {${args[0]}})`;
|
|
153
|
+
if (e.method === "delete")
|
|
154
|
+
return `(${obj} - {${args[0]}})`;
|
|
155
|
+
}
|
|
156
|
+
throw new Error(`Unsupported Dafny method call: .${e.method}() on ${ty}`);
|
|
157
|
+
}
|
|
158
|
+
case "lambda": {
|
|
159
|
+
const ps = paramList(e.params);
|
|
160
|
+
if (e.body.length === 1 && e.body[0].kind === "return")
|
|
161
|
+
return `(${ps}) => ${emitExpr(e.body[0].value)}`;
|
|
162
|
+
throw new Error("Unsupported: multi-statement lambda in Dafny");
|
|
163
|
+
}
|
|
164
|
+
case "unop": {
|
|
165
|
+
const op = mapOp(e.op);
|
|
166
|
+
if (op === "!" && e.expr.kind !== "var" && e.expr.kind !== "bool")
|
|
167
|
+
return `!(${emitExpr(e.expr)})`;
|
|
168
|
+
if (e.op === "-" && e.expr.kind === "num")
|
|
169
|
+
return `(-(${e.expr.value}))`;
|
|
170
|
+
if (e.op === "-")
|
|
171
|
+
return `(-(${emitExpr(e.expr)}))`;
|
|
172
|
+
return `${op}(${emitExpr(e.expr)})`;
|
|
173
|
+
}
|
|
174
|
+
case "binop": {
|
|
175
|
+
// Discriminant check: x == .Ctor → x.Ctor?
|
|
176
|
+
const op = mapOp(e.op);
|
|
177
|
+
if ((op === "==" || op === "!=") && e.right.kind === "constructor") {
|
|
178
|
+
const ctorName = escapeName(e.right.name.replace(/^\./, ""));
|
|
179
|
+
const pred = `${emitExpr(e.left)}.${ctorName}?`;
|
|
180
|
+
return op === "!=" ? `(!${pred})` : pred;
|
|
181
|
+
}
|
|
182
|
+
// Bitwise operators on int: translate to arithmetic
|
|
183
|
+
// x >> n → x / 2^n (right shift)
|
|
184
|
+
// x << n → x * 2^n (left shift)
|
|
185
|
+
if (e.op === ">>") {
|
|
186
|
+
if (e.right.kind === "num") {
|
|
187
|
+
return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
|
|
188
|
+
}
|
|
189
|
+
needsPow2 = true;
|
|
190
|
+
return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
|
|
191
|
+
}
|
|
192
|
+
if (e.op === "<<") {
|
|
193
|
+
if (e.right.kind === "num") {
|
|
194
|
+
return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
|
|
195
|
+
}
|
|
196
|
+
needsPow2 = true;
|
|
197
|
+
return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
|
|
198
|
+
}
|
|
199
|
+
// x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
|
|
200
|
+
if (e.op === "&") {
|
|
201
|
+
if (e.right.kind === "num") {
|
|
202
|
+
const mask = e.right.value;
|
|
203
|
+
const modulus = mask + 1;
|
|
204
|
+
if ((modulus & (modulus - 1)) === 0) {
|
|
205
|
+
return `(${emitExpr(e.left)} % ${modulus})`;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
needsBitAnd = true;
|
|
209
|
+
return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
|
|
210
|
+
}
|
|
211
|
+
// int * real coercion: wrap int side with "as real"
|
|
212
|
+
if (["+", "-", "*", "/"].includes(op)) {
|
|
213
|
+
const leftIsReal = e.left.kind === "num" && !Number.isInteger(e.left.value);
|
|
214
|
+
const rightIsReal = e.right.kind === "num" && !Number.isInteger(e.right.value);
|
|
215
|
+
if (leftIsReal !== rightIsReal) {
|
|
216
|
+
const left = leftIsReal ? emitExpr(e.left) : `(${emitExpr(e.left)} as real)`;
|
|
217
|
+
const right = rightIsReal ? emitExpr(e.right) : `(${emitExpr(e.right)} as real)`;
|
|
218
|
+
return `(${left} ${op} ${right})`;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
return `(${emitExpr(e.left)} ${op} ${emitExpr(e.right)})`;
|
|
222
|
+
}
|
|
223
|
+
case "implies": {
|
|
224
|
+
const parts = [...e.premises.map(emitExpr), emitExpr(e.conclusion)];
|
|
225
|
+
return `(${parts.join(" ==> ")})`;
|
|
226
|
+
}
|
|
227
|
+
case "app": {
|
|
228
|
+
const args = e.args.map(emitExpr);
|
|
229
|
+
if (e.fn === "SetToSeq") {
|
|
230
|
+
needsSetToSeq = true;
|
|
231
|
+
return `SetToSeq(${args.join(", ")})`;
|
|
232
|
+
}
|
|
233
|
+
if (e.fn === "BigInt" || e.fn === "Number")
|
|
234
|
+
return args[0]; // identity: both map to int
|
|
235
|
+
if (e.fn === "JSFloorDiv")
|
|
236
|
+
needsJSFloorDiv = true;
|
|
237
|
+
if (e.fn === "CeilReal")
|
|
238
|
+
needsCeilReal = true;
|
|
239
|
+
if (e.fn === "FloorReal")
|
|
240
|
+
needsFloorReal = true;
|
|
241
|
+
return `${e.fn}(${args.join(", ")})`;
|
|
242
|
+
}
|
|
243
|
+
case "field": {
|
|
244
|
+
const obj = emitExpr(e.obj);
|
|
245
|
+
if (e.field === "size" || e.field === "length" || e.field === "collectionSize")
|
|
246
|
+
return `|${obj}|`;
|
|
247
|
+
if (e.field === "keys")
|
|
248
|
+
return `${obj}.Keys`;
|
|
249
|
+
if (e.field === "toNat")
|
|
250
|
+
return obj;
|
|
251
|
+
return `${obj}.${escapeName(e.field)}`;
|
|
252
|
+
}
|
|
253
|
+
case "toNat":
|
|
254
|
+
// Dafny doesn't need toNat — just emit the inner expression
|
|
255
|
+
return emitExpr(e.expr);
|
|
256
|
+
case "index":
|
|
257
|
+
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]`;
|
|
258
|
+
case "record": {
|
|
259
|
+
if (e.spread) {
|
|
260
|
+
const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
261
|
+
return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
|
|
262
|
+
}
|
|
263
|
+
const ctorName = e.fields.length > 0 ? _recordCtors.get(e.fields[0].name) : undefined;
|
|
264
|
+
const vals = e.fields.map(f => emitExpr(f.value));
|
|
265
|
+
if (ctorName)
|
|
266
|
+
return `${ctorName}(${vals.join(", ")})`;
|
|
267
|
+
return `(${vals.join(", ")})`;
|
|
268
|
+
}
|
|
269
|
+
case "if":
|
|
270
|
+
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
271
|
+
case "match": {
|
|
272
|
+
const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
|
|
273
|
+
const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
|
|
274
|
+
return `(match ${scrut} { ${arms.join(" ")} })`;
|
|
275
|
+
}
|
|
276
|
+
case "forall": {
|
|
277
|
+
// Collapse nested foralls: forall x :: forall y :: P → forall x, y :: P
|
|
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
|
+
}
|
|
300
|
+
case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
|
|
301
|
+
case "havoc": return "*";
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/** Emit a pure expression with indentation for if/match/let. */
|
|
305
|
+
function emitPureExpr(e, indent) {
|
|
306
|
+
const pad = " ".repeat(indent);
|
|
307
|
+
switch (e.kind) {
|
|
308
|
+
case "if":
|
|
309
|
+
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
310
|
+
case "match": {
|
|
311
|
+
const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
|
|
312
|
+
const lines = [`${pad}match ${scrut} {`];
|
|
313
|
+
for (const arm of e.arms) {
|
|
314
|
+
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
315
|
+
lines.push(emitPureExpr(arm.body, indent + 2));
|
|
316
|
+
}
|
|
317
|
+
lines.push(`${pad}}`);
|
|
318
|
+
return lines.join("\n");
|
|
319
|
+
}
|
|
320
|
+
case "let":
|
|
321
|
+
return `${pad}var ${escapeName(e.name)} := ${emitExpr(e.value)};\n${emitPureExpr(e.body, indent)}`;
|
|
322
|
+
default:
|
|
323
|
+
return `${pad}${emitExpr(e)}`;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// ── Statement emission ──────────────────────────────────────
|
|
327
|
+
function emitStmts(stmts, indent) {
|
|
328
|
+
return stmts.map(s => emitStmt(s, indent)).join("\n");
|
|
329
|
+
}
|
|
330
|
+
function emitStmt(s, indent) {
|
|
331
|
+
const pad = " ".repeat(indent);
|
|
332
|
+
switch (s.kind) {
|
|
333
|
+
case "let":
|
|
334
|
+
if (s.value.kind === "havoc")
|
|
335
|
+
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
336
|
+
return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
|
|
337
|
+
case "assign":
|
|
338
|
+
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
339
|
+
case "ghostLet":
|
|
340
|
+
return `${pad}ghost var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
341
|
+
case "ghostAssign":
|
|
342
|
+
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
343
|
+
case "assert":
|
|
344
|
+
return `${pad}assert ${emitExpr(s.expr)};`;
|
|
345
|
+
case "bind":
|
|
346
|
+
// Monadic bind shouldn't appear in Dafny mode, emit as regular assign
|
|
347
|
+
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
348
|
+
case "let-bind":
|
|
349
|
+
// Monadic let-bind shouldn't appear in Dafny mode, emit as regular let
|
|
350
|
+
return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
|
|
351
|
+
case "return":
|
|
352
|
+
return `${pad}return ${emitExpr(s.value)};`;
|
|
353
|
+
case "break":
|
|
354
|
+
return `${pad}break;`;
|
|
355
|
+
case "continue":
|
|
356
|
+
throw new Error("Unsupported Dafny construct: 'continue' statement");
|
|
357
|
+
case "if": {
|
|
358
|
+
let out = `${pad}if ${emitExpr(s.cond)} {\n${emitStmts(s.then, indent + 1)}\n${pad}}`;
|
|
359
|
+
if (s.else.length > 0) {
|
|
360
|
+
if (s.else.length === 1 && s.else[0].kind === "if") {
|
|
361
|
+
out += ` else ${emitStmt(s.else[0], indent).trimStart()}`;
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
out += ` else {\n${emitStmts(s.else, indent + 1)}\n${pad}}`;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
case "match": {
|
|
370
|
+
const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
|
|
371
|
+
const lines = [`${pad}match ${scrut} {`];
|
|
372
|
+
for (const arm of s.arms) {
|
|
373
|
+
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
374
|
+
lines.push(emitStmts(arm.body, indent + 2));
|
|
375
|
+
}
|
|
376
|
+
lines.push(`${pad}}`);
|
|
377
|
+
return lines.join("\n");
|
|
378
|
+
}
|
|
379
|
+
case "while": {
|
|
380
|
+
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
381
|
+
for (const inv of s.invariants)
|
|
382
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
383
|
+
if (s.decreasing)
|
|
384
|
+
lines.push(`${pad} decreases ${emitExpr(s.decreasing)}`);
|
|
385
|
+
lines.push(`${pad}{`);
|
|
386
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
387
|
+
lines.push(`${pad}}`);
|
|
388
|
+
return lines.join("\n");
|
|
389
|
+
}
|
|
390
|
+
case "forin": {
|
|
391
|
+
// Lean for-in → Dafny while loop over index
|
|
392
|
+
const idx = escapeName(s.idx);
|
|
393
|
+
const lines = [
|
|
394
|
+
`${pad}var ${idx} := 0;`,
|
|
395
|
+
`${pad}while ${idx} < ${emitExpr(s.bound)}`,
|
|
396
|
+
];
|
|
397
|
+
for (const inv of s.invariants)
|
|
398
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
399
|
+
lines.push(`${pad}{`);
|
|
400
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
401
|
+
lines.push(`${pad} ${idx} := ${idx} + 1;`);
|
|
402
|
+
lines.push(`${pad}}`);
|
|
403
|
+
return lines.join("\n");
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
// ── Declaration emission ────────────────────────────────────
|
|
408
|
+
function emitDecl(d) {
|
|
409
|
+
switch (d.kind) {
|
|
410
|
+
case "inductive": {
|
|
411
|
+
const ctors = d.constructors.map(c => {
|
|
412
|
+
if (c.fields.length === 0)
|
|
413
|
+
return escapeName(c.name);
|
|
414
|
+
return `${escapeName(c.name)}(${paramList(c.fields)})`;
|
|
415
|
+
});
|
|
416
|
+
return `datatype ${d.name} = ${ctors.join(" | ")}`;
|
|
417
|
+
}
|
|
418
|
+
case "structure": {
|
|
419
|
+
return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
|
|
420
|
+
}
|
|
421
|
+
case "def": {
|
|
422
|
+
const lines = [`function ${d.name}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
423
|
+
for (const r of d.requires)
|
|
424
|
+
lines.push(` requires ${emitExpr(r)}`);
|
|
425
|
+
lines.push(`{`);
|
|
426
|
+
lines.push(emitPureExpr(d.body, 1));
|
|
427
|
+
lines.push(`}`);
|
|
428
|
+
// Companion lemma for ensures (proof target for LLM)
|
|
429
|
+
if (d.ensures.length > 0) {
|
|
430
|
+
lines.push("");
|
|
431
|
+
lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
|
|
432
|
+
for (const r of d.requires)
|
|
433
|
+
lines.push(` requires ${emitExpr(r)}`);
|
|
434
|
+
for (const e of d.ensures)
|
|
435
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
436
|
+
lines.push(`{`);
|
|
437
|
+
lines.push(`}`);
|
|
438
|
+
}
|
|
439
|
+
return lines.join("\n");
|
|
440
|
+
}
|
|
441
|
+
case "method": {
|
|
442
|
+
const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
|
|
443
|
+
for (const r of d.requires)
|
|
444
|
+
lines.push(` requires ${emitExpr(r)}`);
|
|
445
|
+
for (const e of d.ensures)
|
|
446
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
447
|
+
lines.push(`{`);
|
|
448
|
+
lines.push(emitStmts(d.body, 1));
|
|
449
|
+
lines.push(`}`);
|
|
450
|
+
return lines.join("\n");
|
|
451
|
+
}
|
|
452
|
+
case "class": {
|
|
453
|
+
const lines = [`class ${d.name} {`];
|
|
454
|
+
for (const f of d.fields) {
|
|
455
|
+
lines.push(` var ${escapeName(f.name)}: ${tyToDafny(f.type)}`);
|
|
456
|
+
}
|
|
457
|
+
if (d.fields.length > 0 && d.methods.length > 0)
|
|
458
|
+
lines.push("");
|
|
459
|
+
for (const m of d.methods) {
|
|
460
|
+
lines.push(` method ${m.name}(${paramList(m.params)}) returns (res: ${tyToDafny(m.returnType)})`);
|
|
461
|
+
for (const r of m.requires)
|
|
462
|
+
lines.push(` requires ${emitExpr(r)}`);
|
|
463
|
+
for (const e of m.ensures)
|
|
464
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
465
|
+
lines.push(` {`);
|
|
466
|
+
lines.push(emitStmts(m.body, 2));
|
|
467
|
+
lines.push(` }`);
|
|
468
|
+
}
|
|
469
|
+
lines.push(`}`);
|
|
470
|
+
return lines.join("\n");
|
|
471
|
+
}
|
|
472
|
+
case "const": {
|
|
473
|
+
return `const ${escapeName(d.name)}: ${tyToDafny(d.type)} := ${emitExpr(d.value)}`;
|
|
474
|
+
}
|
|
475
|
+
case "namespace": {
|
|
476
|
+
// Dafny doesn't need namespaces — flatten declarations
|
|
477
|
+
return d.decls.map(emitDecl).join("\n\n");
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
// ── File emission ───────────────────────────────────────────
|
|
482
|
+
// ── Preamble helpers ────────────────────────────────────────
|
|
483
|
+
let needsStringIndexOf = false;
|
|
484
|
+
let needsStringTrim = false;
|
|
485
|
+
let needsStringToLower = false;
|
|
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;
|
|
495
|
+
const POW2 = `function Pow2(n: int): int
|
|
496
|
+
requires n >= 0
|
|
497
|
+
decreases n
|
|
498
|
+
{
|
|
499
|
+
if n == 0 then 1 else 2 * Pow2(n - 1)
|
|
500
|
+
}`;
|
|
501
|
+
const BIT_AND = `function BitAnd(x: int, y: int): int
|
|
502
|
+
requires x >= 0 && y >= 0
|
|
503
|
+
decreases x
|
|
504
|
+
{
|
|
505
|
+
if x == 0 || y == 0 then 0
|
|
506
|
+
else 2 * BitAnd(x / 2, y / 2) + (if x % 2 == 1 && y % 2 == 1 then 1 else 0)
|
|
507
|
+
}`;
|
|
508
|
+
const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
|
|
509
|
+
requires b != 0
|
|
510
|
+
{
|
|
511
|
+
if b > 0 then
|
|
512
|
+
if a >= 0 then a / b
|
|
513
|
+
else -((-a - 1) / b) - 1
|
|
514
|
+
else
|
|
515
|
+
if a <= 0 then (-a) / (-b)
|
|
516
|
+
else -((a - 1) / (-b)) - 1
|
|
517
|
+
}`;
|
|
518
|
+
const FLOOR_REAL = `function FloorReal(x: real): int
|
|
519
|
+
{
|
|
520
|
+
x.Floor
|
|
521
|
+
}`;
|
|
522
|
+
const CEIL_REAL = `function CeilReal(x: real): int
|
|
523
|
+
{
|
|
524
|
+
if x == (x.Floor as real) then x.Floor
|
|
525
|
+
else x.Floor + 1
|
|
526
|
+
}`;
|
|
527
|
+
const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
|
|
528
|
+
{
|
|
529
|
+
StringIndexOfFrom(s, sub, 0)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function StringIndexOfFrom(s: string, sub: string, from: nat): int
|
|
533
|
+
decreases |s| - from
|
|
534
|
+
{
|
|
535
|
+
if from + |sub| > |s| then -1
|
|
536
|
+
else if s[from..from + |sub|] == sub then from as int
|
|
537
|
+
else StringIndexOfFrom(s, sub, from + 1)
|
|
538
|
+
}`;
|
|
539
|
+
const STRING_TRIM = `function StringTrimLeft(s: string): string
|
|
540
|
+
ensures |StringTrimLeft(s)| <= |s|
|
|
541
|
+
ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && StringTrimLeft(s)[0] != ' ')
|
|
542
|
+
decreases |s|
|
|
543
|
+
{
|
|
544
|
+
if |s| == 0 then ""
|
|
545
|
+
else if s[0] == ' ' then StringTrimLeft(s[1..])
|
|
546
|
+
else s
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function StringTrimRight(s: string): string
|
|
550
|
+
ensures |StringTrimRight(s)| <= |s|
|
|
551
|
+
decreases |s|
|
|
552
|
+
{
|
|
553
|
+
if |s| == 0 then ""
|
|
554
|
+
else if s[|s|-1] == ' ' then StringTrimRight(s[..|s|-1])
|
|
555
|
+
else s
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function StringTrim(s: string): string
|
|
559
|
+
{
|
|
560
|
+
StringTrimRight(StringTrimLeft(s))
|
|
561
|
+
}`;
|
|
562
|
+
const STRING_TO_LOWER = `function StringToLower(s: string): string
|
|
563
|
+
ensures |StringToLower(s)| == |s|
|
|
564
|
+
decreases |s|
|
|
565
|
+
{
|
|
566
|
+
if |s| == 0 then ""
|
|
567
|
+
else
|
|
568
|
+
var c := s[0];
|
|
569
|
+
var lower := if 'A' <= c <= 'Z' then (c - 'A' + 'a') as char else c;
|
|
570
|
+
[lower] + StringToLower(s[1..])
|
|
571
|
+
}`;
|
|
572
|
+
const STRING_TO_UPPER = `function StringToUpper(s: string): string
|
|
573
|
+
ensures |StringToUpper(s)| == |s|
|
|
574
|
+
decreases |s|
|
|
575
|
+
{
|
|
576
|
+
if |s| == 0 then ""
|
|
577
|
+
else
|
|
578
|
+
var c := s[0];
|
|
579
|
+
var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
|
|
580
|
+
[upper] + StringToUpper(s[1..])
|
|
581
|
+
}`;
|
|
582
|
+
// ── Constructor and record helpers ───────────────────────────
|
|
583
|
+
let _recordCtors = new Map();
|
|
584
|
+
function buildRecordCtorMap(decls) {
|
|
585
|
+
_recordCtors = new Map();
|
|
586
|
+
for (const d of decls) {
|
|
587
|
+
if (d.kind === "structure" && d.fields.length > 0)
|
|
588
|
+
_recordCtors.set(d.fields[0].name, d.name);
|
|
589
|
+
if (d.kind === "namespace")
|
|
590
|
+
for (const inner of d.decls) {
|
|
591
|
+
if (inner.kind === "structure" && inner.fields.length > 0)
|
|
592
|
+
_recordCtors.set(inner.fields[0].name, inner.name);
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
function qualifyCtor(name, type) {
|
|
597
|
+
const rawName = name.replace(/^\./, "");
|
|
598
|
+
if (type)
|
|
599
|
+
return `${type}.${escapeName(rawName)}`;
|
|
600
|
+
return escapeName(rawName);
|
|
601
|
+
}
|
|
602
|
+
/** Translate a Lean match pattern to Dafny syntax.
|
|
603
|
+
* ".ctorName field1 field2" → "ctorName(field1, field2)"
|
|
604
|
+
* ".ctorName" → "ctorName"
|
|
605
|
+
* "_" → "_"
|
|
606
|
+
*/
|
|
607
|
+
const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
608
|
+
function translatePattern(pattern) {
|
|
609
|
+
if (pattern === "_")
|
|
610
|
+
return "_";
|
|
611
|
+
const m = pattern.match(/^\.(\w+)\s*(.*)$/);
|
|
612
|
+
if (!m)
|
|
613
|
+
return pattern;
|
|
614
|
+
const ctorName = CTOR_MAP[m[1]] ?? escapeName(m[1]);
|
|
615
|
+
const fields = m[2].trim();
|
|
616
|
+
if (!fields)
|
|
617
|
+
return ctorName;
|
|
618
|
+
const fieldNames = fields.split(/\s+/).map(escapeName);
|
|
619
|
+
return `${ctorName}(${fieldNames.join(", ")})`;
|
|
620
|
+
}
|
|
621
|
+
const PREAMBLES = {
|
|
622
|
+
StringIndexOf: STRING_INDEX_OF,
|
|
623
|
+
};
|
|
624
|
+
export function emitDafnyFile(file, tsFileName) {
|
|
625
|
+
buildRecordCtorMap(file.decls);
|
|
626
|
+
needsStringIndexOf = false;
|
|
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;
|
|
638
|
+
// Collect pure def names so we can skip their method wrappers
|
|
639
|
+
const pureDefs = new Set();
|
|
640
|
+
for (const d of file.decls) {
|
|
641
|
+
if (d.kind === "namespace") {
|
|
642
|
+
for (const inner of d.decls)
|
|
643
|
+
if (inner.kind === "def")
|
|
644
|
+
pureDefs.add(inner.name);
|
|
645
|
+
}
|
|
646
|
+
if (d.kind === "def")
|
|
647
|
+
pureDefs.add(d.name);
|
|
648
|
+
}
|
|
649
|
+
// Emit declarations
|
|
650
|
+
const declLines = [];
|
|
651
|
+
const skipped = [];
|
|
652
|
+
for (const decl of file.decls) {
|
|
653
|
+
if (decl.kind === "method" && pureDefs.has(decl.name))
|
|
654
|
+
continue;
|
|
655
|
+
try {
|
|
656
|
+
declLines.push("");
|
|
657
|
+
declLines.push(emitDecl(decl));
|
|
658
|
+
}
|
|
659
|
+
catch (e) {
|
|
660
|
+
const name = "name" in decl ? decl.name : "unknown";
|
|
661
|
+
const msg = e.message;
|
|
662
|
+
console.error(`WARNING: skipping '${name}': ${msg}`);
|
|
663
|
+
declLines.push(`// LemmaScript: skipped ${name}`);
|
|
664
|
+
skipped.push(name);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
if (skipped.length > 0) {
|
|
668
|
+
console.error(`WARNING: ${skipped.length} declaration(s) skipped: ${skipped.join(", ")}`);
|
|
669
|
+
}
|
|
670
|
+
// Build output with needed preambles
|
|
671
|
+
const lines = [];
|
|
672
|
+
if (tsFileName)
|
|
673
|
+
lines.push(`// Generated by lsc from ${tsFileName}`);
|
|
674
|
+
if (needsStdCollections)
|
|
675
|
+
lines.push("import Std.Collections.Seq");
|
|
676
|
+
if (needsOptionType) {
|
|
677
|
+
lines.push("");
|
|
678
|
+
lines.push("datatype Option<T> = None | Some(value: T)");
|
|
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);
|
|
735
|
+
}
|
|
736
|
+
lines.push(...declLines);
|
|
737
|
+
return lines.join("\n") + "\n";
|
|
738
|
+
}
|