lemmascript 0.5.16 → 0.5.17

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 CHANGED
@@ -86,6 +86,52 @@ lsc gen --backend=lean src/myModule.ts
86
86
  lake build
87
87
  ```
88
88
 
89
+ ## Continuous Integration
90
+
91
+ LemmaScript ships a **reusable GitHub Actions workflow** that regenerates your artifacts, verifies them, and fails the build if any committed generated file is out of date. Call it from your own repo's workflow:
92
+
93
+ ```yaml
94
+ # .github/workflows/lemmascript.yml
95
+ name: LemmaScript
96
+
97
+ on:
98
+ push:
99
+ branches: [main]
100
+ pull_request:
101
+ branches: [main]
102
+
103
+ jobs:
104
+ verify:
105
+ uses: midspiral/LemmaScript/.github/workflows/verify.yml@main
106
+ with:
107
+ backend: dafny # dafny | lean | dafny-slow
108
+ ```
109
+
110
+ The workflow installs the toolchain and (per `backend`) the Dafny or Lean stack, then runs `tools/check.sh`, which batches over a **`LemmaScript-files.txt`** at your repo root. This file is the list of sources CI verifies — you create and maintain it. One entry per line, `filepath [timeout] [extra dafny flags…]`:
111
+
112
+ ```
113
+ src/domain.ts
114
+ src/patch.ts 120
115
+ src/heavy.ts 300 --isolate-assertions
116
+ ```
117
+
118
+ The optional second column is a per-file timeout in seconds; anything after it is passed verbatim to Dafny. The same list drives `lsc check` locally when you run it with no file argument, so CI and your local runs verify exactly the same set. To verify additional Dafny files outside that list, add an executable `check-extra.sh` at the root and it runs automatically (Dafny backends only).
119
+
120
+ Inputs (all optional):
121
+
122
+ | Input | Default | Purpose |
123
+ |-------|---------|---------|
124
+ | `backend` | `dafny` | `dafny`, `lean`, or `dafny-slow` (isolate-assertions / long-running proofs) |
125
+ | `node-version` | `24` | Node.js version |
126
+ | `ls-ref` | `main` | LemmaScript ref to verify against |
127
+ | `typecheck` | `true` | Run `npm ci && npm run typecheck` in the calling repo first |
128
+
129
+ To verify against **both** backends, add a second job with `backend: lean`.
130
+
131
+ Examples:
132
+ - **[talktimer-lemmascript](https://github.com/midspiral/talktimer-lemmascript/blob/main/.github/workflows/lemmascript.yml)** — Dafny-only.
133
+ - **[pi-lemmascript](https://github.com/midspiral/pi-lemmascript/blob/lemmascript/.github/workflows/lemmascript.yml)** — Dafny + Lean (two jobs).
134
+
89
135
  ## Annotations
90
136
 
91
137
  ```typescript
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lemmascript",
3
- "version": "0.5.16",
3
+ "version": "0.5.17",
4
4
  "description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
5
5
  "type": "module",
