lemmascript 0.1.0 → 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/README.md +24 -31
- package/package.json +1 -1
- package/tools/dist/dafny-commands.js +46 -52
- package/tools/dist/dafny-emit.js +362 -73
- package/tools/dist/extract.js +386 -15
- package/tools/dist/ir.js +2 -2
- package/tools/dist/lean-commands.js +35 -0
- package/tools/dist/lean-emit.js +393 -0
- package/tools/dist/lsc.js +44 -43
- package/tools/dist/resolve.js +285 -19
- package/tools/dist/specparser.js +61 -7
- package/tools/dist/transform.js +533 -187
- package/tools/dist/types.js +46 -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,7 +45,7 @@ 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 = {
|
|
@@ -66,42 +59,101 @@ function emitExpr(e) {
|
|
|
66
59
|
case "var": return escapeName(e.name);
|
|
67
60
|
case "num": return `${e.value}`;
|
|
68
61
|
case "bool": return e.value ? "true" : "false";
|
|
69
|
-
case "str": return `"${e.value}"`;
|
|
62
|
+
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
70
63
|
case "constructor": return qualifyCtor(e.name, e.type);
|
|
71
64
|
case "arrayLiteral":
|
|
72
65
|
if (e.elems.length === 0)
|
|
73
66
|
return `[]`;
|
|
74
67
|
return `[${e.elems.map(emitExpr).join(", ")}]`;
|
|
75
|
-
case "
|
|
68
|
+
case "emptyMap": return `map[]`;
|
|
69
|
+
case "emptySet": return `{}`;
|
|
70
|
+
case "methodCall": {
|
|
76
71
|
const obj = emitExpr(e.obj);
|
|
77
72
|
const args = e.args.map(emitExpr);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
+
}
|
|
85
106
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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)`;
|
|
89
133
|
}
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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})`;
|
|
93
146
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
if (
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
return `(exists ${p} :: ${p} in ${obj} && ${body})`;
|
|
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]}})`;
|
|
103
155
|
}
|
|
104
|
-
|
|
156
|
+
throw new Error(`Unsupported Dafny method call: .${e.method}() on ${ty}`);
|
|
105
157
|
}
|
|
106
158
|
case "lambda": {
|
|
107
159
|
const ps = paramList(e.params);
|
|
@@ -127,6 +179,45 @@ function emitExpr(e) {
|
|
|
127
179
|
const pred = `${emitExpr(e.left)}.${ctorName}?`;
|
|
128
180
|
return op === "!=" ? `(!${pred})` : pred;
|
|
129
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
|
+
}
|
|
130
221
|
return `(${emitExpr(e.left)} ${op} ${emitExpr(e.right)})`;
|
|
131
222
|
}
|
|
132
223
|
case "implies": {
|
|
@@ -135,23 +226,26 @@ function emitExpr(e) {
|
|
|
135
226
|
}
|
|
136
227
|
case "app": {
|
|
137
228
|
const args = e.args.map(emitExpr);
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
return `StringIndexOf(${args.join(", ")})`;
|
|
229
|
+
if (e.fn === "SetToSeq") {
|
|
230
|
+
needsSetToSeq = true;
|
|
231
|
+
return `SetToSeq(${args.join(", ")})`;
|
|
142
232
|
}
|
|
143
|
-
if (e.fn === "
|
|
144
|
-
return
|
|
145
|
-
if (e.fn === "SeqPush")
|
|
146
|
-
return `(${args[0]} + [${args[1]}])`;
|
|
233
|
+
if (e.fn === "BigInt" || e.fn === "Number")
|
|
234
|
+
return args[0]; // identity: both map to int
|
|
147
235
|
if (e.fn === "JSFloorDiv")
|
|
148
236
|
needsJSFloorDiv = true;
|
|
237
|
+
if (e.fn === "CeilReal")
|
|
238
|
+
needsCeilReal = true;
|
|
239
|
+
if (e.fn === "FloorReal")
|
|
240
|
+
needsFloorReal = true;
|
|
149
241
|
return `${e.fn}(${args.join(", ")})`;
|
|
150
242
|
}
|
|
151
243
|
case "field": {
|
|
152
244
|
const obj = emitExpr(e.obj);
|
|
153
|
-
if (e.field === "size" || e.field === "length")
|
|
245
|
+
if (e.field === "size" || e.field === "length" || e.field === "collectionSize")
|
|
154
246
|
return `|${obj}|`;
|
|
247
|
+
if (e.field === "keys")
|
|
248
|
+
return `${obj}.Keys`;
|
|
155
249
|
if (e.field === "toNat")
|
|
156
250
|
return obj;
|
|
157
251
|
return `${obj}.${escapeName(e.field)}`;
|
|
@@ -175,12 +269,36 @@ function emitExpr(e) {
|
|
|
175
269
|
case "if":
|
|
176
270
|
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
177
271
|
case "match": {
|
|
178
|
-
const
|
|
179
|
-
|
|
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)}`;
|
|
180
299
|
}
|
|
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
300
|
case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
|
|
301
|
+
case "havoc": return "*";
|
|
184
302
|
}
|
|
185
303
|
}
|
|
186
304
|
/** Emit a pure expression with indentation for if/match/let. */
|
|
@@ -190,7 +308,8 @@ function emitPureExpr(e, indent) {
|
|
|
190
308
|
case "if":
|
|
191
309
|
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
192
310
|
case "match": {
|
|
193
|
-
const
|
|
311
|
+
const scrut = typeof e.scrutinee === "string" ? escapeName(e.scrutinee) : emitExpr(e.scrutinee);
|
|
312
|
+
const lines = [`${pad}match ${scrut} {`];
|
|
194
313
|
for (const arm of e.arms) {
|
|
195
314
|
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
196
315
|
lines.push(emitPureExpr(arm.body, indent + 2));
|
|
@@ -212,9 +331,17 @@ function emitStmt(s, indent) {
|
|
|
212
331
|
const pad = " ".repeat(indent);
|
|
213
332
|
switch (s.kind) {
|
|
214
333
|
case "let":
|
|
334
|
+
if (s.value.kind === "havoc")
|
|
335
|
+
return `${pad}var ${escapeName(s.name)}: ${tyToDafny(s.type)} := ${emitExpr(s.value)};`;
|
|
215
336
|
return `${pad}var ${escapeName(s.name)} := ${emitExpr(s.value)};`;
|
|
216
337
|
case "assign":
|
|
217
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)};`;
|
|
218
345
|
case "bind":
|
|
219
346
|
// Monadic bind shouldn't appear in Dafny mode, emit as regular assign
|
|
220
347
|
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
@@ -240,7 +367,8 @@ function emitStmt(s, indent) {
|
|
|
240
367
|
return out;
|
|
241
368
|
}
|
|
242
369
|
case "match": {
|
|
243
|
-
const
|
|
370
|
+
const scrut = typeof s.scrutinee === "string" ? escapeName(s.scrutinee) : emitExpr(s.scrutinee);
|
|
371
|
+
const lines = [`${pad}match ${scrut} {`];
|
|
244
372
|
for (const arm of s.arms) {
|
|
245
373
|
lines.push(`${pad} case ${translatePattern(arm.pattern)} =>`);
|
|
246
374
|
lines.push(emitStmts(arm.body, indent + 2));
|
|
@@ -291,7 +419,7 @@ function emitDecl(d) {
|
|
|
291
419
|
return `datatype ${d.name} = ${d.name}(${paramList(d.fields)})`;
|
|
292
420
|
}
|
|
293
421
|
case "def": {
|
|
294
|
-
const lines = [`function ${d.name}(${paramList(d.params)}): ${
|
|
422
|
+
const lines = [`function ${d.name}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
295
423
|
for (const r of d.requires)
|
|
296
424
|
lines.push(` requires ${emitExpr(r)}`);
|
|
297
425
|
lines.push(`{`);
|
|
@@ -311,7 +439,7 @@ function emitDecl(d) {
|
|
|
311
439
|
return lines.join("\n");
|
|
312
440
|
}
|
|
313
441
|
case "method": {
|
|
314
|
-
const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${
|
|
442
|
+
const lines = [`method ${d.name}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
|
|
315
443
|
for (const r of d.requires)
|
|
316
444
|
lines.push(` requires ${emitExpr(r)}`);
|
|
317
445
|
for (const e of d.ensures)
|
|
@@ -321,6 +449,29 @@ function emitDecl(d) {
|
|
|
321
449
|
lines.push(`}`);
|
|
322
450
|
return lines.join("\n");
|
|
323
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
|
+
}
|
|
324
475
|
case "namespace": {
|
|
325
476
|
// Dafny doesn't need namespaces — flatten declarations
|
|
326
477
|
return d.decls.map(emitDecl).join("\n\n");
|
|
@@ -330,8 +481,30 @@ function emitDecl(d) {
|
|
|
330
481
|
// ── File emission ───────────────────────────────────────────
|
|
331
482
|
// ── Preamble helpers ────────────────────────────────────────
|
|
332
483
|
let needsStringIndexOf = false;
|
|
484
|
+
let needsStringTrim = false;
|
|
485
|
+
let needsStringToLower = false;
|
|
486
|
+
let needsStringToUpper = false;
|
|
333
487
|
let needsJSFloorDiv = false;
|
|
488
|
+
let needsCeilReal = false;
|
|
489
|
+
let needsFloorReal = false;
|
|
334
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
|
+
}`;
|
|
335
508
|
const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
|
|
336
509
|
requires b != 0
|
|
337
510
|
{
|
|
@@ -342,6 +515,15 @@ const JS_FLOOR_DIV = `function JSFloorDiv(a: int, b: int): int
|
|
|
342
515
|
if a <= 0 then (-a) / (-b)
|
|
343
516
|
else -((a - 1) / (-b)) - 1
|
|
344
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
|
+
}`;
|
|
345
527
|
const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
|
|
346
528
|
{
|
|
347
529
|
StringIndexOfFrom(s, sub, 0)
|
|
@@ -354,6 +536,49 @@ function StringIndexOfFrom(s: string, sub: string, from: nat): int
|
|
|
354
536
|
else if s[from..from + |sub|] == sub then from as int
|
|
355
537
|
else StringIndexOfFrom(s, sub, from + 1)
|
|
356
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
|
+
}`;
|
|
357
582
|
// ── Constructor and record helpers ───────────────────────────
|
|
358
583
|
let _recordCtors = new Map();
|
|
359
584
|
function buildRecordCtorMap(decls) {
|
|
@@ -379,13 +604,14 @@ function qualifyCtor(name, type) {
|
|
|
379
604
|
* ".ctorName" → "ctorName"
|
|
380
605
|
* "_" → "_"
|
|
381
606
|
*/
|
|
607
|
+
const CTOR_MAP = { "some": "Some", "none": "None" };
|
|
382
608
|
function translatePattern(pattern) {
|
|
383
609
|
if (pattern === "_")
|
|
384
610
|
return "_";
|
|
385
611
|
const m = pattern.match(/^\.(\w+)\s*(.*)$/);
|
|
386
612
|
if (!m)
|
|
387
613
|
return pattern;
|
|
388
|
-
const ctorName = escapeName(m[1]);
|
|
614
|
+
const ctorName = CTOR_MAP[m[1]] ?? escapeName(m[1]);
|
|
389
615
|
const fields = m[2].trim();
|
|
390
616
|
if (!fields)
|
|
391
617
|
return ctorName;
|
|
@@ -398,8 +624,17 @@ const PREAMBLES = {
|
|
|
398
624
|
export function emitDafnyFile(file, tsFileName) {
|
|
399
625
|
buildRecordCtorMap(file.decls);
|
|
400
626
|
needsStringIndexOf = false;
|
|
627
|
+
needsStringTrim = false;
|
|
628
|
+
needsStringToLower = false;
|
|
629
|
+
needsStringToUpper = false;
|
|
401
630
|
needsJSFloorDiv = false;
|
|
631
|
+
needsCeilReal = false;
|
|
632
|
+
needsFloorReal = false;
|
|
402
633
|
needsStdCollections = false;
|
|
634
|
+
needsOptionType = false;
|
|
635
|
+
needsSetToSeq = false;
|
|
636
|
+
needsBitAnd = false;
|
|
637
|
+
needsPow2 = false;
|
|
403
638
|
// Collect pure def names so we can skip their method wrappers
|
|
404
639
|
const pureDefs = new Set();
|
|
405
640
|
for (const d of file.decls) {
|
|
@@ -423,7 +658,9 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
423
658
|
}
|
|
424
659
|
catch (e) {
|
|
425
660
|
const name = "name" in decl ? decl.name : "unknown";
|
|
426
|
-
|
|
661
|
+
const msg = e.message;
|
|
662
|
+
console.error(`WARNING: skipping '${name}': ${msg}`);
|
|
663
|
+
declLines.push(`// LemmaScript: skipped ${name}`);
|
|
427
664
|
skipped.push(name);
|
|
428
665
|
}
|
|
429
666
|
}
|
|
@@ -436,14 +673,66 @@ export function emitDafnyFile(file, tsFileName) {
|
|
|
436
673
|
lines.push(`// Generated by lsc from ${tsFileName}`);
|
|
437
674
|
if (needsStdCollections)
|
|
438
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
|
+
}
|
|
439
708
|
if (needsJSFloorDiv) {
|
|
440
709
|
lines.push("");
|
|
441
710
|
lines.push(JS_FLOOR_DIV);
|
|
442
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
|
+
}
|
|
443
720
|
if (needsStringIndexOf) {
|
|
444
721
|
lines.push("");
|
|
445
722
|
lines.push(PREAMBLES.StringIndexOf);
|
|
446
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
|
+
}
|
|
447
736
|
lines.push(...declLines);
|
|
448
737
|
return lines.join("\n") + "\n";
|
|
449
738
|
}
|