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.
@@ -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": return qualifyCtor(e.name, e.type);
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 === "slice")
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 `(${emitExpr(e.left)} ${op} ${emitExpr(e.right)})`;
354
+ return `(${wrapQuantifier(e.left)} ${op} ${emitExpr(e.right)})`;
243
355
  }
244
356
  case "implies": {
245
- const parts = [...e.premises.map(emitExpr), emitExpr(e.conclusion)];
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
- return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]`;
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
- if (structFields && e.fields.length < structFields.length) {
311
- // Pad missing fields: match by name, fill None for optional
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}(${paramList(d.params)}) returns (res: ${tyToDafny(d.returnType)})`];
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}(${paramList(m.params)}) returns (res: ${tyToDafny(m.returnType)})`);
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: nat): int
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 StringIndexOfFrom(s, sub, from + 1)
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