6
6
  "engines": {
@@ -258,6 +258,10 @@ function emitExpr(e) {
258
258
  if (e.method === "concat")
259
259
  return `(${obj} + [${args.join(", ")}])`;
260
260
  if (e.method === "sort") {
261
+ if (args.length === 0) {
262
+ needPreamble("SeqSort");
263
+ return `SeqSort(${obj})`;
264
+ }
261
265
  needPreamble("SeqSortBy");
262
266
  return `SeqSortBy(${obj}, ${args[0]})`;
263
267
  }
@@ -298,6 +302,10 @@ function emitExpr(e) {
298
302
  needPreamble("SeqFindIndex");
299
303
  return `SeqFindIndex(${obj}, ${args[0]})`;
300
304
  }
305
+ if (e.method === "findLastIndex") {
306
+ needPreamble("SeqFindLastIndex");
307
+ return `SeqFindLastIndex(${obj}, ${args[0]})`;
308
+ }
301
309
  if (e.method === "flat" && args.length === 0) {
302
310
  needPreamble("SeqFlatten");
303
311
  return `SeqFlatten(${obj})`;
@@ -306,16 +314,26 @@ function emitExpr(e) {
306
314
  needPreamble("SeqJoin");
307
315
  return `SeqJoin(${obj}, ${args[0]})`;
308
316
  }
309
- if (e.method === "some" && e.args[0].kind === "lambda" &&
310
- e.args[0].body.length === 1 && e.args[0].body[0].kind === "return") {
317
+ // `.some(pred)`: inline a single-return lambda's body, else apply the
318
+ // predicate (e.g. a function reference).
319
+ if (e.method === "some") {
311
320
  const lam = e.args[0];
312
- const ret = lam.body[0];
313
- if (ret.kind !== "return")
314
- throw new Error("unreachable");
315
- const { binder: p, body: v } = comprehensionBinder(lam, ret.value, e.obj);
316
- const body = emitExpr(v);
321
+ let p, body;
322
+ if (lam.kind === "lambda" && lam.body.length === 1 && lam.body[0].kind === "return") {
323
+ const cb = comprehensionBinder(lam, lam.body[0].value, e.obj);
324
+ p = cb.binder;
325
+ body = emitExpr(cb.body);
326
+ }
327
+ else {
328
+ p = escapeName(freshBinder("x", e.obj, e.args[0]));
329
+ body = `${args[0]}(${p})`;
330
+ }
317
331
  return `(exists ${p} :: ${p} in ${obj} && ${body})`;
318
332
  }
333
+ // `.reduce(f, init)` → Std's FoldLeft(f, init, xs) (same arg order).
334
+ if (e.method === "reduce" && args.length === 2) {
335
+ return `Std.Collections.Seq.FoldLeft(${args[0]}, ${args[1]}, ${obj})`;
336
+ }
319
337
  }
320
338
  // String methods
321
339
  if (ty === "string") {
@@ -1006,6 +1024,18 @@ function SeqIndexOfFrom<T(==)>(s: seq<T>, x: T, from: nat): int
1006
1024
  else if s[from] == x then from as int
1007
1025
  else SeqIndexOfFrom(s, x, from + 1)
1008
1026
  }`;
1027
+ const SEQ_FIND_LAST_INDEX = `function SeqFindLastIndex<T>(s: seq<T>, p: T -> bool): int
1028
+ ensures -1 <= SeqFindLastIndex(s, p) < |s|
1029
+ ensures SeqFindLastIndex(s, p) >= 0 ==> p(s[SeqFindLastIndex(s, p)])
1030
+ ensures SeqFindLastIndex(s, p) >= 0 ==>
1031
+ (forall j: int :: SeqFindLastIndex(s, p) < j < |s| ==> !p(s[j]))
1032
+ ensures SeqFindLastIndex(s, p) == -1 ==> (forall i: nat :: i < |s| ==> !p(s[i]))
1033
+ decreases |s|
1034
+ {
1035
+ if |s| == 0 then -1
1036
+ else if p(s[|s|-1]) then |s| - 1
1037
+ else SeqFindLastIndex(s[..|s|-1], p)
1038
+ }`;
1009
1039
  const SEQ_FIND_LAST = `function SeqFindLast<T>(s: seq<T>, p: T -> bool): Option<T>
1010
1040
  ensures SeqFindLast(s, p).Some? ==> p(SeqFindLast(s, p).value)
1011
1041
  ensures SeqFindLast(s, p).Some? ==> SeqFindLast(s, p).value in s
@@ -1032,12 +1062,21 @@ const SEQ_JOIN = `function SeqJoin(s: seq<string>, sep: string): string
1032
1062
  else if |s| == 1 then s[0]
1033
1063
  else s[0] + sep + SeqJoin(s[1..], sep)
1034
1064
  }`;
1035
- const SAFE_SLICE = `function SafeSlice<T>(s: seq<T>, lo: int, hi: int): seq<T>
1065
+ const SAFE_SLICE = `function NormalizeSliceIndex(n: nat, i: int): int
1066
+ ensures 0 <= NormalizeSliceIndex(n, i) <= n as int
1067
+ {
1068
+ if i < 0 then
1069
+ if n as int + i < 0 then 0 else n as int + i
1070
+ else if i > n as int then n as int
1071
+ else i
1072
+ }
1073
+
1074
+ function SafeSlice<T>(s: seq<T>, lo: int, hi: int): seq<T>
1036
1075
  ensures |SafeSlice(s, lo, hi)| <= |s|
1037
1076
  {
1038
- var lo' := if lo < 0 then 0 else if lo > |s| as int then |s| else lo;
1039
- var hi' := if hi > |s| as int then |s| else if hi < lo' then lo' else hi;
1040
- s[lo'..hi']
1077
+ var lo' := NormalizeSliceIndex(|s|, lo);
1078
+ var hi' := NormalizeSliceIndex(|s|, hi);
1079
+ if hi' < lo' then [] else s[lo'..hi']
1041
1080
  }`;
1042
1081
  const STRING_INDEX_OF = `function StringIndexOf(s: string, sub: string): int
1043
1082
  ensures StringIndexOf(s, sub) == -1
@@ -1080,6 +1119,10 @@ const STRING_SPLIT = `function {:axiom} StringSplit(s: string, d: string): seq<s
1080
1119
  // is the soundness condition — cmp must be a total preorder, otherwise no sorted
1081
1120
  // permutation exists and the axiom would be vacuous. Callers discharge it (e.g.
1082
1121
  // `(a,b) => a.k - b.k` is total + transitive by linear arithmetic).
1122
+ // Bare `.sort()`: permutation only, no sortedness (JS default order is type-dependent).
1123
+ const SEQ_SORT = `function {:axiom} SeqSort<T(==,!new)>(s: seq<T>): seq<T>
1124
+ ensures multiset(SeqSort(s)) == multiset(s)
1125
+ ensures |SeqSort(s)| == |s|`;
1083
1126
  const SEQ_SORT_BY = `function {:axiom} SeqSortBy<T(==,!new)>(s: seq<T>, cmp: (T, T) -> int): seq<T>
1084
1127
  requires forall a: T, b: T :: cmp(a, b) <= 0 || cmp(b, a) <= 0
1085
1128
  requires forall a: T, b: T, c: T :: cmp(a, b) <= 0 && cmp(b, c) <= 0 ==> cmp(a, c) <= 0
@@ -1249,6 +1292,7 @@ const PREAMBLE_CODE = [
1249
1292
  ["FloorReal", FLOOR_REAL],
1250
1293
  ["SeqIndexOf", SEQ_INDEX_OF],
1251
1294
  ["SeqFindIndex", SEQ_FIND_INDEX],
1295
+ ["SeqFindLastIndex", SEQ_FIND_LAST_INDEX],
1252
1296
  ["SeqFilterSome", SEQ_FILTER_SOME],
1253
1297
  ["SeqFindLast", SEQ_FIND_LAST],
1254
1298
  ["SeqFlatten", SEQ_FLATTEN],
@@ -1256,6 +1300,7 @@ const PREAMBLE_CODE = [
1256
1300
  ["SafeSlice", SAFE_SLICE],
1257
1301
  ["StringIndexOf", STRING_INDEX_OF],
1258
1302
  ["StringSplit", STRING_SPLIT],
1303
+ ["SeqSort", SEQ_SORT],
1259
1304
  ["SeqSortBy", SEQ_SORT_BY],
1260
1305
  ["StringTrim", STRING_TRIM],
1261
1306
  ["StringToLower", STRING_TO_LOWER],
@@ -1887,7 +1887,8 @@ export function extractModule(sourceFile) {
1887
1887
  const name = recordMatch[1];
1888
1888
  // `<T extends B>` → bare `T`: a Dafny type param, like the def path.
1889
1889
  const typeParams = recordMatch[2]?.split(",").map(s => s.trim().split(/\s+extends\s+/)[0].trim()).filter(Boolean);
1890
- const fields = recordMatch[3].split(",").map(f => f.trim()).filter(Boolean).map(f => {
1890
+ // Split on `,` OR `;` TS object types allow both.
1891
+ const fields = recordMatch[3].split(/[,;]/).map(f => f.trim()).filter(Boolean).map(f => {
1891
1892
  const [fname, ftype] = f.split(":").map(s => s.trim());
1892
1893
  const synth = _synthFromTsTypeString(ftype);
1893
1894
  return { name: fname, tsType: synth ?? ftype };
@@ -236,6 +236,8 @@ function emitMethodCall(tyKind, method, monadic, obj, args) {
236
236
  return `${obj}.${monadic ? "allM" : "all"} ${args[0]}`;
237
237
  if (method === "some")
238
238
  return `${obj}.${monadic ? "anyM" : "any"} ${args[0]}`;
239
+ if (method === "reduce" && args.length === 2)
240
+ return `(${obj}.foldl ${args[0]} ${args[1]})`;
239
241
  if (method === "includes")
240
242
  return args.length > 1 ? `(${obj}.extract ${args[1]} ${obj}.size).contains ${args[0]}` : `${obj}.contains ${args[0]}`;
241
243
  if (method === "find")
@@ -476,8 +478,13 @@ function emitExpr(e, parentPrec) {
476
478
  // A real value reached the Lean backend via coercion (e.g. number `/`).
477
479
  // Same unsupported-real story as the `real` type case in tyToLean.
478
480
  throw new Error("real arithmetic is not supported by the Lean backend (needs noncomputable ℝ / Mathlib).");
479
- case "index":
480
- return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
481
+ case "index": {
482
+ const arr = emitExpr(e.arr);
483
+ // Parenthesize a low-precedence array expr (e.g. a function application)
484
+ // so the index binds to the whole thing, not its last token.
485
+ const wrap = e.arr.kind === "app" || e.arr.kind === "binop" || e.arr.kind === "methodCall" || e.arr.kind === "if" || e.arr.kind === "let" || e.arr.kind === "unop";
486
+ return `${wrap ? `(${arr})` : arr}[${emitExpr(e.idx)}]!`;
487
+ }
481
488
  case "record": {
482
489
  const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
483
490
  if (e.spread)
@@ -652,6 +659,59 @@ function emitStmt(s, indent) {
652
659
  }
653
660
  }
654
661
  // ── Declaration emission ─────────────────────────────────────
662
+ /** Collect every function/variable name an IR tree references (stripping any
663
+ * `Pure.` qualifier), via a generic walk over `app`/`var` nodes. */
664
+ function collectRefNames(node, into) {
665
+ if (node === null || typeof node !== "object")
666
+ return;
667
+ if (Array.isArray(node)) {
668
+ for (const x of node)
669
+ collectRefNames(x, into);
670
+ return;
671
+ }
672
+ const n = node;
673
+ if (n.kind === "app" && typeof n.fn === "string")
674
+ into.add(n.fn.replace(/^Pure\./, ""));
675
+ if (n.kind === "var" && typeof n.name === "string")
676
+ into.add(n.name.replace(/^Pure\./, ""));
677
+ for (const k of Object.keys(node))
678
+ collectRefNames(node[k], into);
679
+ }
680
+ /** Lean requires definition-before-use: order sibling decls so one that
681
+ * references another is emitted after it. Cycles are left in place (they would
682
+ * need a `mutual` block). Bails to the original order if any decl is unnamed. */
683
+ function orderDeclsByDeps(decls) {
684
+ const named = decls.filter((d) => typeof d.name === "string");
685
+ if (named.length !== decls.length)
686
+ return decls;
687
+ const names = new Set(named.map(d => d.name));
688
+ const byName = new Map(named.map(d => [d.name, d]));
689
+ const deps = new Map();
690
+ for (const d of named) {
691
+ const refs = new Set();
692
+ collectRefNames(d, refs);
693
+ refs.delete(d.name);
694
+ deps.set(d.name, [...refs].filter(r => names.has(r)));
695
+ }
696
+ const sorted = [];
697
+ const done = new Set();
698
+ const onStack = new Set();
699
+ const visit = (name) => {
700
+ if (done.has(name) || onStack.has(name))
701
+ return;
702
+ onStack.add(name);
703
+ for (const dep of deps.get(name) ?? [])
704
+ visit(dep);
705
+ onStack.delete(name);
706
+ done.add(name);
707
+ const def = byName.get(name);
708
+ if (def)
709
+ sorted.push(def);
710
+ };
711
+ for (const d of named)
712
+ visit(d.name);
713
+ return sorted;
714
+ }
655
715
  function emitDecl(d) {
656
716
  switch (d.kind) {
657
717
  case "inductive": {
@@ -730,7 +790,7 @@ function emitDecl(d) {
730
790
  }
731
791
  case "namespace": {
732
792
  const lines = [`namespace ${d.name}`];
733
- for (const inner of d.decls)
793
+ for (const inner of orderDeclsByDeps(d.decls))
734
794
  lines.push("", emitDecl(inner));
735
795
  lines.push("", `end ${d.name}`);
736
796
  return lines.join("\n");
@@ -752,8 +812,10 @@ function emitDecl(d) {
752
812
  return sig;
753
813
  // Spec axiom: ∀ params, req1 → … → (ens1 ∧ … ∧ ensN). Tagged `@[grind]` so
754
814
  // the proof automation can use it, matching the ghost-function convention.
755
- const hyps = d.requires.map(emitExpr);
756
- const concl = d.ensures.map(emitExpr).join("");
815
+ // Parenthesize each clause: an unwrapped `∀ k, P` would otherwise swallow
816
+ // the following `…` conjuncts into its body.
817
+ const hyps = d.requires.map(e => `(${emitExpr(e)})`);
818
+ const concl = d.ensures.map(e => `(${emitExpr(e)})`).join(" ∧ ");
757
819
  const axBody = [...hyps, concl].join(" → ");
758
820
  const axiom = `@[grind] axiom ${escapeName(d.name)}_spec${params ? ` ${params}` : ""} : ${axBody}`;
759
821
  return `${sig}\n${axiom}`;
@@ -260,51 +260,86 @@ function ruleEarlyReturnConsume(s, rest) {
260
260
  noneBody: noneBranch,
261
261
  };
262
262
  }
263
- /** Collect a `||` chain of negative optional checks (`x === undefined`).
264
- * Returns the list of checks if every leaf is a negative optional check; null otherwise. */
265
- function collectOrChainOfNegativeChecks(cond) {
266
- if (cond.kind === "binop" && cond.op === "||") {
267
- const left = collectOrChainOfNegativeChecks(cond.left);
268
- const right = collectOrChainOfNegativeChecks(cond.right);
269
- if (!left || !right)
270
- return null;
271
- return [...left, ...right];
263
+ /** Flatten a nested `||` chain into its leaf conditions. */
264
+ function flattenOr(e) {
265
+ if (e.kind === "binop" && e.op === "||")
266
+ return [...flattenOr(e.left), ...flattenOr(e.right)];
267
+ return [e];
268
+ }
269
+ function classifyDisjunct(leaf) {
270
+ // `x?.chain !== lit` — `undefined !== lit` is true when x is None.
271
+ if (leaf.kind === "binop" && leaf.op === "!==") {
272
+ const oc = leaf.left.kind === "optChain" ? leaf.left : leaf.right.kind === "optChain" ? leaf.right : null;
273
+ if (oc && oc.kind === "optChain" && oc.obj.ty.kind === "optional") {
274
+ const hint = binderHintFor(oc.obj);
275
+ if (hint === null)
276
+ return null;
277
+ const binder = freshName(hint);
278
+ const unwrapped = applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain);
279
+ if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
280
+ const base = unwrapped.obj.ty.name.replace(/<.*/, "");
281
+ const decl = _typeDecls.find(d => d.name === base);
282
+ if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field)
283
+ unwrapped.isDiscriminant = true;
284
+ }
285
+ const lit = leaf.left === oc ? leaf.right : leaf.left;
286
+ return { scrutinee: oc.obj, innerTy: oc.obj.ty.inner, binder, residual: { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } } };
287
+ }
272
288
  }
273
- const check = parseSimpleOptionalCheck(cond);
274
- if (!check || !check.negated)
275
- return null;
276
- return [check];
289
+ // `!x` / `x === undefined`.
290
+ const chk = parseOptionalCheck(leaf);
291
+ if (chk && chk.negated) {
292
+ const residual = canBeFalsy(chk)
293
+ ? { kind: "unop", op: "!", expr: { kind: "var", name: chk.binderHint, ty: chk.innerTy }, ty: { kind: "bool" } }
294
+ : null;
295
+ return { scrutinee: chk.scrutinee, innerTy: chk.innerTy, binder: chk.binderHint, residual };
296
+ }
297
+ return null;
277
298
  }
278
- /** Rule: `if (x === undefined || y === undefined || ...) terminate; rest`.
279
- * nested someMatches narrowing each var in turn, each None branch = terminate,
280
- * deepest someBody = rest.
299
+ /** Rule: `if (D1 || || Dn) terminate; rest`. Each `Di` that detects some optional
300
+ * `x` is None (`!x`, `x === undefined`, `x?.chain !== lit`) narrows that `x` to Some
301
+ * across `rest`; the rest — value guards reading a narrowed `x` directly, plus the
302
+ * detectors' Some-case residuals — become a trailing early-return. Sound: reaching
303
+ * `rest` means every disjunct was false, so every detected optional is present.
304
+ * Covers `if (!x || x.f !== v) continue` / `if (x?.t !== 'm' || x.g) break`.
281
305
  * Closes the resolve.ts:602 TODO ("|| narrowing"). */
282
306
  function ruleEarlyReturnOrChain(s, rest) {
283
307
  if (s.kind !== "if")
284
308
  return null;
285
309
  if (rest.length === 0)
286
310
  return null;
287
- if (s.then.length === 0 || s.else.length !== 0)
311
+ if (s.then.length === 0 || s.else.length !== 0 || !isTerminating(s.then))
288
312
  return null;
289
313
  if (s.cond.kind !== "binop" || s.cond.op !== "||")
290
314
  return null; // single check is the simpler rule
291
- const checks = collectOrChainOfNegativeChecks(s.cond);
292
- if (!checks || checks.length < 2)
293
- return null;
294
- // Build nested someMatch from innermost outward
295
- let inner = rest;
296
- for (let i = checks.length - 1; i >= 0; i--) {
297
- const check = checks[i];
298
- const someBody = canBeFalsy(check)
299
- ? [{ kind: "if", cond: bound(check), then: inner, else: s.then }]
300
- : inner;
301
- inner = [{
302
- kind: "someMatch",
303
- scrutinee: check.scrutinee, binderTy: check.innerTy,
304
- binder: check.binderHint,
305
- someBody,
306
- noneBody: s.then,
307
- }];
315
+ const leaves = flattenOr(s.cond);
316
+ if (leaves.length < 2)
317
+ return null;
318
+ const detectors = [];
319
+ const residualLeaves = [];
320
+ const seen = new Set();
321
+ for (const leaf of leaves) {
322
+ const d = classifyDisjunct(leaf);
323
+ if (!d) {
324
+ residualLeaves.push(leaf);
325
+ continue;
326
+ }
327
+ const key = binderHintFor(d.scrutinee);
328
+ if (seen.has(key))
329
+ return null; // two detectors on one optional: rare; leave to other rules
330
+ seen.add(key);
331
+ detectors.push(d);
332
+ if (d.residual)
333
+ residualLeaves.push(d.residual);
334
+ }
335
+ if (detectors.length === 0)
336
+ return null;
337
+ let inner = residualLeaves.length === 0
338
+ ? rest
339
+ : [{ kind: "if", cond: residualLeaves.reduce((a, b) => ({ kind: "binop", op: "||", left: a, right: b, ty: { kind: "bool" } })), then: s.then, else: [] }, ...rest];
340
+ for (let i = detectors.length - 1; i >= 0; i--) {
341
+ const d = detectors[i];
342
+ inner = [{ kind: "someMatch", scrutinee: d.scrutinee, binderTy: d.innerTy, binder: d.binder, someBody: inner, noneBody: s.then }];
308
343
  }
309
344
  return inner[0];
310
345
  }
@@ -480,8 +480,19 @@ function inferLambdaParamTypes(fn, rawArgs, ctx) {
480
480
  return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
481
481
  }
482
482
  }
483
+ // reduce's callback is (acc, elem): acc from the init arg's type, elem from the array.
484
+ if (fn.kind === "field" && fn.obj.ty.kind === "array" && fn.field === "reduce" && ctx &&
485
+ rawArgs.length >= 2 && rawArgs[0].kind === "lambda" && rawArgs[0].params.length >= 2) {
486
+ const accTs = tyToTsStr(resolveExpr(rawArgs[1], ctx).ty);
487
+ const elemTs = tyToTsStr(fn.obj.ty.elem);
488
+ if (accTs && elemTs) {
489
+ const lam = rawArgs[0];
490
+ const updatedParams = lam.params.map((p, i) => p.tsType || i > 1 ? p : { ...p, tsType: i === 0 ? accTs : elemTs });
491
+ return [{ ...lam, params: updatedParams }, ...rawArgs.slice(1)];
492
+ }
493
+ }
483
494
  if (fn.kind === "field" && fn.obj.ty.kind === "array" &&
484
- ["map", "filter", "every", "some", "find", "findLast", "findIndex"].includes(fn.field) &&
495
+ ["map", "filter", "every", "some", "find", "findLast", "findIndex", "findLastIndex"].includes(fn.field) &&
485
496
  rawArgs.length >= 1 && rawArgs[0].kind === "lambda" &&
486
497
  rawArgs[0].params.length >= 1 && !rawArgs[0].params[0].tsType) {
487
498
  const elemTy = fn.obj.ty.elem;
@@ -604,9 +615,11 @@ function inferMethodReturnTy(fn, args, ctx) {
604
615
  return objTy;
605
616
  if (fn.field === "every" || fn.field === "some")
606
617
  return { kind: "bool" };
618
+ if (fn.field === "reduce" && args.length === 2)
619
+ return args[1].ty;
607
620
  if (fn.field === "find" || fn.field === "findLast")
608
621
  return { kind: "optional", inner: objTy.elem };
609
- if (fn.field === "findIndex")
622
+ if (fn.field === "findIndex" || fn.field === "findLastIndex")
610
623
  return { kind: "int" };
611
624
  if (fn.field === "flat" && objTy.elem.kind === "array")
612
625
  return { kind: "array", elem: objTy.elem.elem };
@@ -189,6 +189,12 @@ export const DAFNY_OPTIONS = {
189
189
  let _opts = DAFNY_OPTIONS;
190
190
  /** Type declarations — set once per module transform for discriminated union handling. */
191
191
  let _typeDecls = [];
192
+ /** Names of functions that get a `Pure.` mirror (Lean). A bare reference to one
193
+ * in a higher-order position resolves to the monadic method, so it must be
194
+ * redirected to the pure mirror. Set once per module transform. */
195
+ let _pureDefNames = new Set();
196
+ /** Array methods that take a function argument. */
197
+ const HOF_METHODS = new Set(["map", "filter", "every", "some", "find", "findLast", "findIndex", "findLastIndex", "reduce"]);
192
198
  /** Prefix match-bound field names to avoid capturing user variables.
193
199
  * When prefix is given (the scrutinee name), include it to avoid
194
200
  * collisions in nested matches on different variables. `freshName` closes
@@ -331,7 +337,8 @@ function coerceCondToBool(cond, ty) {
331
337
  return { kind: "binop", op: "≠", left: cond, right: { kind: "num", value: 0 } };
332
338
  if (ty.kind === "string")
333
339
  return { kind: "binop", op: ">", left: { kind: "field", obj: cond, field: "length" }, right: { kind: "num", value: 0 } };
334
- if (ty.kind === "array")
340
+ // Arrays, objects, maps, sets, tuples are always truthy in JS (even `[]`/`{}`).
341
+ if (["array", "user", "map", "set", "tuple"].includes(ty.kind))
335
342
  return { kind: "bool", value: true };
336
343
  return cond;
337
344
  }
@@ -608,6 +615,13 @@ function lowerExpr(e, binds) {
608
615
  else: { kind: "var", name: "undefined" },
609
616
  };
610
617
  }
618
+ // || on a number → `if x != 0 then x else default` (0 the only falsy int).
619
+ if (e.op === "||" && (e.left.ty.kind === "int" || e.left.ty.kind === "nat")) {
620
+ const left = lowerExpr(e.left, binds);
621
+ const right = lowerExpr(e.right, binds);
622
+ const truthy = valueTruthyCond(left, e.left.ty);
623
+ return { kind: "if", cond: truthy, then: left, else: right };
624
+ }
611
625
  // String concatenation: `+` with a string operand. Stringify int/nat
612
626
  // operands (Dafny NatToString, Lean toString) and join with arrayConcat
613
627
  // (rendered `+` in Dafny, `++` in Lean).
@@ -773,8 +787,15 @@ function lowerExpr(e, binds) {
773
787
  if (e.fn.kind === "field") {
774
788
  const recv = lowerExpr(e.fn.obj, binds);
775
789
  let method = e.fn.field;
790
+ const isHOF = e.fn.obj.ty.kind === "array" && HOF_METHODS.has(method);
776
791
  const args = e.args.map((a, i) => {
777
792
  const lowered = lowerExpr(a, binds);
793
+ // Lean: a pure fn passed to a HOF by name resolves to the monadic
794
+ // method; redirect to its pure `Pure.` mirror.
795
+ if (_opts.backend === "lean" && isHOF &&
796
+ lowered.kind === "var" && _pureDefNames.has(lowered.name)) {
797
+ return { kind: "var", name: `Pure.${lowered.name}` };
798
+ }
778
799
  // Array index args must be nat in Lean: `with`'s index (0), includes/indexOf `from` (1).
779
800
  const isArrIdxArg = e.fn.kind === "field" && e.fn.obj.ty.kind === "array" &&
780
801
  ((e.fn.field === "with" && i === 0) || ((e.fn.field === "includes" || e.fn.field === "indexOf") && i === 1));
@@ -2182,6 +2203,7 @@ export function transformModule(mod, specImport, moduleBaseOverride) {
2182
2203
  _forofCounters.clear();
2183
2204
  _liftCounter = 0;
2184
2205
  _typeDecls = mod.typeDecls;
2206
+ _pureDefNames = new Set(mod.functions.filter(f => f.isPure).map(f => f.name));
2185
2207
  const typeDecls = mod.typeDecls.map(transformTypeDecl);
2186
2208
  // Module-level constants
2187
2209
  const constDecls = (mod.constants ?? []).map(c => ({