lemmascript 0.5.16 → 0.5.18
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 +46 -0
- package/package.json +1 -1
- package/tools/dist/dafny-emit.js +56 -11
- package/tools/dist/extract.js +2 -1
- package/tools/dist/lean-emit.js +67 -5
- package/tools/dist/narrow.js +71 -36
- package/tools/dist/resolve.js +20 -4
- package/tools/dist/transform.js +23 -1
- package/tools/dist/typedir.js +6 -0
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
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
if (
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
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' :=
|
|
1039
|
-
var 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],
|
package/tools/dist/extract.js
CHANGED
|
@@ -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
|
-
|
|
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 };
|
package/tools/dist/lean-emit.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
756
|
-
|
|
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}`;
|
package/tools/dist/narrow.js
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
* (early-return, let-cond) run in `walkStmts` so they can consume the rest
|
|
35
35
|
* of the block.
|
|
36
36
|
*/
|
|
37
|
+
import { isTerminatorKind } from "./typedir.js";
|
|
37
38
|
import { freshName } from "./names.js";
|
|
38
39
|
// ── Optional-check detection ────────────────────────────────
|
|
39
40
|
/** Counter for naming optChain binders. Reset per module. */
|
|
@@ -260,51 +261,86 @@ function ruleEarlyReturnConsume(s, rest) {
|
|
|
260
261
|
noneBody: noneBranch,
|
|
261
262
|
};
|
|
262
263
|
}
|
|
263
|
-
/**
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
264
|
+
/** Flatten a nested `||` chain into its leaf conditions. */
|
|
265
|
+
function flattenOr(e) {
|
|
266
|
+
if (e.kind === "binop" && e.op === "||")
|
|
267
|
+
return [...flattenOr(e.left), ...flattenOr(e.right)];
|
|
268
|
+
return [e];
|
|
269
|
+
}
|
|
270
|
+
function classifyDisjunct(leaf) {
|
|
271
|
+
// `x?.chain !== lit` — `undefined !== lit` is true when x is None.
|
|
272
|
+
if (leaf.kind === "binop" && leaf.op === "!==") {
|
|
273
|
+
const oc = leaf.left.kind === "optChain" ? leaf.left : leaf.right.kind === "optChain" ? leaf.right : null;
|
|
274
|
+
if (oc && oc.kind === "optChain" && oc.obj.ty.kind === "optional") {
|
|
275
|
+
const hint = binderHintFor(oc.obj);
|
|
276
|
+
if (hint === null)
|
|
277
|
+
return null;
|
|
278
|
+
const binder = freshName(hint);
|
|
279
|
+
const unwrapped = applyChain({ kind: "var", name: binder, ty: oc.obj.ty.inner }, oc.chain);
|
|
280
|
+
if (unwrapped.kind === "field" && unwrapped.obj.ty.kind === "user") {
|
|
281
|
+
const base = unwrapped.obj.ty.name.replace(/<.*/, "");
|
|
282
|
+
const decl = _typeDecls.find(d => d.name === base);
|
|
283
|
+
if (decl?.kind === "discriminated-union" && decl.discriminant === unwrapped.field)
|
|
284
|
+
unwrapped.isDiscriminant = true;
|
|
285
|
+
}
|
|
286
|
+
const lit = leaf.left === oc ? leaf.right : leaf.left;
|
|
287
|
+
return { scrutinee: oc.obj, innerTy: oc.obj.ty.inner, binder, residual: { kind: "binop", op: "!==", left: unwrapped, right: lit, ty: { kind: "bool" } } };
|
|
288
|
+
}
|
|
272
289
|
}
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
290
|
+
// `!x` / `x === undefined`.
|
|
291
|
+
const chk = parseOptionalCheck(leaf);
|
|
292
|
+
if (chk && chk.negated) {
|
|
293
|
+
const residual = canBeFalsy(chk)
|
|
294
|
+
? { kind: "unop", op: "!", expr: { kind: "var", name: chk.binderHint, ty: chk.innerTy }, ty: { kind: "bool" } }
|
|
295
|
+
: null;
|
|
296
|
+
return { scrutinee: chk.scrutinee, innerTy: chk.innerTy, binder: chk.binderHint, residual };
|
|
297
|
+
}
|
|
298
|
+
return null;
|
|
277
299
|
}
|
|
278
|
-
/** Rule: `if (
|
|
279
|
-
*
|
|
280
|
-
*
|
|
300
|
+
/** Rule: `if (D1 || … || Dn) terminate; rest`. Each `Di` that detects some optional
|
|
301
|
+
* `x` is None (`!x`, `x === undefined`, `x?.chain !== lit`) narrows that `x` to Some
|
|
302
|
+
* across `rest`; the rest — value guards reading a narrowed `x` directly, plus the
|
|
303
|
+
* detectors' Some-case residuals — become a trailing early-return. Sound: reaching
|
|
304
|
+
* `rest` means every disjunct was false, so every detected optional is present.
|
|
305
|
+
* Covers `if (!x || x.f !== v) continue` / `if (x?.t !== 'm' || x.g) break`.
|
|
281
306
|
* Closes the resolve.ts:602 TODO ("|| narrowing"). */
|
|
282
307
|
function ruleEarlyReturnOrChain(s, rest) {
|
|
283
308
|
if (s.kind !== "if")
|
|
284
309
|
return null;
|
|
285
310
|
if (rest.length === 0)
|
|
286
311
|
return null;
|
|
287
|
-
if (s.then.length === 0 || s.else.length !== 0)
|
|
312
|
+
if (s.then.length === 0 || s.else.length !== 0 || !isTerminating(s.then))
|
|
288
313
|
return null;
|
|
289
314
|
if (s.cond.kind !== "binop" || s.cond.op !== "||")
|
|
290
315
|
return null; // single check is the simpler rule
|
|
291
|
-
const
|
|
292
|
-
if (
|
|
293
|
-
return null;
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
const
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
316
|
+
const leaves = flattenOr(s.cond);
|
|
317
|
+
if (leaves.length < 2)
|
|
318
|
+
return null;
|
|
319
|
+
const detectors = [];
|
|
320
|
+
const residualLeaves = [];
|
|
321
|
+
const seen = new Set();
|
|
322
|
+
for (const leaf of leaves) {
|
|
323
|
+
const d = classifyDisjunct(leaf);
|
|
324
|
+
if (!d) {
|
|
325
|
+
residualLeaves.push(leaf);
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
const key = binderHintFor(d.scrutinee);
|
|
329
|
+
if (seen.has(key))
|
|
330
|
+
return null; // two detectors on one optional: rare; leave to other rules
|
|
331
|
+
seen.add(key);
|
|
332
|
+
detectors.push(d);
|
|
333
|
+
if (d.residual)
|
|
334
|
+
residualLeaves.push(d.residual);
|
|
335
|
+
}
|
|
336
|
+
if (detectors.length === 0)
|
|
337
|
+
return null;
|
|
338
|
+
let inner = residualLeaves.length === 0
|
|
339
|
+
? rest
|
|
340
|
+
: [{ kind: "if", cond: residualLeaves.reduce((a, b) => ({ kind: "binop", op: "||", left: a, right: b, ty: { kind: "bool" } })), then: s.then, else: [] }, ...rest];
|
|
341
|
+
for (let i = detectors.length - 1; i >= 0; i--) {
|
|
342
|
+
const d = detectors[i];
|
|
343
|
+
inner = [{ kind: "someMatch", scrutinee: d.scrutinee, binderTy: d.innerTy, binder: d.binder, someBody: inner, noneBody: s.then }];
|
|
308
344
|
}
|
|
309
345
|
return inner[0];
|
|
310
346
|
}
|
|
@@ -883,8 +919,7 @@ function parseNegativeDiscriminantCond(cond) {
|
|
|
883
919
|
function isTerminating(stmts) {
|
|
884
920
|
if (stmts.length === 0)
|
|
885
921
|
return false;
|
|
886
|
-
|
|
887
|
-
return last.kind === "return" || last.kind === "throw" || last.kind === "break" || last.kind === "continue";
|
|
922
|
+
return isTerminatorKind(stmts[stmts.length - 1].kind);
|
|
888
923
|
}
|
|
889
924
|
/** Rule (list-level): consecutive `if (x.kind === "v") ...` chain → tagMatch.
|
|
890
925
|
* Walks consecutive top-level ifs on the same discriminator var; the first
|
package/tools/dist/resolve.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Uses linked environments (Scheme-style) for lexical scoping.
|
|
5
5
|
* No mutation — each let extends the chain, lookup walks it.
|
|
6
6
|
*/
|
|
7
|
-
import { isBigInt, tyEqual } from "./typedir.js";
|
|
7
|
+
import { isBigInt, tyEqual, isTerminatorKind } from "./typedir.js";
|
|
8
8
|
import { parseTsType, tyToCanonical } from "./types.js";
|
|
9
9
|
import { parseExpr } from "./specparser.js";
|
|
10
10
|
import { freshName } from "./names.js";
|
|
@@ -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 };
|
|
@@ -1250,9 +1263,12 @@ function resolveBlock(stmts, ctx) {
|
|
|
1250
1263
|
env = nextEnv;
|
|
1251
1264
|
// Flow narrowing: if (x === undefined) { return } narrows x for rest of block.
|
|
1252
1265
|
// Also handles compound: if (x === undefined || y === undefined) { return }
|
|
1266
|
+
// Any terminator counts (return/throw/break/continue — same set as narrow's
|
|
1267
|
+
// isTerminating): each exits the current block, so the rest of the block
|
|
1268
|
+
// only runs when the guard was false.
|
|
1253
1269
|
// Field chains are excluded — resolve can't substitute in statement lists;
|
|
1254
1270
|
// transform's emitOptionalMatch handles field chains in statement contexts.
|
|
1255
|
-
if (s.kind === "if" && s.then.length > 0 && s.then[s.then.length - 1].kind
|
|
1271
|
+
if (s.kind === "if" && s.then.length > 0 && isTerminatorKind(s.then[s.then.length - 1].kind) && s.else.length === 0) {
|
|
1256
1272
|
const narrowings = collectEarlyReturnNarrowings(s.cond, withEnv(ctx, env));
|
|
1257
1273
|
for (const n of narrowings) {
|
|
1258
1274
|
env = extend(env, n.varName, n.innerTy);
|
package/tools/dist/transform.js
CHANGED
|
@@ -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
|
-
|
|
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 => ({
|
package/tools/dist/typedir.js
CHANGED
|
@@ -44,3 +44,9 @@ export function tyEqual(a, b) {
|
|
|
44
44
|
case "unknown": return true; // no payload
|
|
45
45
|
}
|
|
46
46
|
}
|
|
47
|
+
/** Statement kinds that unconditionally leave the enclosing block. Shared by
|
|
48
|
+
* resolve (block-tail narrowing) and narrow (isTerminating); works on raw and
|
|
49
|
+
* typed IR alike since both use these kind strings. */
|
|
50
|
+
export function isTerminatorKind(kind) {
|
|
51
|
+
return kind === "return" || kind === "throw" || kind === "break" || kind === "continue";
|
|
52
|
+
}
|