lemmascript 0.3.3 → 0.5.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 +20 -13
- package/package.json +4 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +302 -17
- package/tools/dist/extract.js +1087 -181
- package/tools/dist/info-command.js +38 -0
- package/tools/dist/lean-emit.js +81 -5
- package/tools/dist/lsc.js +29 -9
- package/tools/dist/narrow.js +932 -0
- package/tools/dist/peephole.js +451 -0
- package/tools/dist/resolve.js +680 -258
- package/tools/dist/specparser.js +18 -2
- package/tools/dist/transform.js +597 -441
- package/tools/dist/types.js +128 -69
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -18,6 +18,7 @@ function tyToDafny(ty) {
|
|
|
18
18
|
return `Option<${tyToDafny(ty.inner)}>`;
|
|
19
19
|
}
|
|
20
20
|
case "user": return ty.name;
|
|
21
|
+
case "fn": return `(${ty.params.map(tyToDafny).join(", ")}) -> ${tyToDafny(ty.result)}`;
|
|
21
22
|
case "unknown": return "int";
|
|
22
23
|
}
|
|
23
24
|
}
|
|
@@ -36,6 +37,10 @@ const DAFNY_KEYWORDS = new Set([
|
|
|
36
37
|
"by", "calc", "reveal",
|
|
37
38
|
]);
|
|
38
39
|
function escapeName(name) {
|
|
40
|
+
// \result is carried through the IR as the var name "\\result"; render it
|
|
41
|
+
// as Dafny's canonical return-value identifier.
|
|
42
|
+
if (name === "\\result")
|
|
43
|
+
return "res";
|
|
39
44
|
if (DAFNY_KEYWORDS.has(name))
|
|
40
45
|
return `${name}_`;
|
|
41
46
|
// Dafny doesn't allow identifiers starting with _
|
|
@@ -47,6 +52,13 @@ function escapeName(name) {
|
|
|
47
52
|
function paramList(params) {
|
|
48
53
|
return params.map(p => `${escapeName(p.name)}: ${tyToDafny(p.type)}`).join(", ");
|
|
49
54
|
}
|
|
55
|
+
/** Format a method signature header, omitting `returns` for void methods.
|
|
56
|
+
* Dafny's definite-assignment rule rejects unassigned out-parameters, so a
|
|
57
|
+
* `returns (res: ())` on a void method fails verification. */
|
|
58
|
+
function methodHeader(prefix, params, returnType) {
|
|
59
|
+
const sig = `${prefix}(${paramList(params)})`;
|
|
60
|
+
return returnType.kind === "void" ? sig : `${sig} returns (res: ${tyToDafny(returnType)})`;
|
|
61
|
+
}
|
|
50
62
|
// ── Lean op → Dafny op ─────────────────────────────────────
|
|
51
63
|
const OP_MAP = {
|
|
52
64
|
"=": "==", "≠": "!=", "≥": ">=", "≤": "<=",
|
|
@@ -71,19 +83,42 @@ function emitQuantifier(e, keyword) {
|
|
|
71
83
|
}
|
|
72
84
|
return `${keyword} ${vars.join(", ")} :: ${emitExpr(body)}`;
|
|
73
85
|
}
|
|
86
|
+
// Dafny's `forall`/`exists ::` body extends as far as possible. So
|
|
87
|
+
// `(forall i :: P(i)) <op> Q` (or `... ==> Q`) would parse with the operator
|
|
88
|
+
// absorbed into the body. Wrap a quantifier in parens to terminate its body
|
|
89
|
+
// before the operator. Only the LEFT operand needs this — a quantifier in
|
|
90
|
+
// right-operand position is fine because its body correctly spans the rest.
|
|
91
|
+
function wrapQuantifier(sub) {
|
|
92
|
+
const inner = emitExpr(sub);
|
|
93
|
+
return (sub.kind === "forall" || sub.kind === "exists") ? `(${inner})` : inner;
|
|
94
|
+
}
|
|
74
95
|
function emitExpr(e) {
|
|
75
96
|
switch (e.kind) {
|
|
76
97
|
case "var": return e.name === "undefined" ? "None" : escapeName(e.name);
|
|
77
98
|
case "num": return `${e.value}`;
|
|
78
99
|
case "bool": return e.value ? "true" : "false";
|
|
79
100
|
case "str": return `"${e.value.replace(/\\/g, '\\\\').replace(/"/g, '\\"').replace(/\n/g, '\\n')}"`;
|
|
80
|
-
case "constructor":
|
|
101
|
+
case "constructor": {
|
|
102
|
+
// Option constructors (Some/None) may appear in inferred positions
|
|
103
|
+
// (e.g. lambda result) without an explicit `optional<T>` in any
|
|
104
|
+
// signature, so request the preamble here.
|
|
105
|
+
if (e.type === "Option")
|
|
106
|
+
needPreamble("OptionType");
|
|
107
|
+
const head = qualifyCtor(e.name, e.type);
|
|
108
|
+
if (!e.args || e.args.length === 0)
|
|
109
|
+
return head;
|
|
110
|
+
return `${head}(${e.args.map(emitExpr).join(", ")})`;
|
|
111
|
+
}
|
|
81
112
|
case "arrayLiteral":
|
|
82
113
|
if (e.elems.length === 0)
|
|
83
114
|
return `[]`;
|
|
84
115
|
return `[${e.elems.map(emitExpr).join(", ")}]`;
|
|
85
116
|
case "emptyMap": return `map[]`;
|
|
86
117
|
case "emptySet": return `{}`;
|
|
118
|
+
case "mapLiteral": {
|
|
119
|
+
const entries = e.entries.map(en => `${emitExpr(en.key)} := ${emitExpr(en.value)}`);
|
|
120
|
+
return `map[${entries.join(", ")}]`;
|
|
121
|
+
}
|
|
87
122
|
case "methodCall": {
|
|
88
123
|
const obj = emitExpr(e.obj);
|
|
89
124
|
const args = e.args.map(emitExpr);
|
|
@@ -102,16 +137,45 @@ function emitExpr(e) {
|
|
|
102
137
|
return `(${obj} + [${args[0]}])`;
|
|
103
138
|
if (e.method === "concat")
|
|
104
139
|
return `(${obj} + [${args[0]}])`;
|
|
140
|
+
// No-arg slice is a full copy; Dafny seq is an immutable value type, so
|
|
141
|
+
// the copy is just the seq itself (the idiom for "copy then mutate").
|
|
142
|
+
if (e.method === "slice" && args.length === 0)
|
|
143
|
+
return obj;
|
|
105
144
|
if (e.method === "slice" && args.length === 1)
|
|
106
145
|
return `${obj}[${args[0]}..]`;
|
|
107
|
-
if (e.method === "slice" && args.length === 2)
|
|
146
|
+
if (e.method === "slice" && args.length === 2) {
|
|
147
|
+
// JS slice clamps both bounds; Dafny requires `0 <= lo <= hi <= |s|`.
|
|
148
|
+
// Direct slice is default (matches existing case studies that wrote
|
|
149
|
+
// bounded calls). Files needing JS clamping opt in via `//@ safe-slice`.
|
|
150
|
+
if (_useSafeSlice) {
|
|
151
|
+
needPreamble("SafeSlice");
|
|
152
|
+
return `SafeSlice(${obj}, ${args[0]}, ${args[1]})`;
|
|
153
|
+
}
|
|
108
154
|
return `${obj}[${args[0]}..${args[1]}]`;
|
|
155
|
+
}
|
|
109
156
|
if (e.method === "map")
|
|
110
157
|
return `Std.Collections.Seq.Map(${args[0]}, ${obj})`;
|
|
111
158
|
if (e.method === "filter")
|
|
112
159
|
return `Std.Collections.Seq.Filter(${args[0]}, ${obj})`;
|
|
113
160
|
if (e.method === "every")
|
|
114
161
|
return `Std.Collections.Seq.All(${obj}, ${args[0]})`;
|
|
162
|
+
if (e.method === "findLast") {
|
|
163
|
+
needPreamble("OptionType");
|
|
164
|
+
needPreamble("SeqFindLast");
|
|
165
|
+
return `SeqFindLast(${obj}, ${args[0]})`;
|
|
166
|
+
}
|
|
167
|
+
if (e.method === "findIndex") {
|
|
168
|
+
needPreamble("SeqFindIndex");
|
|
169
|
+
return `SeqFindIndex(${obj}, ${args[0]})`;
|
|
170
|
+
}
|
|
171
|
+
if (e.method === "flat" && args.length === 0) {
|
|
172
|
+
needPreamble("SeqFlatten");
|
|
173
|
+
return `SeqFlatten(${obj})`;
|
|
174
|
+
}
|
|
175
|
+
if (e.method === "join") {
|
|
176
|
+
needPreamble("SeqJoin");
|
|
177
|
+
return `SeqJoin(${obj}, ${args[0]})`;
|
|
178
|
+
}
|
|
115
179
|
if (e.method === "some" && e.args[0].kind === "lambda" &&
|
|
116
180
|
e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
|
|
117
181
|
const lam = e.args[0];
|
|
@@ -127,14 +191,46 @@ function emitExpr(e) {
|
|
|
127
191
|
if (ty === "string") {
|
|
128
192
|
if (e.method === "indexOf") {
|
|
129
193
|
needPreamble("StringIndexOf");
|
|
194
|
+
if (args.length === 2)
|
|
195
|
+
return `StringIndexOfFrom(${obj}, ${args[0]}, ${args[1]})`;
|
|
130
196
|
return `StringIndexOf(${obj}, ${args[0]})`;
|
|
131
197
|
}
|
|
132
|
-
if (e.method === "
|
|
198
|
+
if (e.method === "split") {
|
|
199
|
+
needPreamble("StringSplit");
|
|
200
|
+
return `StringSplit(${obj}, ${args[0]})`;
|
|
201
|
+
}
|
|
202
|
+
if (e.method === "slice") {
|
|
203
|
+
// JS negative index: arr.slice(0, -N) → arr[0..|arr|-N]. After
|
|
204
|
+
// transform, unary minus on a numeric literal is folded to a
|
|
205
|
+
// negative `num` IR node, so check for that here.
|
|
206
|
+
const negVal = (a) => a.kind === "num" && a.value < 0 ? -a.value : null;
|
|
207
|
+
const loN = negVal(e.args[0]);
|
|
208
|
+
const loEx = loN !== null ? `|${obj}|-${loN}` : args[0];
|
|
209
|
+
if (args.length === 1)
|
|
210
|
+
return `${obj}[${loEx}..]`;
|
|
211
|
+
const hiN = negVal(e.args[1]);
|
|
212
|
+
const hiEx = hiN !== null ? `|${obj}|-${hiN}` : args[1];
|
|
213
|
+
return `${obj}[${loEx}..${hiEx}]`;
|
|
214
|
+
}
|
|
215
|
+
if (e.method === "substring") {
|
|
216
|
+
if (args.length === 1)
|
|
217
|
+
return `${obj}[${args[0]}..]`;
|
|
133
218
|
return `${obj}[${args[0]}..${args[1]}]`;
|
|
219
|
+
}
|
|
220
|
+
if (e.method === "endsWith")
|
|
221
|
+
return `(|${obj}| >= |${args[0]}| && ${obj}[|${obj}|-|${args[0]}|..] == ${args[0]})`;
|
|
134
222
|
if (e.method === "trim") {
|
|
135
223
|
needPreamble("StringTrim");
|
|
136
224
|
return `StringTrim(${obj})`;
|
|
137
225
|
}
|
|
226
|
+
if (e.method === "trimEnd") {
|
|
227
|
+
needPreamble("StringTrim");
|
|
228
|
+
return `StringTrimRight(${obj})`;
|
|
229
|
+
}
|
|
230
|
+
if (e.method === "trimStart") {
|
|
231
|
+
needPreamble("StringTrim");
|
|
232
|
+
return `StringTrimLeft(${obj})`;
|
|
233
|
+
}
|
|
138
234
|
if (e.method === "toLowerCase") {
|
|
139
235
|
needPreamble("StringToLower");
|
|
140
236
|
return `StringToLower(${obj})`;
|
|
@@ -147,6 +243,8 @@ function emitExpr(e) {
|
|
|
147
243
|
needPreamble("StringIndexOf");
|
|
148
244
|
return `(StringIndexOf(${obj}, ${args[0]}) >= 0)`;
|
|
149
245
|
}
|
|
246
|
+
if (e.method === "startsWith")
|
|
247
|
+
return `(|${obj}| >= |${args[0]}| && ${obj}[..|${args[0]}|] == ${args[0]})`;
|
|
150
248
|
if (e.method === "charCodeAt")
|
|
151
249
|
return `(${obj}[${args[0]}] as int)`;
|
|
152
250
|
}
|
|
@@ -173,6 +271,20 @@ function emitExpr(e) {
|
|
|
173
271
|
return `(${obj} + {${args[0]}})`;
|
|
174
272
|
if (e.method === "delete")
|
|
175
273
|
return `(${obj} - {${args[0]}})`;
|
|
274
|
+
// `.filter(pred)` on a set: extract collapses the JS idiom
|
|
275
|
+
// `new Set([...s].filter(p))` into `s.filter(p)` with set receiver
|
|
276
|
+
// (the spread → array → set round-trip is identity over set
|
|
277
|
+
// semantics). Lower to Dafny set-builder: `set x | x in s && p(x)`.
|
|
278
|
+
if (e.method === "filter" && e.args.length === 1 && e.args[0].kind === "lambda" &&
|
|
279
|
+
e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
|
|
280
|
+
const lam = e.args[0];
|
|
281
|
+
const ret = lam.body[0];
|
|
282
|
+
if (ret.kind !== "return")
|
|
283
|
+
throw new Error("unreachable");
|
|
284
|
+
const p = escapeName(lam.params[0]?.name ?? "x");
|
|
285
|
+
const body = emitExpr(ret.value);
|
|
286
|
+
return `(set ${p} | ${p} in ${obj} && ${body})`;
|
|
287
|
+
}
|
|
176
288
|
}
|
|
177
289
|
throw new Error(`Unsupported Dafny method call: .${e.method}() on ${ty}`);
|
|
178
290
|
}
|
|
@@ -239,10 +351,10 @@ function emitExpr(e) {
|
|
|
239
351
|
return `(${left} ${op} ${right})`;
|
|
240
352
|
}
|
|
241
353
|
}
|
|
242
|
-
return `(${
|
|
354
|
+
return `(${wrapQuantifier(e.left)} ${op} ${emitExpr(e.right)})`;
|
|
243
355
|
}
|
|
244
356
|
case "implies": {
|
|
245
|
-
const parts = [...e.premises.map(
|
|
357
|
+
const parts = [...e.premises.map(wrapQuantifier), emitExpr(e.conclusion)];
|
|
246
358
|
return `(${parts.join(" ==> ")})`;
|
|
247
359
|
}
|
|
248
360
|
case "app": {
|
|
@@ -270,6 +382,14 @@ function emitExpr(e) {
|
|
|
270
382
|
needPreamble("MathMin");
|
|
271
383
|
if (e.fn === "MathMax")
|
|
272
384
|
needPreamble("MathMax");
|
|
385
|
+
if (e.fn === "MaxOfSeq") {
|
|
386
|
+
needPreamble("MathMax");
|
|
387
|
+
needPreamble("MaxOfSeq");
|
|
388
|
+
}
|
|
389
|
+
if (e.fn === "MinOfSeq") {
|
|
390
|
+
needPreamble("MathMin");
|
|
391
|
+
needPreamble("MinOfSeq");
|
|
392
|
+
}
|
|
273
393
|
return `${escapeName(e.fn)}(${args.join(", ")})`;
|
|
274
394
|
}
|
|
275
395
|
case "field": {
|
|
@@ -285,10 +405,19 @@ function emitExpr(e) {
|
|
|
285
405
|
case "toNat":
|
|
286
406
|
// Dafny doesn't need toNat — just emit the inner expression
|
|
287
407
|
return emitExpr(e.expr);
|
|
288
|
-
case "index":
|
|
289
|
-
|
|
408
|
+
case "index": {
|
|
409
|
+
const obj = emitExpr(e.arr);
|
|
410
|
+
const idx = emitExpr(e.idx);
|
|
411
|
+
// Plain seq/map subscript. For maps where the result is meant to be
|
|
412
|
+
// `Option<V>` (the TS `Record<K,V>[k]` shape), transform should have
|
|
413
|
+
// wrapped this in an Option-coercion; here we just emit the subscript.
|
|
414
|
+
return `${obj}[${idx}]`;
|
|
415
|
+
}
|
|
290
416
|
case "record": {
|
|
291
417
|
if (e.spread) {
|
|
418
|
+
if (e.fields.length === 0) {
|
|
419
|
+
return emitExpr(e.spread);
|
|
420
|
+
}
|
|
292
421
|
const updates = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
293
422
|
return `${emitExpr(e.spread)}.(${updates.join(", ")})`;
|
|
294
423
|
}
|
|
@@ -307,8 +436,9 @@ function emitExpr(e) {
|
|
|
307
436
|
}
|
|
308
437
|
if (ctorName) {
|
|
309
438
|
const structFields = _structureDecls.get(ctorName);
|
|
310
|
-
|
|
311
|
-
|
|
439
|
+
// Always reorder by struct field name — TS object literal order ≠ Dafny
|
|
440
|
+
// positional order. Pad missing optional fields with None.
|
|
441
|
+
if (structFields) {
|
|
312
442
|
const provided = new Map(e.fields.map(f => [f.name, f]));
|
|
313
443
|
const vals = structFields.map(sf => {
|
|
314
444
|
const f = provided.get(sf.name);
|
|
@@ -331,7 +461,7 @@ function emitExpr(e) {
|
|
|
331
461
|
return `(${vals.join(", ")})`;
|
|
332
462
|
}
|
|
333
463
|
case "if":
|
|
334
|
-
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
464
|
+
return `(if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)})`;
|
|
335
465
|
case "match": {
|
|
336
466
|
const scrut = emitScrutinee(e.scrutinee);
|
|
337
467
|
const arms = e.arms.map(a => `case ${translatePattern(a.pattern)} => ${emitExpr(a.body)}`);
|
|
@@ -339,7 +469,7 @@ function emitExpr(e) {
|
|
|
339
469
|
}
|
|
340
470
|
case "forall": return emitQuantifier(e, "forall");
|
|
341
471
|
case "exists": return emitQuantifier(e, "exists");
|
|
342
|
-
case "let": return `var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)}`;
|
|
472
|
+
case "let": return `(var ${escapeName(e.name)} := ${emitExpr(e.value)}; ${emitExpr(e.body)})`;
|
|
343
473
|
case "havoc": return "*";
|
|
344
474
|
}
|
|
345
475
|
}
|
|
@@ -389,7 +519,7 @@ function emitStmt(s, indent) {
|
|
|
389
519
|
case "ghostAssign":
|
|
390
520
|
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
391
521
|
case "assert":
|
|
392
|
-
return `${pad}assert ${emitExpr(s.expr)};`;
|
|
522
|
+
return `${pad}${s.assumed ? "assume {:axiom}" : "assert"} ${emitExpr(s.expr)};`;
|
|
393
523
|
case "bind":
|
|
394
524
|
// Monadic bind shouldn't appear in Dafny mode, emit as regular assign
|
|
395
525
|
return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)};`;
|
|
@@ -511,7 +641,7 @@ function emitDecl(d) {
|
|
|
511
641
|
}
|
|
512
642
|
case "method": {
|
|
513
643
|
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
514
|
-
const lines = [`method ${d.name}${tp}
|
|
644
|
+
const lines = [methodHeader(`method ${d.name}${tp}`, d.params, d.returnType)];
|
|
515
645
|
for (const r of d.requires)
|
|
516
646
|
lines.push(` requires ${emitExpr(r)}`);
|
|
517
647
|
for (const e of d.ensures)
|
|
@@ -529,7 +659,7 @@ function emitDecl(d) {
|
|
|
529
659
|
if (d.fields.length > 0 && d.methods.length > 0)
|
|
530
660
|
lines.push("");
|
|
531
661
|
for (const m of d.methods) {
|
|
532
|
-
lines.push(` method ${m.name}
|
|
662
|
+
lines.push(` ${methodHeader(`method ${m.name}`, m.params, m.returnType)}`);
|
|
533
663
|
for (const r of m.requires)
|
|
534
664
|
lines.push(` requires ${emitExpr(r)}`);
|
|
535
665
|
for (const e of m.ensures)
|
|
@@ -544,6 +674,18 @@ function emitDecl(d) {
|
|
|
544
674
|
case "const": {
|
|
545
675
|
return `const ${escapeName(d.name)}: ${tyToDafny(d.type)} := ${emitExpr(d.value)}`;
|
|
546
676
|
}
|
|
677
|
+
case "extern": {
|
|
678
|
+
// Body-less Dafny function — `:axiom` makes Dafny accept the missing body
|
|
679
|
+
// and treats it as an uninterpreted symbol. Any `requires`/`ensures` were
|
|
680
|
+
// lifted from the source declaration's annotations.
|
|
681
|
+
const tp = d.typeParams.length > 0 ? `<${d.typeParams.join(", ")}>` : "";
|
|
682
|
+
const lines = [`function {:axiom} ${escapeName(d.name)}${tp}(${paramList(d.params)}): ${tyToDafny(d.returnType)}`];
|
|
683
|
+
for (const r of d.requires)
|
|
684
|
+
lines.push(` requires ${emitExpr(r)}`);
|
|
685
|
+
for (const e of d.ensures)
|
|
686
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
687
|
+
return lines.join("\n");
|
|
688
|
+
}
|
|
547
689
|
case "namespace": {
|
|
548
690
|
// Dafny doesn't need namespaces — flatten declarations
|
|
549
691
|
return d.decls.map(emitDecl).join("\n\n");
|
|
@@ -555,6 +697,11 @@ function emitDecl(d) {
|
|
|
555
697
|
/** Preamble tracking — emitters add keys via `needPreamble(key)`, emitDafnyFile emits them. */
|
|
556
698
|
const _neededPreambles = new Set();
|
|
557
699
|
function needPreamble(key) { _neededPreambles.add(key); }
|
|
700
|
+
/** File-level opt-in for JS-clamp semantics on `arr.slice(lo, hi)`. Set by
|
|
701
|
+
* `emitDafnyFile` from the `//@ safe-slice` directive; consulted by the
|
|
702
|
+
* array-method emit. Off by default — case studies that wrote their `.slice`
|
|
703
|
+
* calls with provable bounds get direct `s[lo..hi]` emission. */
|
|
704
|
+
let _useSafeSlice = false;
|
|
558
705
|
const POW2 = `function Pow2(n: int): int
|
|
559
706
|
requires n >= 0
|
|
560
707
|
decreases n
|
|
@@ -587,6 +734,31 @@ const CEIL_REAL = `function CeilReal(x: real): int
|
|
|
587
734
|
if x == (x.Floor as real) then x.Floor
|
|
588
735
|
else x.Floor + 1
|
|
589
736
|
}`;
|
|
737
|
+
const SEQ_FIND_INDEX = `function SeqFindIndex<T>(s: seq<T>, p: T -> bool): int
|
|
738
|
+
ensures -1 <= SeqFindIndex(s, p) < |s|
|
|
739
|
+
ensures SeqFindIndex(s, p) >= 0 ==> p(s[SeqFindIndex(s, p)])
|
|
740
|
+
ensures SeqFindIndex(s, p) >= 0 ==>
|
|
741
|
+
(forall i: nat :: i < SeqFindIndex(s, p) ==> !p(s[i]))
|
|
742
|
+
ensures SeqFindIndex(s, p) == -1 ==> (forall i: nat :: i < |s| ==> !p(s[i]))
|
|
743
|
+
{
|
|
744
|
+
SeqFindIndexFrom(s, p, 0)
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
function SeqFindIndexFrom<T>(s: seq<T>, p: T -> bool, from: nat): int
|
|
748
|
+
requires from <= |s|
|
|
749
|
+
ensures -1 <= SeqFindIndexFrom(s, p, from) < |s|
|
|
750
|
+
ensures SeqFindIndexFrom(s, p, from) >= 0 ==>
|
|
751
|
+
from <= SeqFindIndexFrom(s, p, from) && p(s[SeqFindIndexFrom(s, p, from)])
|
|
752
|
+
ensures SeqFindIndexFrom(s, p, from) >= 0 ==>
|
|
753
|
+
(forall i: nat :: from <= i < SeqFindIndexFrom(s, p, from) ==> !p(s[i]))
|
|
754
|
+
ensures SeqFindIndexFrom(s, p, from) == -1 ==>
|
|
755
|
+
(forall i: nat :: from <= i < |s| ==> !p(s[i]))
|
|
756
|
+
decreases |s| - from
|
|
757
|
+
{
|
|
758
|
+
if from >= |s| then -1
|
|
759
|
+
else if p(s[from]) then from as int
|
|
760
|
+
else SeqFindIndexFrom(s, p, from + 1)
|
|
761
|
+
}`;
|
|
590
762
|
const SEQ_INDEX_OF = `function SeqIndexOf<T(==)>(s: seq<T>, x: T): int
|
|
591
763
|
ensures -1 <= SeqIndexOf(s, x) < |s|
|
|
592
764
|
ensures SeqIndexOf(s, x) >= 0 ==> s[SeqIndexOf(s, x)] == x
|
|
@@ -606,18 +778,75 @@ function SeqIndexOfFrom<T(==)>(s: seq<T>, x: T, from: nat): int
|
|
|
606
778
|
else if s[from] == x then from as int
|
|
607
779
|
else SeqIndexOfFrom(s, x, from + 1)
|
|
608
780
|
}`;
|
|
781
|
+
const SEQ_FIND_LAST = `function SeqFindLast<T>(s: seq<T>, p: T -> bool): Option<T>
|
|
782
|
+
ensures SeqFindLast(s, p).Some? ==> p(SeqFindLast(s, p).value)
|
|
783
|
+
ensures SeqFindLast(s, p).Some? ==> SeqFindLast(s, p).value in s
|
|
784
|
+
ensures SeqFindLast(s, p).Some? ==>
|
|
785
|
+
exists i: nat :: i < |s| && s[i] == SeqFindLast(s, p).value && p(s[i]) &&
|
|
786
|
+
(forall j: nat :: i < j < |s| ==> !p(s[j]))
|
|
787
|
+
ensures SeqFindLast(s, p).None? ==> forall i :: 0 <= i < |s| ==> !p(s[i])
|
|
788
|
+
decreases |s|
|
|
789
|
+
{
|
|
790
|
+
if |s| == 0 then None
|
|
791
|
+
else if p(s[|s|-1]) then Some(s[|s|-1])
|
|
792
|
+
else SeqFindLast(s[..|s|-1], p)
|
|
793
|
+
}`;
|
|
794
|
+
const SEQ_FLATTEN = `function SeqFlatten<T>(s: seq<seq<T>>): seq<T>
|
|
795
|
+
decreases |s|
|
|
796
|
+
{
|
|
797
|
+
if |s| == 0 then []
|
|
798
|
+
else s[0] + SeqFlatten(s[1..])
|
|
799
|
+
}`;
|
|
800
|
+
const SEQ_JOIN = `function SeqJoin(s: seq<string>, sep: string): string
|
|
801
|
+
decreases |s|
|
|
802
|
+
{
|
|
803
|
+
if |s| == 0 then ""
|
|
804
|
+
else if |s| == 1 then s[0]
|
|
805
|
+
else s[0] + sep + SeqJoin(s[1..], sep)
|
|
806
|
+
}`;
|
|
807
|
+
const SAFE_SLICE = `function SafeSlice<T>(s: seq<T>, lo: int, hi: int): seq<T>
|
|
808
|
+
ensures |SafeSlice(s, lo, hi)| <= |s|
|
|
809
|
+
{
|
|
810
|
+
var lo' := if lo < 0 then 0 else if lo > |s| as int then |s| else lo;
|
|
811
|
+
var hi' := if hi > |s| as int then |s| else if hi < lo' then lo' else hi;
|
|
812
|
+
s[lo'..hi']
|
|
813
|
+
}`;
|
|
609
814
|
const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
|
|
815
|
+
ensures StringIndexOf(s, sub) == -1
|
|
816
|
+
|| (0 <= StringIndexOf(s, sub) <= |s| - |sub| && s[StringIndexOf(s, sub)..StringIndexOf(s, sub) + |sub|] == sub)
|
|
610
817
|
{
|
|
611
818
|
StringIndexOfFrom(s, sub, 0)
|
|
612
819
|
}
|
|
613
820
|
|
|
614
|
-
function StringIndexOfFrom(s: string, sub: string, from:
|
|
821
|
+
function StringIndexOfFrom(s: string, sub: string, from: int): int
|
|
822
|
+
ensures StringIndexOfFrom(s, sub, from) == -1
|
|
823
|
+
|| (0 <= StringIndexOfFrom(s, sub, from) <= |s| - |sub|
|
|
824
|
+
&& s[StringIndexOfFrom(s, sub, from)..StringIndexOfFrom(s, sub, from) + |sub|] == sub
|
|
825
|
+
&& StringIndexOfFrom(s, sub, from) >= from)
|
|
826
|
+
{
|
|
827
|
+
StringIndexOfFromN(s, sub, if from < 0 then 0 else from)
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
function StringIndexOfFromN(s: string, sub: string, from: nat): int
|
|
615
831
|
decreases |s| - from
|
|
832
|
+
ensures StringIndexOfFromN(s, sub, from) == -1
|
|
833
|
+
|| (from <= StringIndexOfFromN(s, sub, from) <= |s| - |sub|
|
|
834
|
+
&& s[StringIndexOfFromN(s, sub, from)..StringIndexOfFromN(s, sub, from) + |sub|] == sub)
|
|
616
835
|
{
|
|
617
836
|
if from + |sub| > |s| then -1
|
|
618
837
|
else if s[from..from + |sub|] == sub then from as int
|
|
619
|
-
else
|
|
838
|
+
else StringIndexOfFromN(s, sub, from + 1)
|
|
620
839
|
}`;
|
|
840
|
+
// `s.split(d)` in TS returns a non-empty sequence of segments. Modeled here as
|
|
841
|
+
// an axiom — defining it recursively would force StringIndexOf to grow ensures
|
|
842
|
+
// clauses that callers don't need. The two ensures cover what verification
|
|
843
|
+
// usually wants: result has at least one element, and every element fits
|
|
844
|
+
// within the source length.
|
|
845
|
+
const STRING_SPLIT = `function {:axiom} StringSplit(s: string, d: string): seq<string>
|
|
846
|
+
requires |d| > 0
|
|
847
|
+
ensures |StringSplit(s, d)| >= 1
|
|
848
|
+
ensures |StringSplit(s, d)| <= |s| + 1
|
|
849
|
+
ensures forall k :: 0 <= k < |StringSplit(s, d)| ==> |StringSplit(s, d)[k]| <= |s|`;
|
|
621
850
|
const STRING_TRIM = `function StringTrimLeft(s: string): string
|
|
622
851
|
ensures |StringTrimLeft(s)| <= |s|
|
|
623
852
|
ensures StringTrimLeft(s) == "" || (|StringTrimLeft(s)| > 0 && StringTrimLeft(s)[0] != ' ')
|
|
@@ -663,6 +892,53 @@ const STRING_TO_UPPER = `function StringToUpper(s: string): string
|
|
|
663
892
|
}`;
|
|
664
893
|
const MATH_MIN = `function MathMin(a: int, b: int): int { if a <= b then a else b }`;
|
|
665
894
|
const MATH_MAX = `function MathMax(a: int, b: int): int { if a >= b then a else b }`;
|
|
895
|
+
const MAX_OF_SEQ = `function MaxOfSeq(s: seq<int>): int
|
|
896
|
+
requires |s| > 0
|
|
897
|
+
ensures forall i: nat :: i < |s| ==> s[i] <= MaxOfSeq(s)
|
|
898
|
+
ensures exists i: nat :: i < |s| && s[i] == MaxOfSeq(s)
|
|
899
|
+
decreases |s|
|
|
900
|
+
{
|
|
901
|
+
if |s| == 1 then s[0]
|
|
902
|
+
else MathMax(s[0], MaxOfSeq(s[1..]))
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Helper for proofs about MaxOfSeq applied to concatenations. Users invoke
|
|
906
|
+
// this in _ensures lemma bodies when Dafny doesn't automatically connect
|
|
907
|
+
// indices through (a + b)[i].
|
|
908
|
+
lemma MaxOfSeqConcat(a: seq<int>, b: seq<int>)
|
|
909
|
+
requires |a| + |b| > 0
|
|
910
|
+
ensures forall i: nat :: i < |a| ==> a[i] <= MaxOfSeq(a + b)
|
|
911
|
+
ensures forall i: nat :: i < |b| ==> b[i] <= MaxOfSeq(a + b)
|
|
912
|
+
{
|
|
913
|
+
forall i: nat | i < |a| ensures a[i] <= MaxOfSeq(a + b) {
|
|
914
|
+
assert (a + b)[i] == a[i];
|
|
915
|
+
}
|
|
916
|
+
forall i: nat | i < |b| ensures b[i] <= MaxOfSeq(a + b) {
|
|
917
|
+
assert (a + b)[|a| + i] == b[i];
|
|
918
|
+
}
|
|
919
|
+
}`;
|
|
920
|
+
const MIN_OF_SEQ = `function MinOfSeq(s: seq<int>): int
|
|
921
|
+
requires |s| > 0
|
|
922
|
+
ensures forall i: nat :: i < |s| ==> MinOfSeq(s) <= s[i]
|
|
923
|
+
ensures exists i: nat :: i < |s| && s[i] == MinOfSeq(s)
|
|
924
|
+
decreases |s|
|
|
925
|
+
{
|
|
926
|
+
if |s| == 1 then s[0]
|
|
927
|
+
else MathMin(s[0], MinOfSeq(s[1..]))
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
lemma MinOfSeqConcat(a: seq<int>, b: seq<int>)
|
|
931
|
+
requires |a| + |b| > 0
|
|
932
|
+
ensures forall i: nat :: i < |a| ==> MinOfSeq(a + b) <= a[i]
|
|
933
|
+
ensures forall i: nat :: i < |b| ==> MinOfSeq(a + b) <= b[i]
|
|
934
|
+
{
|
|
935
|
+
forall i: nat | i < |a| ensures MinOfSeq(a + b) <= a[i] {
|
|
936
|
+
assert (a + b)[i] == a[i];
|
|
937
|
+
}
|
|
938
|
+
forall i: nat | i < |b| ensures MinOfSeq(a + b) <= b[i] {
|
|
939
|
+
assert (a + b)[|a| + i] == b[i];
|
|
940
|
+
}
|
|
941
|
+
}`;
|
|
666
942
|
const NAT_TO_STRING = `function NatToString(n: nat): string
|
|
667
943
|
decreases n
|
|
668
944
|
{
|
|
@@ -700,7 +976,13 @@ const PREAMBLE_CODE = [
|
|
|
700
976
|
["CeilReal", CEIL_REAL],
|
|
701
977
|
["FloorReal", FLOOR_REAL],
|
|
702
978
|
["SeqIndexOf", SEQ_INDEX_OF],
|
|
979
|
+
["SeqFindIndex", SEQ_FIND_INDEX],
|
|
980
|
+
["SeqFindLast", SEQ_FIND_LAST],
|
|
981
|
+
["SeqFlatten", SEQ_FLATTEN],
|
|
982
|
+
["SeqJoin", SEQ_JOIN],
|
|
983
|
+
["SafeSlice", SAFE_SLICE],
|
|
703
984
|
["StringIndexOf", STRING_INDEX_OF],
|
|
985
|
+
["StringSplit", STRING_SPLIT],
|
|
704
986
|
["StringTrim", STRING_TRIM],
|
|
705
987
|
["StringToLower", STRING_TO_LOWER],
|
|
706
988
|
["StringToUpper", STRING_TO_UPPER],
|
|
@@ -708,6 +990,8 @@ const PREAMBLE_CODE = [
|
|
|
708
990
|
["MathAbs", MATH_ABS],
|
|
709
991
|
["MathMin", MATH_MIN],
|
|
710
992
|
["MathMax", MATH_MAX],
|
|
993
|
+
["MaxOfSeq", MAX_OF_SEQ],
|
|
994
|
+
["MinOfSeq", MIN_OF_SEQ],
|
|
711
995
|
];
|
|
712
996
|
// ── Constructor and record helpers ───────────────────────────
|
|
713
997
|
let _recordCtors = new Map();
|
|
@@ -777,7 +1061,8 @@ function translatePattern(pattern) {
|
|
|
777
1061
|
const fieldNames = fields.split(/\s+/).map(escapeName);
|
|
778
1062
|
return `${ctorName}(${fieldNames.join(", ")})`;
|
|
779
1063
|
}
|
|
780
|
-
export function emitDafnyFile(file, tsFileName) {
|
|
1064
|
+
export function emitDafnyFile(file, tsFileName, opts) {
|
|
1065
|
+
_useSafeSlice = !!opts?.safeSlice;
|
|
781
1066
|
buildRecordCtorMap(file.decls);
|
|
782
1067
|
_neededPreambles.clear();
|
|
783
1068
|
// Track successfully emitted pure defs — method wrappers are only
|