lemmascript 0.1.0 → 0.3.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/README.md +25 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +47 -53
- package/tools/dist/dafny-emit.js +495 -93
- package/tools/dist/extract.js +847 -46
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +397 -0
- package/tools/dist/lsc.js +62 -44
- package/tools/dist/resolve.js +500 -34
- package/tools/dist/specparser.js +66 -9
- package/tools/dist/transform.js +813 -202
- package/tools/dist/types.js +59 -13
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -1,32 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Dafny emitter —
|
|
3
|
-
*
|
|
4
|
-
* No separate Dafny IR. The shared transform produces Lean IR,
|
|
5
|
-
* and this emitter maps it to Dafny syntax.
|
|
2
|
+
* Dafny emitter — IR → Dafny text.
|
|
6
3
|
*/
|
|
7
|
-
// ──
|
|
8
|
-
function
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
return
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
if (parenMatch)
|
|
27
|
-
return leanTypeToDafny(parenMatch[1]);
|
|
28
|
-
// User types pass through
|
|
29
|
-
return t;
|
|
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
|
+
}
|
|
30
23
|
}
|
|
31
24
|
// ── Dafny keyword escaping ──────────────────────────────────
|
|
32
25
|
const DAFNY_KEYWORDS = new Set([
|
|
@@ -37,7 +30,7 @@ const DAFNY_KEYWORDS = new Set([
|
|
|
37
30
|
"match", "case", "return", "break", "continue",
|
|
38
31
|
"requires", "ensures", "invariant", "decreases",
|
|
39
32
|
"forall", "exists", "old", "fresh", "allocated",
|
|
40
|
-
"true", "false", "null", "this"
|
|
33
|
+
"true", "false", "null", /*"this",*/ "new",
|
|
41
34
|
"datatype", "type", "const", "ghost", "static",
|
|
42
35
|
"reads", "modifies", "assert", "assume", "print",
|
|
43
36
|
"by", "calc", "reveal",
|
|
@@ -52,56 +45,116 @@ function escapeName(name) {
|
|
|
52
45
|
}
|
|
53
46
|
/** Format a typed parameter list for Dafny: "x: int, y: seq<int>" */
|
|
54
47
|
function paramList(params) {
|
|
55
|
-
return params.map(p => `${escapeName(p.name)}: ${
|
|
48
|
+
return params.map(p => `${escapeName(p.name)}: ${tyToDafny(p.type)}`).join(", ");
|
|
56
49
|
}
|
|
57
50
|
// ── Lean op → Dafny op ─────────────────────────────────────
|
|
58
51
|
const OP_MAP = {
|
|
59
52
|
"=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
|
|
60
53
|
"∧": "&&", "∨": "||", "¬": "!",
|
|
54
|
+
"arrayConcat": "+",
|
|
61
55
|
};
|
|
62
56
|
function mapOp(op) { return OP_MAP[op] ?? op; }
|
|
63
57
|
// ── Expression emission ─────────────────────────────────────
|
|
64
58
|
function emitExpr(e) {
|
|
65
59
|
switch (e.kind) {
|
|
66
|
-
case "var": return escapeName(e.name);
|
|
60
|
+
case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
|
|
67
61
|
case "num": return `${e.value}`;
|
|
68
62
|
case "bool": return e.value ? "true" : "false";
|
|
69
|
-
case "str": return `"${e.value}"`;
|
|
63
|
+
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
70
64
|
case "constructor": return qualifyCtor(e.name, e.type);
|
|
71
65
|
case "arrayLiteral":
|
|
72
66
|
if (e.elems.length === 0)
|
|
73
67
|
return `[]`;
|
|
74
68
|
return `[${e.elems.map(emitExpr).join(", ")}]`;
|
|
75
|
-
case "
|
|
69
|
+
case "emptyMap": return `map[]`;
|
|
70
|
+
case "emptySet": return `{}`;
|
|
71
|
+
case "methodCall": {
|
|
76
72
|
const obj = emitExpr(e.obj);
|
|
77
73
|
const args = e.args.map(emitExpr);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
74
|
+
const ty = e.objTy.kind;
|
|
75
|
+
// Array methods
|
|
76
|
+
if (ty === "array") {
|
|
77
|
+
if (e.method === "with")
|
|
78
|
+
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
79
|
+
if (e.method === "includes")
|
|
80
|
+
return `(${args[0]} in ${obj})`;
|
|
81
|
+
if (e.method === "push")
|
|
82
|
+
return `(${obj} + [${args[0]}])`;
|
|
83
|
+
if (e.method === "concat")
|
|
84
|
+
return `(${obj} + [${args[0]}])`;
|
|
85
|
+
if (e.method === "slice" && args.length === 1)
|
|
86
|
+
return `${obj}[${args[0]}..]`;
|
|
87
|
+
if (e.method === "slice" && args.length === 2)
|
|
88
|
+
return `${obj}[${args[0]}..${args[1]}]`;
|
|
89
|
+
if (e.method === "map")
|
|
90
|
+
return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
|
|
91
|
+
if (e.method === "filter")
|
|
92
|
+
return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
|
|
93
|
+
if (e.method === "every")
|
|
94
|
+
return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
|
|
95
|
+
if (e.method === "some" && e.args[0].kind === "lambda" &&
|
|
96
|
+
e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
|
|
97
|
+
const lam = e.args[0];
|
|
98
|
+
const ret = lam.body[0];
|
|
99
|
+
if (ret.kind !== "return")
|
|
100
|
+
throw new Error("unreachable");
|
|
101
|
+
const p = escapeName(lam.params[0]?.name ?? "x");
|
|
102
|
+
const body = emitExpr(ret.value);
|
|
103
|
+
return `(exists ${p} :: ${p} in ${obj} && ${body})`;
|
|
104
|
+
}
|
|
85
105
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
106
|
+
// String methods
|
|
107
|
+
if (ty === "string") {
|
|
108
|
+
if (e.method === "indexOf") {
|
|
109
|
+
needsStringIndexOf = true;
|
|
110
|
+
return `StringIndexOf(${obj}, ${args[0]})`;
|
|
111
|
+
}
|
|
112
|
+
if (e.method === "slice")
|
|
113
|
+
return `${obj}[${args[0]}..${args[1]}]`;
|
|
114
|
+
if (e.method === "trim") {
|
|
115
|
+
needsStringTrim = true;
|
|
116
|
+
return `StringTrim(${obj})`;
|
|
117
|
+
}
|
|
118
|
+
if (e.method === "toLowerCase") {
|
|
119
|
+
needsStringToLower = true;
|
|
120
|
+
return `StringToLower(${obj})`;
|
|
121
|
+
}
|
|
122
|
+
if (e.method === "toUpperCase") {
|
|
123
|
+
needsStringToUpper = true;
|
|
124
|
+
return `StringToUpper(${obj})`;
|
|
125
|
+
}
|
|
126
|
+
if (e.method === "includes") {
|
|
127
|
+
needsStringIndexOf = true;
|
|
128
|
+
return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
|
|
129
|
+
}
|
|
130
|
+
if (e.method === "charCodeAt")
|
|
131
|
+
return `(${obj}[${args[0]}] as int)`;
|
|
89
132
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
133
|
+
// Map methods
|
|
134
|
+
if (ty === "map") {
|
|
135
|
+
if (e.method === "getDirect")
|
|
136
|
+
return `${obj}[${args[0]}]`;
|
|
137
|
+
if (e.method === "get") {
|
|
138
|
+
needsOptionType = true;
|
|
139
|
+
return `(if ${args[0]} in ${obj} then Some(${obj}[${args[0]}]) else None)`;
|
|
140
|
+
}
|
|
141
|
+
if (e.method === "set")
|
|
142
|
+
return `${obj}[${args[0]} := ${args[1]}]`;
|
|
143
|
+
if (e.method === "has")
|
|
144
|
+
return `(${args[0]} in ${obj})`;
|
|
145
|
+
if (e.method === "delete")
|
|
146
|
+
return `(map k | k in ${obj} && k != ${args[0]} :: ${obj}[k])`;
|
|
93
147
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return `(exists ${p} :: ${p} in ${obj} && ${body})`;
|
|
148
|
+
// Set methods
|
|
149
|
+
if (ty === "set") {
|
|
150
|
+
if (e.method === "has")
|
|
151
|
+
return `(${args[0]} in ${obj})`;
|
|
152
|
+
if (e.method === "add")
|
|
153
|
+
return `(${obj} + {${args[0]}})`;
|
|
154
|
+
if (e.method === "delete")
|
|
155
|
+
return `(${obj} - {${args[0]}})`;
|
|
103
156
|
}
|
|
104
|
-
|
|
157
|
+
throw new Error(`Unsupported Dafny method call: .${e.method}() on ${ty}`);
|
|
105
158
|
}
|
|
106
159
|
case "lambda": {
|
|
107
160
|
const ps = paramList(e.params);
|
|
@@ -127,6 +180,45 @@ function emitExpr(e) {
|
|
|
127
180
|
const pred = `${emitExpr(e.left)}.${ctorName}?`;
|
|
128
181
|
return op === "!=" ? `(!${pred})` : pred;
|
|
129
182
|
}
|
|
183
|
+
// Bitwise operators on int: translate to arithmetic
|
|
184
|
+
// x >> n → x / 2^n (right shift)
|
|
185
|
+
// x << n → x * 2^n (left shift)
|
|
186
|
+
if (e.op === ">>") {
|
|
187
|
+
if (e.right.kind === "num") {
|
|
188
|
+
return `(${emitExpr(e.left)} / ${Math.pow(2, e.right.value)})`;
|
|
189
|
+
}
|
|
190
|
+
needsPow2 = true;
|
|
191
|
+
return `(${emitExpr(e.left)} / Pow2(${emitExpr(e.right)}))`;
|
|
192
|
+
}
|
|
193
|
+
if (e.op === "<<") {
|
|
194
|
+
if (e.right.kind === "num") {
|
|
195
|
+
return `(${emitExpr(e.left)} * ${Math.pow(2, e.right.value)})`;
|
|
196
|
+
}
|
|
197
|
+
needsPow2 = true;
|
|
198
|
+
return `(${emitExpr(e.left)} * Pow2(${emitExpr(e.right)}))`;
|
|
199
|
+
}
|
|
200
|
+
// x & mask → x % (mask + 1) for literal masks of form 2^n - 1, else BitAnd
|
|
201
|
+
if (e.op === "&") {
|
|
202
|
+
if (e.right.kind === "num") {
|
|
203
|
+
const mask = e.right.value;
|
|
204
|
+
const modulus = mask + 1;
|
|
205
|
+
if ((modulus & (modulus - 1)) === 0) {
|
|
206
|
+
return `(${emitExpr(e.left)} % ${modulus})`;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
needsBitAnd = true;
|
|
210
|
+
return `BitAnd(${emitExpr(e.left)}, ${emitExpr(e.right)})`;
|
|
211
|
+
}
|
|
212
|
+
// int * real coercion: wrap int side with "as real"
|
|
213
|
+
if (["+", "-", "*", "/"].includes(op)) {
|
|
214
|
+
const leftIsReal = e.left.kind === "num" && !Number.isInteger(e.left.value);
|
|
215
|
+
const rightIsReal = e.right.kind === "num" && !Number.isInteger(e.right.value);
|
|
216
|
+
if (leftIsReal !== rightIsReal) {
|
|
217
|
+
const left = leftIsReal ? emitExpr(e.left) : `(${emitExpr(e.left)} as real)`;
|
|
218
|
+
const right = rightIsReal ? emitExpr(e.right) : `(${emitExpr(e.right)} as real)`;
|
|
219
|
+
return `(${left} ${op} ${right})`;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
130
222
|
return `(${emitExpr(e.left)} ${op} ${emitExpr(e.right)})`;
|
|
131
223
|
}
|
|
132
224
|
case "implies": {
|
|
@@ -135,23 +227,37 @@ function emitExpr(e) {
|
|
|
135
227
|
}
|
|
136
228
|
case "app": {
|
|
137
229
|
const args = e.args.map(emitExpr);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return `StringIndexOf(${args.join(", ")})`;
|
|
230
|
+
if (e.fn === "SetToSeq") {
|
|
231
|
+
needsSetToSeq = true;
|
|
232
|
+
return `SetToSeq(${args.join(", ")})`;
|
|
142
233
|
}
|
|
143
|
-
if (e.fn === "
|
|
144
|
-
return
|
|
145
|
-
|
|
146
|
-
|
|
234
|
+
if (e.fn === "BigInt" || e.fn === "Number")
|
|
235
|
+
return args[0]; // identity: both map to int
|
|
236
|
+
// Set literal: {a, b, c}
|
|
237
|
+
if (e.fn === "SetLiteral")
|
|
238
|
+
return `{${args.join(", ")}}`;
|
|
147
239
|
if (e.fn === "JSFloorDiv")
|
|
148
240
|
needsJSFloorDiv = true;
|
|
149
|
-
|
|
241
|
+
if (e.fn === "CeilReal")
|
|
242
|
+
needsCeilReal = true;
|
|
243
|
+
if (e.fn === "FloorReal")
|
|
244
|
+
needsFloorReal = true;
|
|
245
|
+
if (e.fn === "NatToString")
|
|
246
|
+
needsNatToString = true;
|
|
247
|
+
if (e.fn === "MathAbs")
|
|
248
|
+
needsMathAbs = true;
|
|
249
|
+
if (e.fn === "MathMin")
|
|
250
|
+
needsMathMin = true;
|
|
251
|
+
if (e.fn === "MathMax")
|
|
252
|
+
needsMathMax = true;
|
|
253
|
+
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
150
254
|
}
|
|
151
255
|
case "field": {
|
|
152
256
|
const obj = emitExpr(e.obj);
|
|
153
|
-
if (e.field === "size" || e.field === "length")
|
|
257
|
+
if (e.field === "size" || e.field === "length" || e.field === "collectionSize")
|
|
154
258
|
return `|${obj}|`;
|
|
259
|
+
if (e.field === "keys")
|
|
260
|
+
return `${obj}.Keys`;
|
|
155
261
|
if (e.field === "toNat")
|
|
156
262
|
return obj;
|
|
157
263
|
return `${obj}.${escapeName(e.field)}`;
|
|
@@ -166,21 +272,75 @@ function emitExpr(e) {
|
|
|
166
272
|
const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
167
273
|
return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
|
|
168
274
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
if (
|
|
275
|
+
// Match constructor by field names — prefer exact match over first-field heuristic
|
|
276
|
+
let ctorName;
|
|
277
|
+
if (e.fields.length > 0) {
|
|
278
|
+
const fieldNames = new Set(e.fields.map(f => f.name));
|
|
279
|
+
for (const [name, fields] of _structureDecls) {
|
|
280
|
+
if (fields.length >= e.fields.length && fields.every(f => fieldNames.has(f.name) || f.type.kind === "optional")) {
|
|
281
|
+
ctorName = name;
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
if (!ctorName)
|
|
286
|
+
ctorName = _recordCtors.get(e.fields[0].name);
|
|
287
|
+
}
|
|
288
|
+
if (ctorName) {
|
|
289
|
+
const structFields = _structureDecls.get(ctorName);
|
|
290
|
+
if (structFields && e.fields.length < structFields.length) {
|
|
291
|
+
// Pad missing fields: match by name, fill None for optional
|
|
292
|
+
const provided = new Map(e.fields.map(f => [f.name, f]));
|
|
293
|
+
const vals = structFields.map(sf => {
|
|
294
|
+
const f = provided.get(sf.name);
|
|
295
|
+
if (f)
|
|
296
|
+
return emitExpr(f.value);
|
|
297
|
+
if (sf.type.kind === "optional") {
|
|
298
|
+
needsOptionType = true;
|
|
299
|
+
return "None";
|
|
300
|
+
}
|
|
301
|
+
return `/* missing: ${sf.name} */`;
|
|
302
|
+
});
|
|
303
|
+
return `${ctorName}(${vals.join(", ")})`;
|
|
304
|
+
}
|
|
305
|
+
const vals = e.fields.map(f => emitExpr(f.value));
|
|
172
306
|
return `${ctorName}(${vals.join(", ")})`;
|
|
307
|
+
}
|
|
308
|
+
const vals = e.fields.map(f => emitExpr(f.value));
|
|
173
309
|
return `(${vals.join(", ")})`;
|
|
174
310
|
}
|
|
175
311
|
case "if":
|
|
176
312
|
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
177
313
|
case "match": {
|
|
178
|
-
const
|
|
179
|
-
|
|
314
|
+
const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
|
|
315
|
+
const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
|
|
316
|
+
return `(match ${scrut} { ${arms.join(" ")} })`;
|
|
317
|
+
}
|
|
318
|
+
case "forall": {
|
|
319
|
+
// Collapse nested foralls: forall x :: forall y :: P → forall x, y :: P
|
|
320
|
+
const vars = [];
|
|
321
|
+
let body = e;
|
|
322
|
+
while (body.kind === "forall") {
|
|
323
|
+
const dty = tyToDafny(body.type);
|
|
324
|
+
const ann = dty === "string" ? "" : `: ${dty}`;
|
|
325
|
+
vars.push(`${body.var}${ann}`);
|
|
326
|
+
body = body.body;
|
|
327
|
+
}
|
|
328
|
+
return `forall ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
329
|
+
}
|
|
330
|
+
case "exists": {
|
|
331
|
+
// Collapse nested exists: exists x :: exists y :: P → exists x, y :: P
|
|
332
|
+
const vars = [];
|
|
333
|
+
let body = e;
|
|
334
|
+
while (body.kind === "exists") {
|
|
335
|
+
const dty = tyToDafny(body.type);
|
|
336
|
+
const ann = dty === "string" ? "" : `: ${dty}`;
|
|
337
|
+
vars.push(`${body.var}${ann}`);
|
|
338
|
+
body = body.body;
|
|
339
|
+
}
|
|
340
|
+
return `exists ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
180
341
|
}
|
|
181
|
-
case "forall": return `forall ${e.var}: ${leanTypeToDafny(e.type)} :: ${emitExpr(e.body)}`;
|
|
182
|
-
case "exists": return `exists ${e.var}: ${leanTypeToDafny(e.type)} :: ${emitExpr(e.body)}`;
|
|
183
342
|
case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
|
|
343
|
+
case "havoc": return "*";
|
|
184
344
|
}
|
|
185
345
|
}
|
|
186
346
|
/** Emit a pure expression with indentation for if/match/let. */
|
|
@@ -190,7 +350,8 @@ function emitPureExpr(e, indent) {
|
|
|
190
350
|
case "if":
|
|
191
351
|
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
192
352
|
case "match": {
|
|
193
|
-
const
|
|
353
|
+
const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
|
|
354
|
+
const lines = [`${pad}match ${scrut} {`];
|
|
194
355
|
for (const arm of e.arms) {
|
|
195
356
|
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
196
357
|
lines.push(emitPureExpr(arm.body, indent + 2));
|
|
@@ -212,9 +373,23 @@ function emitStmt(s, indent) {
|
|
|
212
373
|
const pad = " ".repeat(indent);
|
|
213
374
|
switch (s.kind) {
|
|
214
375
|
case "let":
|
|
376
|
+
// Record literal assigned to map type → emit as map[k := v, ...]
|
|
377
|
+
if (s.type.kind === "map" && s.value.kind === "record" && !s.value.spread) {
|
|
378
|
+
const entries = s.value.fields.map(f => `${emitExpr({ kind: "str", value: f.name })} := ${emitExpr(f.value)}`);
|
|
379
|
+
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(resolveTy(s.type))} := map[${entries.join(", ")}];`;
|
|
380
|
+
}
|
|
381
|
+
if (s.value.kind === "havoc" || s.value.kind === "emptyMap" || s.value.kind === "emptySet" ||
|
|
382
|
+
(s.value.kind === "arrayLiteral" && s.value.elems.length === 0))
|
|
383
|
+
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
215
384
|
return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
|
|
216
385
|
case "assign":
|
|
217
386
|
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
387
|
+
case "ghostLet":
|
|
388
|
+
return `${pad}ghost var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
389
|
+
case "ghostAssign":
|
|
390
|
+
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
391
|
+
case "assert":
|
|
392
|
+
return `${pad}assert ${emitExpr(s.expr)};`;
|
|
218
393
|
case "bind":
|
|
219
394
|
// Monadic bind shouldn't appear in Dafny mode, emit as regular assign
|
|
220
395
|
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
@@ -240,7 +415,8 @@ function emitStmt(s, indent) {
|
|
|
240
415
|
return out;
|
|
241
416
|
}
|
|
242
417
|
case "match": {
|
|
243
|
-
const
|
|
418
|
+
const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
|
|
419
|
+
const lines = [`${pad}match ${scrut} {`];
|
|
244
420
|
for (const arm of s.arms) {
|
|
245
421
|
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
246
422
|
lines.push(emitStmts(arm.body, indent + 2));
|
|
@@ -280,18 +456,23 @@ function emitStmt(s, indent) {
|
|
|
280
456
|
function emitDecl(d) {
|
|
281
457
|
switch (d.kind) {
|
|
282
458
|
case "inductive": {
|
|
459
|
+
const tp = d.typeParams?.length ? `<${d.typeParams.join(", ")}>` : "";
|
|
283
460
|
const ctors = d.constructors.map(c => {
|
|
284
461
|
if (c.fields.length === 0)
|
|
285
462
|
return escapeName(c.name);
|
|
286
463
|
return `${escapeName(c.name)}(${paramList(c.fields)})`;
|
|
287
464
|
});
|
|
288
|
-
return `datatype ${d.name} = ${ctors.join(" | ")}`;
|
|
465
|
+
return `datatype ${d.name}${tp} = ${ctors.join(" | ")}`;
|
|
289
466
|
}
|
|
290
467
|
case "structure": {
|
|
291
468
|
return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
|
|
292
469
|
}
|
|
470
|
+
case "type-alias": {
|
|
471
|
+
return `type ${d.name} = ${tyToDafny(d.target)}`;
|
|
472
|
+
}
|
|
293
473
|
case "def": {
|
|
294
|
-
const
|
|
474
|
+
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
475
|
+
const lines = [`function ${d.name}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
295
476
|
for (const r of d.requires)
|
|
296
477
|
lines.push(` requires ${emitExpr(r)}`);
|
|
297
478
|
lines.push(`{`);
|
|
@@ -300,7 +481,7 @@ function emitDecl(d) {
|
|
|
300
481
|
// Companion lemma for ensures (proof target for LLM)
|
|
301
482
|
if (d.ensures.length > 0) {
|
|
302
483
|
lines.push("");
|
|
303
|
-
lines.push(`lemma ${d.name}_ensures(${paramList(d.params)})`);
|
|
484
|
+
lines.push(`lemma ${d.name}_ensures${tp}(${paramList(d.params)})`);
|
|
304
485
|
for (const r of d.requires)
|
|
305
486
|
lines.push(` requires ${emitExpr(r)}`);
|
|
306
487
|
for (const e of d.ensures)
|
|
@@ -311,7 +492,8 @@ function emitDecl(d) {
|
|
|
311
492
|
return lines.join("\n");
|
|
312
493
|
}
|
|
313
494
|
case "method": {
|
|
314
|
-
const
|
|
495
|
+
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
496
|
+
const lines = [`method ${d.name}${tp}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
|
|
315
497
|
for (const r of d.requires)
|
|
316
498
|
lines.push(` requires ${emitExpr(r)}`);
|
|
317
499
|
for (const e of d.ensures)
|
|
@@ -321,6 +503,29 @@ function emitDecl(d) {
|
|
|
321
503
|
lines.push(`}`);
|
|
322
504
|
return lines.join("\n");
|
|
323
505
|
}
|
|
506
|
+
case "class": {
|
|
507
|
+
const lines = [`class ${d.name} {`];
|
|
508
|
+
for (const f of d.fields) {
|
|
509
|
+
lines.push(` var ${escapeName(f.name)}: ${tyToDafny(f.type)}`);
|
|
510
|
+
}
|
|
511
|
+
if (d.fields.length > 0 && d.methods.length > 0)
|
|
512
|
+
lines.push("");
|
|
513
|
+
for (const m of d.methods) {
|
|
514
|
+
lines.push(` method ${m.name}(${paramList(m.params)}) returns (res: ${tyToDafny(m.returnType)})`);
|
|
515
|
+
for (const r of m.requires)
|
|
516
|
+
lines.push(` requires ${emitExpr(r)}`);
|
|
517
|
+
for (const e of m.ensures)
|
|
518
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
519
|
+
lines.push(` {`);
|
|
520
|
+
lines.push(emitStmts(m.body, 2));
|
|
521
|
+
lines.push(` }`);
|
|
522
|
+
}
|
|
523
|
+
lines.push(`}`);
|
|
524
|
+
return lines.join("\n");
|
|
525
|
+
}
|
|
526
|
+
case "const": {
|
|
527
|
+
return `const ${escapeName(d.name)}: ${tyToDafny(d.type)} := ${emitExpr(d.value)}`;
|
|
528
|
+
}
|
|
324
529
|
case "namespace": {
|
|
325
530
|
// Dafny doesn't need namespaces — flatten declarations
|
|
326
531
|
return d.decls.map(emitDecl).join("\n\n");
|
|
@@ -330,8 +535,33 @@ function emitDecl(d) {
|
|
|
330
535
|
// ── File emission ───────────────────────────────────────────
|
|
331
536
|
// ── Preamble helpers ────────────────────────────────────────
|
|
332
537
|
let needsStringIndexOf = false;
|
|
538
|
+
let needsStringTrim = false;
|
|
539
|
+
let needsStringToLower = false;
|
|
540
|
+
let needsStringToUpper = false;
|
|
333
541
|
let needsJSFloorDiv = false;
|
|
334
|
-
let
|
|
542
|
+
let needsCeilReal = false;
|
|
543
|
+
let needsFloorReal = false;
|
|
544
|
+
let needsOptionType = false;
|
|
545
|
+
let needsSetToSeq = false;
|
|
546
|
+
let needsBitAnd = false;
|
|
547
|
+
let needsPow2 = false;
|
|
548
|
+
let needsNatToString = false;
|
|
549
|
+
let needsMathAbs = false;
|
|
550
|
+
let needsMathMin = false;
|
|
551
|
+
let needsMathMax = false;
|
|
552
|
+
const POW2 = `function Pow2(n: int): int
|
|
553
|
+
requires n >= 0
|
|
554
|
+
decreases n
|
|
555
|
+
{
|
|
556
|
+
if n == 0 then 1 else 2 * Pow2(n - 1)
|
|
557
|
+
}`;
|
|
558
|
+
const BIT_AND = `function BitAnd(x: int, y: int): int
|
|
559
|
+
requires x >= 0 && y >= 0
|
|
560
|
+
decreases x
|
|
561
|
+
{
|
|
562
|
+
if x == 0 || y == 0 then 0
|
|
563
|
+
else 2 * BitAnd(x / 2, y / 2) + (if x % 2 == 1 && y % 2 == 1 then 1 else 0)
|
|
564
|
+
}`;
|
|
335
565
|
const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
|
|
336
566
|
requires b != 0
|
|
337
567
|
{
|
|
@@ -342,6 +572,15 @@ const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
|
|
|
342
572
|
if a <= 0 then (-a) / (-b)
|
|
343
573
|
else -((a - 1) / (-b)) - 1
|
|
344
574
|
}`;
|
|
575
|
+
const FLOOR_REAL = `function FloorReal(x: real): int
|
|
576
|
+
{
|
|
577
|
+
x.Floor
|
|
578
|
+
}`;
|
|
579
|
+
const CEIL_REAL = `function CeilReal(x: real): int
|
|
580
|
+
{
|
|
581
|
+
if x == (x.Floor as real) then x.Floor
|
|
582
|
+
else x.Floor + 1
|
|
583
|
+
}`;
|
|
345
584
|
const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
|
|
346
585
|
{
|
|
347
586
|
StringIndexOfFrom(s, sub, 0)
|
|
@@ -354,38 +593,121 @@ function StringIndexOfFrom(s: string, sub: string, from: nat): int
|
|
|
354
593
|
else if s[from..from + |sub|] == sub then from as int
|
|
355
594
|
else StringIndexOfFrom(s, sub, from + 1)
|
|
356
595
|
}`;
|
|
596
|
+
const STRING_TRIM = `function StringTrimLeft(s: string): string
|
|
597
|
+
ensures |StringTrimLeft(s)| <= |s|
|
|
598
|
+
ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && StringTrimLeft(s)[0] != ' ')
|
|
599
|
+
decreases |s|
|
|
600
|
+
{
|
|
601
|
+
if |s| == 0 then ""
|
|
602
|
+
else if s[0] == ' ' then StringTrimLeft(s[1..])
|
|
603
|
+
else s
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function StringTrimRight(s: string): string
|
|
607
|
+
ensures |StringTrimRight(s)| <= |s|
|
|
608
|
+
decreases |s|
|
|
609
|
+
{
|
|
610
|
+
if |s| == 0 then ""
|
|
611
|
+
else if s[|s|-1] == ' ' then StringTrimRight(s[..|s|-1])
|
|
612
|
+
else s
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
function StringTrim(s: string): string
|
|
616
|
+
{
|
|
617
|
+
StringTrimRight(StringTrimLeft(s))
|
|
618
|
+
}`;
|
|
619
|
+
const STRING_TO_LOWER = `function StringToLower(s: string): string
|
|
620
|
+
ensures |StringToLower(s)| == |s|
|
|
621
|
+
decreases |s|
|
|
622
|
+
{
|
|
623
|
+
if |s| == 0 then ""
|
|
624
|
+
else
|
|
625
|
+
var c := s[0];
|
|
626
|
+
var lower := if 'A' <= c <= 'Z' then (c - 'A' + 'a') as char else c;
|
|
627
|
+
[lower] + StringToLower(s[1..])
|
|
628
|
+
}`;
|
|
629
|
+
const STRING_TO_UPPER = `function StringToUpper(s: string): string
|
|
630
|
+
ensures |StringToUpper(s)| == |s|
|
|
631
|
+
decreases |s|
|
|
632
|
+
{
|
|
633
|
+
if |s| == 0 then ""
|
|
634
|
+
else
|
|
635
|
+
var c := s[0];
|
|
636
|
+
var upper := if 'a' <= c <= 'z' then (c - 'a' + 'A') as char else c;
|
|
637
|
+
[upper] + StringToUpper(s[1..])
|
|
638
|
+
}`;
|
|
639
|
+
const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
|
|
640
|
+
const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
|
|
641
|
+
const NAT_TO_STRING = `function NatToString(n: nat): string
|
|
642
|
+
decreases n
|
|
643
|
+
{
|
|
644
|
+
var digit := ('0' as int + n % 10) as char;
|
|
645
|
+
if n < 10 then [digit]
|
|
646
|
+
else NatToString(n / 10) + [digit]
|
|
647
|
+
}`;
|
|
648
|
+
const MATH_ABS = `function MathAbs(x: int): nat { if x >= 0 then x else -x }`;
|
|
357
649
|
// ── Constructor and record helpers ───────────────────────────
|
|
358
650
|
let _recordCtors = new Map();
|
|
651
|
+
let _structureDecls = new Map();
|
|
652
|
+
let _declaredTypes = new Set();
|
|
359
653
|
function buildRecordCtorMap(decls) {
|
|
360
654
|
_recordCtors = new Map();
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
655
|
+
_structureDecls = new Map();
|
|
656
|
+
_declaredTypes = new Set();
|
|
657
|
+
function collectDecl(d) {
|
|
658
|
+
if (d.kind === "structure") {
|
|
659
|
+
_declaredTypes.add(d.name);
|
|
660
|
+
_structureDecls.set(d.name, d.fields);
|
|
661
|
+
if (d.fields.length > 0)
|
|
662
|
+
_recordCtors.set(d.fields[0].name, d.name);
|
|
663
|
+
}
|
|
664
|
+
if (d.kind === "inductive")
|
|
665
|
+
_declaredTypes.add(d.name);
|
|
666
|
+
if (d.kind === "type-alias")
|
|
667
|
+
_declaredTypes.add(d.name);
|
|
668
|
+
if (d.kind === "def")
|
|
669
|
+
_declaredTypes.add(d.name);
|
|
364
670
|
if (d.kind === "namespace")
|
|
365
|
-
for (const inner of d.decls)
|
|
366
|
-
|
|
367
|
-
_recordCtors.set(inner.fields[0].name, inner.name);
|
|
368
|
-
}
|
|
671
|
+
for (const inner of d.decls)
|
|
672
|
+
collectDecl(inner);
|
|
369
673
|
}
|
|
674
|
+
for (const d of decls)
|
|
675
|
+
collectDecl(d);
|
|
676
|
+
}
|
|
677
|
+
/** Resolve a Ty to a Dafny-safe type, falling back to string for undeclared user types. */
|
|
678
|
+
function resolveTy(ty) {
|
|
679
|
+
if (ty.kind === "user" && !_declaredTypes.has(ty.name))
|
|
680
|
+
return { kind: "string" };
|
|
681
|
+
if (ty.kind === "optional")
|
|
682
|
+
return { kind: "optional", inner: resolveTy(ty.inner) };
|
|
683
|
+
if (ty.kind === "array")
|
|
684
|
+
return { kind: "array", elem: resolveTy(ty.elem) };
|
|
685
|
+
if (ty.kind === "map")
|
|
686
|
+
return { kind: "map", key: resolveTy(ty.key), value: resolveTy(ty.value) };
|
|
687
|
+
if (ty.kind === "set")
|
|
688
|
+
return { kind: "set", elem: resolveTy(ty.elem) };
|
|
689
|
+
return ty;
|
|
370
690
|
}
|
|
371
691
|
function qualifyCtor(name, type) {
|
|
372
692
|
const rawName = name.replace(/^\./, "");
|
|
693
|
+
const mapped = CTOR_MAP[rawName] ?? escapeName(rawName);
|
|
373
694
|
if (type)
|
|
374
|
-
return `${type}.${
|
|
375
|
-
return
|
|
695
|
+
return `${type}.${mapped}`;
|
|
696
|
+
return mapped;
|
|
376
697
|
}
|
|
377
698
|
/** Translate a Lean match pattern to Dafny syntax.
|
|
378
699
|
* ".ctorName field1 field2" → "ctorName(field1, field2)"
|
|
379
700
|
* ".ctorName" → "ctorName"
|
|
380
701
|
* "_" → "_"
|
|
381
702
|
*/
|
|
703
|
+
const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
382
704
|
function translatePattern(pattern) {
|
|
383
705
|
if (pattern === "_")
|
|
384
706
|
return "_";
|
|
385
707
|
const m = pattern.match(/^\.(\w+)\s*(.*)$/);
|
|
386
708
|
if (!m)
|
|
387
709
|
return pattern;
|
|
388
|
-
const ctorName = escapeName(m[1]);
|
|
710
|
+
const ctorName = CTOR_MAP[m[1]] ?? escapeName(m[1]);
|
|
389
711
|
const fields = m[2].trim();
|
|
390
712
|
if (!fields)
|
|
391
713
|
return ctorName;
|
|
@@ -398,8 +720,20 @@ const PREAMBLES = {
|
|
|
398
720
|
export function emitDafnyFile(file, tsFileName) {
|
|
399
721
|
buildRecordCtorMap(file.decls);
|
|
400
722
|
needsStringIndexOf = false;
|
|
723
|
+
needsStringTrim = false;
|
|
724
|
+
needsStringToLower = false;
|
|
725
|
+
needsStringToUpper = false;
|
|
401
726
|
needsJSFloorDiv = false;
|
|
402
|
-
|
|
727
|
+
needsCeilReal = false;
|
|
728
|
+
needsFloorReal = false;
|
|
729
|
+
needsOptionType = false;
|
|
730
|
+
needsSetToSeq = false;
|
|
731
|
+
needsBitAnd = false;
|
|
732
|
+
needsPow2 = false;
|
|
733
|
+
needsNatToString = false;
|
|
734
|
+
needsMathAbs = false;
|
|
735
|
+
needsMathMin = false;
|
|
736
|
+
needsMathMax = false;
|
|
403
737
|
// Collect pure def names so we can skip their method wrappers
|
|
404
738
|
const pureDefs = new Set();
|
|
405
739
|
for (const d of file.decls) {
|
|
@@ -423,7 +757,9 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
423
757
|
}
|
|
424
758
|
catch (e) {
|
|
425
759
|
const name = "name" in decl ? decl.name : "unknown";
|
|
426
|
-
|
|
760
|
+
const msg = e.message;
|
|
761
|
+
console.error(`WARNING: skipping '${name}': ${msg}`);
|
|
762
|
+
declLines.push(`// LemmaScript: skipped ${name}`);
|
|
427
763
|
skipped.push(name);
|
|
428
764
|
}
|
|
429
765
|
}
|
|
@@ -434,16 +770,82 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
434
770
|
const lines = [];
|
|
435
771
|
if (tsFileName)
|
|
436
772
|
lines.push(`// Generated by lsc from ${tsFileName}`);
|
|
437
|
-
if (
|
|
438
|
-
lines.push("
|
|
773
|
+
if (needsOptionType) {
|
|
774
|
+
lines.push("");
|
|
775
|
+
lines.push("datatype Option<T> = None | Some(value: T)");
|
|
776
|
+
}
|
|
777
|
+
if (needsSetToSeq) {
|
|
778
|
+
lines.push("");
|
|
779
|
+
lines.push(`method SetToSeq<T>(s: set<T>) returns (res: seq<T>)
|
|
780
|
+
ensures forall x :: x in s <==> x in res
|
|
781
|
+
ensures |res| == |s|
|
|
782
|
+
{
|
|
783
|
+
var remaining := s;
|
|
784
|
+
res := [];
|
|
785
|
+
while remaining != {}
|
|
786
|
+
invariant remaining <= s
|
|
787
|
+
invariant forall x :: x in res <==> (x in s && x !in remaining)
|
|
788
|
+
invariant |res| + |remaining| == |s|
|
|
789
|
+
decreases remaining
|
|
790
|
+
{
|
|
791
|
+
var x :| x in remaining;
|
|
792
|
+
res := res + [x];
|
|
793
|
+
remaining := remaining - {x};
|
|
794
|
+
}
|
|
795
|
+
}`);
|
|
796
|
+
}
|
|
797
|
+
if (needsPow2) {
|
|
798
|
+
lines.push("");
|
|
799
|
+
lines.push(POW2);
|
|
800
|
+
}
|
|
801
|
+
if (needsBitAnd) {
|
|
802
|
+
lines.push("");
|
|
803
|
+
lines.push(BIT_AND);
|
|
804
|
+
}
|
|
439
805
|
if (needsJSFloorDiv) {
|
|
440
806
|
lines.push("");
|
|
441
807
|
lines.push(JS_FLOOR_DIV);
|
|
442
808
|
}
|
|
809
|
+
if (needsCeilReal) {
|
|
810
|
+
lines.push("");
|
|
811
|
+
lines.push(CEIL_REAL);
|
|
812
|
+
}
|
|
813
|
+
if (needsFloorReal) {
|
|
814
|
+
lines.push("");
|
|
815
|
+
lines.push(FLOOR_REAL);
|
|
816
|
+
}
|
|
443
817
|
if (needsStringIndexOf) {
|
|
444
818
|
lines.push("");
|
|
445
819
|
lines.push(PREAMBLES.StringIndexOf);
|
|
446
820
|
}
|
|
821
|
+
if (needsStringTrim) {
|
|
822
|
+
lines.push("");
|
|
823
|
+
lines.push(STRING_TRIM);
|
|
824
|
+
}
|
|
825
|
+
if (needsStringToLower) {
|
|
826
|
+
lines.push("");
|
|
827
|
+
lines.push(STRING_TO_LOWER);
|
|
828
|
+
}
|
|
829
|
+
if (needsStringToUpper) {
|
|
830
|
+
lines.push("");
|
|
831
|
+
lines.push(STRING_TO_UPPER);
|
|
832
|
+
}
|
|
833
|
+
if (needsNatToString) {
|
|
834
|
+
lines.push("");
|
|
835
|
+
lines.push(NAT_TO_STRING);
|
|
836
|
+
}
|
|
837
|
+
if (needsMathAbs) {
|
|
838
|
+
lines.push("");
|
|
839
|
+
lines.push(MATH_ABS);
|
|
840
|
+
}
|
|
841
|
+
if (needsMathMin) {
|
|
842
|
+
lines.push("");
|
|
843
|
+
lines.push(MATH_MIN);
|
|
844
|
+
}
|
|
845
|
+
if (needsMathMax) {
|
|
846
|
+
lines.push("");
|
|
847
|
+
lines.push(MATH_MAX);
|
|
848
|
+
}
|
|
447
849
|
lines.push(...declLines);
|
|
448
850
|
return lines.join("\n") + "\n";
|
|
449
851
|
}
|