lemmascript 0.5.4 → 0.5.5
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 +2 -1
- package/package.json +1 -1
- package/tools/dist/dafny-emit.js +11 -2
- package/tools/dist/emit.js +253 -0
- package/tools/dist/narrow.js +16 -8
- package/tools/dist/peephole.js +2 -1
- package/tools/dist/transform.js +40 -10
package/README.md
CHANGED
|
@@ -24,7 +24,7 @@ See the external case studies:
|
|
|
24
24
|
- **[talktimer-lemmascript](https://github.com/midspiral/talktimer-lemmascript/)** — verified talk timer React app, ported from a Dafny-only [`talktimer-lemmafit`](https://github.com/midspiral/talktimer-lemmafit/) twin. 17-variant `Action` state machine + verified `History` (undo/redo/preview/commitFrom) all in one `domain.ts` — the original Dafny's `Domain refines Kernel` abstract-module pattern inlined since LS has no abstract modules. 108 VCs in `domain.dfy` (invariant preservation) + 123 in `domain.proofs.dfy` (behavioral lemmas + Kernel round-trip). Dafny only.
|
|
25
25
|
- **[node-casbin-lemmascript](https://github.com/midspiral/node-casbin-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [node-casbin](https://github.com/casbin/node-casbin). 5 functions verified, 217 existing tests pass. End-to-end correctness and order independence for all 4 effect modes in both Lean and Dafny (39 lemmas).
|
|
26
26
|
- **[hono-lemmascript](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [hono](https://github.com/honojs/hono)'s security middleware. Four CVEs covered: IP restriction bypass ([CVE-2026-39409](https://github.com/honojs/hono/security/advisories/GHSA-3mpf-rcc7-5347)) and cookie name bypass ([CVE-2026-39410](https://github.com/honojs/hono/security/advisories/GHSA-r5rp-j6wh-rvv4)) — 51 Dafny lemmas, [cookie verification **in-place**](https://github.com/midspiral/hono-lemmascript/blob/lemmascript/src/utils/cookie.ts#L79); plus `serveStatic`'s URL-encoded directory traversal ([CVE-2024-32869](https://github.com/honojs/hono/security/advisories/GHSA-q5w7-8mq6-2hxq)) + repeated-slash bypass ([CVE-2026-39407](https://github.com/honojs/hono/security/advisories/GHSA-jw53-c2g8-vmwm)), proved as a *composition* — `decode(rawPath)` before `check(decoded)`, so a buggy implementation that reordered the steps would fail the proof. First use of `//@ assume` + `//@ havoc`-on-assign. Dafny only.
|
|
27
|
-
- **[hono-rate-limiter-with-lemmascript](https://github.com/midspiral/hono-rate-limiter-with-lemmascript)** — a greenfield verified feature shipped through a brownfield host's *middleware seam*: a standalone [Hono](https://hono.dev) rate-limit middleware whose admission decision is a machine-checked theorem.
|
|
27
|
+
- **[hono-rate-limiter-with-lemmascript](https://github.com/midspiral/hono-rate-limiter-with-lemmascript)** — a greenfield verified feature shipped through a brownfield host's *middleware seam*: a standalone [Hono](https://hono.dev) sliding-window rate-limit middleware whose admission decision is a machine-checked theorem. `SlidingWindowBound` proves no half-open window `(s, s+W]` ever admits more than `limit`, so the boundary-straddling 2× burst that every fixed-window limiter leaks is *impossible* (the naive cousin is refuted in the same file, `FixedWindowLeaks`); the proof is wired into a live Hono server, with the clock's monotonicity and the per-key store's atomicity as the named trust boundary. 23 Dafny VCs, 0 errors. Dafny only.
|
|
28
28
|
- **[charmchat](https://github.com/CHARM-BDF/charmchat/blob/lemma/README_LemmaScript.md)** — brownfield verification of an AI agent orchestration backend. `isEmptyResult` (string emptiness predicate, 8 postconditions, <1s) and `topologicalSort` (Kahn's algorithm — memory safety, output bounds, completeness via acyclicity ranking witness, termination). Full completeness proof: 23 helper lemmas, 14 opaque ghost predicates, 115 loop invariants; 736 VCs verified under `--isolate-assertions --verification-time-limit 600`. Key technique: snapshot-based inner invariants (`ghost var originalRemDeps := remDeps`) replace the mid-iteration SEEN/UNSEEN split so preservation is frame reasoning against a ghost-constant rather than set-subtraction against mutating state. Dafny only.
|
|
29
29
|
- **[xyflow-lemmascript](https://github.com/midspiral/xyflow-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [xyflow](https://github.com/xyflow/xyflow)'s core edge and geometry utilities. 9 functions verified: `addEdge` (dedup — never loses edges, adds at most one), `reconnectEdge` (semantic: under a unique-id precondition, the result is in-place — `|result| ≤ |edges|`, no insertion — *and* when a matching edge existed with non-empty new endpoints, the output contains an edge with those endpoints. Uses `//@ assume` to characterize destructuring, `find`, and the constructed edge), `connectionExists`, `getEdgeCenter` (midpoint correctness), `clamp` (bounds), `rectToBox`/`boxToRect` (field arithmetic), `getBoundsOfBoxes` (enclosure), `getOverlappingArea` (non-negative), `areSetsEqual` (subset + same size). 14 Dafny proof obligations. Also adds a new verified feature — a **DAG connection gate**: `canReach` decides reachability soundly *and* completely, and `wouldCreateCycle` gates both new connections and edge reconnections so the graph is proven to stay acyclic, with `isAcyclic` (sound + complete) establishing the base case and a topological-rank witness giving a safe evaluation order (+29 obligations), shown live in a React Flow demo that refuses cycle-closing edges — extending the case study from verifying existing code to adding a verified feature. Dafny only.
|
|
30
30
|
- **[rallly-lemmascript](https://github.com/midspiral/rallly-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [rallly](https://github.com/lukevella/rallly)'s meeting-poll Next.js app. 2 functions: `validateRedirectUrl` (in-place — open-redirect predicate; non-`undefined` outputs start with `/` but not `//`) and `scorePoll` (extracted ranking core — length preservation, score bounds, top-choice characterization, score-formula equality, within-poll monotonicity, tiebreaker injectivity). The injectivity proof surfaced a real spec-level constraint on the existing `(yes + ifNeedBe) * 1000 + yes` encoding: it overflows when an option has ≥ 1000 yes votes. 10 Dafny VCs, 0 errors. Drove four toolchain additions: `s.startsWith()`, `T | null` nullability, `\result` narrowing under `==>`, `Math.max(...arr)` spread. Dafny only.
|
|
@@ -36,6 +36,7 @@ See the external case studies:
|
|
|
36
36
|
- **[quota-lemmascript](https://github.com/midspiral/quota-lemmascript)** — greenfield verified booking app: providers publish a page of limited-capacity *featured slots*, signed-in users grab them (React + Cloudflare Durable Objects + D1). A deliberate **inverse** of [quorum-lemmascript](https://github.com/midspiral/quorum-lemmascript): where Quorum counts *up to* a threshold over data partitioned **per participant** (no conflicts ⇒ optimistic, lock-free, no rollback), Quota's bookings **contend** for shared inventory, so the load-bearing fact flips from a count to a **bound** — for every slot `j`, `confirmedCount(bookings, j) <= slots[j].capacity` — and so does the concurrency story: the *same* `domain.ts` runs in the browser and the Durable Object, but **server-authoritatively** (it never oversells under contention; no optimistic client apply). Proven: no overbooking (invariant preservation across `tryBook` / cancel), accept-iff-room, an **idempotent three-way `tryBook`** (a retry reads as success, not rejection — only "confirmed" mutates), cancellation frees seats, replay determinism, and full order-invariance of availability under contention — `confirmedCountPerm` / `hasRoomPermInvariant` show availability depends only on the *multiset* of the booking log (any reordering, not just a pairwise swap), via the same `perm(...)` predicate Quorum drove into LemmaScript. NDJSON export is built on the verified `confirmedOnly`. The counting kernel is Quorum's `countFree` re-pointed at bookings — total and precondition-free so it composes. 80 Dafny VCs, 0 errors. Trust boundary (stated plainly): auth, the React UI, WebSocket/DO/D1 I/O, email, slot date/time labeling, and abuse/rate-limiting. Dafny only.
|
|
37
37
|
- **[henri-lemmascript](https://github.com/midspiral/henri-lemmascript)** — a runnable TypeScript coding-agent CLI (a port of [henri](https://github.com/metareflection/henri/); multi-provider, Anthropic + AWS Bedrock) whose security- and protocol-critical core is verified and **imported directly by the live agent** — `decide()` gates every real tool call and the conversation invariant is asserted on every turn (it streams end-to-end against Bedrock). Three modules, 48 Dafny VCs, 0 errors. (1) **Permission gate**: soundness (`decide == Allow ⟺ isAllowed`), **path-traversal containment** — auto-allow-in-cwd can never resolve outside cwd, with `.`/`..` normalization proved in-core so the shell is trusted only to `resolve().split('/')` — grant monotonicity, and `rejectPrompts` is deny-only. (2) **Conversation protocol**: tool-call/result pairing plus the [pi-lemmascript](https://github.com/midspiral/pi-lemmascript)-style **no-orphaned-`tool_result`** property, proved as an invariant *preserved by the loop* — `wellFormed(msgs + [assistant(calls), tool(makeResults(calls))])` — not checked after the fact. (3) **Hook/config merge**: removal, **tool-name uniqueness — a fix** (henri concatenated hook tool lists with no dedup, so two hooks could shadow a name), order-independence, and additivity composed **cross-module** with the gate's monotonicity (merging only grows the allow-sets, which by P3 never revokes an `Allow`). The merge is verified **in place** via `//@ declare-type Tool { name: string }`, shadowing the real `Tool`'s function-valued `execute` so the actual `mergeTools(Tool[])` is the proof target rather than a parallel model. Dafny only.
|
|
38
38
|
- **[eslint-plugin-with-lemmascript](https://github.com/midspiral/eslint-plugin-with-lemmascript)** — a greenfield verified feature shipped through a brownfield host's *extension API*: a real, npm-published [ESLint](https://eslint.org) flat-config plugin whose rule decision is a machine-checked theorem. `no-forbidden-reach` enforces architecture boundaries — "the UI must never reach the DB layer" — through **any** import chain, catching the *laundered* `ui → service → db` violation that every one-hop incumbent (`import/no-restricted-paths`, `eslint-plugin-boundaries`, Nx module boundaries) silently passes. The verified core decides reachability soundly *and* completely (`reachesAny` / `violates`) and **constructs** the offending chain in proven code (`findReachPath` — a path-carrying BFS proven sound + complete by mirroring the frontier's endpoints in a ghost seq, so completeness reduces to the same closure argument as the reachability search; the chain printed in the lint error is therefore itself a *verified* import path, not a heuristic guess). The headline is a meta-theorem — `Domination` + `Strictness` — proving the transitive check **strictly dominates** one-hop checking: every direct violation is caught, *and* there provably exist laundered violations that direct-edge checks miss. 30 Dafny VCs, 0 errors. The reachability decision and the witness are proven; the import-graph extraction (which edges exist) is the stated trust boundary, and `dist/*.js` is `tsc`'s erasure of the verified source it ships alongside. Dafny only.
|
|
39
|
+
- **[infisical-lemmascript](https://github.com/midspiral/infisical-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield, **in-place** verification of the permission-boundary glob set-containment check in [Infisical](https://github.com/Infisical/infisical) — the privilege-escalation guard that stops a role from delegating broader secret-path access than it holds. The loop-bearing `segmentMatch` compiles to a Dafny `method` (unnameable in specs), so soundness is proven by **refinement**: the method certifies `result == segMatchSpec` (a pure mirror), and a standalone lemma proves that spec sound against a hand-written segment-glob semantics — *if it returns true, every path the subset glob matches, the parent matches too*. 12 Dafny VCs, 0 errors; drove a toolchain fix (recursive `method`s now carry a method-level `//@ decreases`). Dafny only.
|
|
39
40
|
|
|
40
41
|
## Setup
|
|
41
42
|
|
package/package.json
CHANGED
package/tools/dist/dafny-emit.js
CHANGED
|
@@ -458,12 +458,19 @@ function emitExpr(e) {
|
|
|
458
458
|
let ctorName;
|
|
459
459
|
if (e.fields.length > 0) {
|
|
460
460
|
const fieldNames = new Set(e.fields.map(f => f.name));
|
|
461
|
+
const matches = [];
|
|
461
462
|
for (const [name, fields] of _structureDecls) {
|
|
462
463
|
if (fields.length >= e.fields.length && fields.every(f => fieldNames.has(f.name) || f.type.kind === "optional")) {
|
|
463
|
-
|
|
464
|
-
break;
|
|
464
|
+
matches.push(name);
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
|
+
// When several datatypes share this field-name set (e.g. Event vs
|
|
468
|
+
// SparseEvent), disambiguate by the resolved record type carried from
|
|
469
|
+
// transform; otherwise take the sole structural match.
|
|
470
|
+
if (e.ctor && matches.includes(e.ctor))
|
|
471
|
+
ctorName = e.ctor;
|
|
472
|
+
else if (matches.length > 0)
|
|
473
|
+
ctorName = matches[0];
|
|
467
474
|
if (!ctorName)
|
|
468
475
|
ctorName = _recordCtors.get(e.fields[0].name);
|
|
469
476
|
}
|
|
@@ -708,6 +715,8 @@ function emitDecl(d) {
|
|
|
708
715
|
lines.push(` requires ${emitExpr(r)}`);
|
|
709
716
|
for (const e of d.ensures)
|
|
710
717
|
lines.push(` ensures ${emitExpr(e)}`);
|
|
718
|
+
if (d.decreases)
|
|
719
|
+
lines.push(` decreases ${emitExpr(d.decreases)}`);
|
|
711
720
|
lines.push(`{`);
|
|
712
721
|
lines.push(emitStmts(d.body, 1));
|
|
713
722
|
lines.push(`}`);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lean IR → text. Trivial pretty-printer.
|
|
3
|
+
* No logic, no type decisions — just serialization.
|
|
4
|
+
*/
|
|
5
|
+
// ── Lean keyword escaping ────────────────────────────────────
|
|
6
|
+
const LEAN_KEYWORDS = new Set([
|
|
7
|
+
"def", "theorem", "lemma", "example", "structure", "class", "instance",
|
|
8
|
+
"inductive", "where", "match", "with", "if", "then", "else", "do",
|
|
9
|
+
"let", "mut", "return", "for", "in", "while", "break", "continue",
|
|
10
|
+
"import", "open", "section", "namespace", "end", "set_option",
|
|
11
|
+
"variable", "axiom", "constant", "private", "protected", "noncomputable",
|
|
12
|
+
"partial", "unsafe", "macro", "syntax", "by", "fun", "have", "show",
|
|
13
|
+
"at", "from", "to", "deriving", "extends", "true", "false",
|
|
14
|
+
]);
|
|
15
|
+
function escapeName(name) {
|
|
16
|
+
return LEAN_KEYWORDS.has(name) ? `«${name}»` : name;
|
|
17
|
+
}
|
|
18
|
+
// ── Operator precedence (for parenthesization) ──────────────
|
|
19
|
+
const PREC = {
|
|
20
|
+
"→": 1, "∨": 2, "∧": 3,
|
|
21
|
+
"=": 4, "≠": 4, "≥": 4, "≤": 4, ">": 4, "<": 4,
|
|
22
|
+
"+": 5, "-": 5, "*": 6, "/": 6, "%": 6,
|
|
23
|
+
};
|
|
24
|
+
function prec(op) { return PREC[op] ?? 10; }
|
|
25
|
+
// ── Expression emission ─────────────────────────────────────
|
|
26
|
+
function emitExpr(e, parentPrec) {
|
|
27
|
+
switch (e.kind) {
|
|
28
|
+
case "var": return escapeName(e.name);
|
|
29
|
+
case "num": return `${e.value}`;
|
|
30
|
+
case "bool": return e.value ? "true" : "false";
|
|
31
|
+
case "str": return `"${e.value}"`;
|
|
32
|
+
case "constructor": return `.${e.name}`;
|
|
33
|
+
case "arrayLiteral":
|
|
34
|
+
if (e.elems.length === 0)
|
|
35
|
+
return `#[]`;
|
|
36
|
+
return `#[${e.elems.map(el => emitExpr(el)).join(", ")}]`;
|
|
37
|
+
case "dotCall": {
|
|
38
|
+
const obj = emitExpr(e.obj);
|
|
39
|
+
const wrap = e.obj.kind === "binop" || e.obj.kind === "app" || e.obj.kind === "dotCall";
|
|
40
|
+
const receiver = wrap ? `(${obj})` : obj;
|
|
41
|
+
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
42
|
+
return args.length > 0 ? `${receiver}.${e.method} ${args.join(" ")}` : `${receiver}.${e.method}`;
|
|
43
|
+
}
|
|
44
|
+
case "lambda": {
|
|
45
|
+
const params = e.params.map(p => p.name).join(" ");
|
|
46
|
+
// Single return statement → expression lambda
|
|
47
|
+
if (e.body.length === 1 && e.body[0].kind === "return") {
|
|
48
|
+
return `(fun ${params} => ${emitExpr(e.body[0].value)})`;
|
|
49
|
+
}
|
|
50
|
+
// Multi-statement → do block
|
|
51
|
+
return `(fun ${params} => do\n${emitStmts(e.body, 2)})`;
|
|
52
|
+
}
|
|
53
|
+
case "unop":
|
|
54
|
+
if (e.op === "¬")
|
|
55
|
+
return `¬(${emitExpr(e.expr)})`;
|
|
56
|
+
if (e.op === "-" && e.expr.kind === "num")
|
|
57
|
+
return `-${e.expr.value}`;
|
|
58
|
+
return `(-${emitExpr(e.expr)})`;
|
|
59
|
+
case "binop": {
|
|
60
|
+
const s = `${emitExpr(e.left, prec(e.op))} ${e.op} ${emitExpr(e.right, prec(e.op))}`;
|
|
61
|
+
return (parentPrec !== undefined && prec(e.op) < parentPrec) ? `(${s})` : s;
|
|
62
|
+
}
|
|
63
|
+
case "implies": {
|
|
64
|
+
const parts = [...e.premises.map(p => emitExpr(p)), emitExpr(e.conclusion)];
|
|
65
|
+
const s = parts.join(" → ");
|
|
66
|
+
return parentPrec !== undefined ? `(${s})` : s;
|
|
67
|
+
}
|
|
68
|
+
case "app": {
|
|
69
|
+
const args = e.args.map(a => (a.kind === "binop" || a.kind === "unop" || a.kind === "implies" || a.kind === "app") ? `(${emitExpr(a)})` : emitExpr(a));
|
|
70
|
+
return `${e.fn} ${args.join(" ")}`;
|
|
71
|
+
}
|
|
72
|
+
case "field": {
|
|
73
|
+
const obj = emitExpr(e.obj);
|
|
74
|
+
const wrap = e.obj.kind !== "var" && e.obj.kind !== "num" && e.obj.kind !== "bool";
|
|
75
|
+
return wrap ? `(${obj}).${escapeName(e.field)}` : `${obj}.${escapeName(e.field)}`;
|
|
76
|
+
}
|
|
77
|
+
case "toNat": {
|
|
78
|
+
const inner = emitExpr(e.expr);
|
|
79
|
+
const wrap = e.expr.kind !== "var" && e.expr.kind !== "num";
|
|
80
|
+
return wrap ? `(${inner}).toNat` : `${inner}.toNat`;
|
|
81
|
+
}
|
|
82
|
+
case "index":
|
|
83
|
+
return `${emitExpr(e.arr)}[${emitExpr(e.idx)}]!`;
|
|
84
|
+
case "record": {
|
|
85
|
+
const fields = e.fields.map(f => `${escapeName(f.name)} := ${emitExpr(f.value)}`);
|
|
86
|
+
if (e.spread)
|
|
87
|
+
return `{ ${emitExpr(e.spread)} with ${fields.join(", ")} }`;
|
|
88
|
+
return `{ ${fields.join(", ")} }`;
|
|
89
|
+
}
|
|
90
|
+
case "if":
|
|
91
|
+
return `if ${emitExpr(e.cond)} then ${emitExpr(e.then)} else ${emitExpr(e.else)}`;
|
|
92
|
+
case "match": {
|
|
93
|
+
const arms = e.arms.map(a => `| ${a.pattern} => ${emitExpr(a.body)}`);
|
|
94
|
+
return `match ${e.scrutinee} with ${arms.join(" ")}`;
|
|
95
|
+
}
|
|
96
|
+
case "forall": return `∀ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
97
|
+
case "exists": return `∃ ${e.var} : ${e.type}, ${emitExpr(e.body)}`;
|
|
98
|
+
case "let": return `let ${e.name} := ${emitExpr(e.value)}\n${emitExpr(e.body)}`;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// ── Statement emission ──────────────────────────────────────
|
|
102
|
+
function emitStmts(stmts, indent) {
|
|
103
|
+
const pad = " ".repeat(indent);
|
|
104
|
+
return stmts.map(s => emitStmt(s, indent)).join("\n");
|
|
105
|
+
}
|
|
106
|
+
function emitStmt(s, indent) {
|
|
107
|
+
const pad = " ".repeat(indent);
|
|
108
|
+
switch (s.kind) {
|
|
109
|
+
case "let":
|
|
110
|
+
return s.mutable
|
|
111
|
+
? `${pad}let mut ${escapeName(s.name)} : ${s.type} := ${emitExpr(s.value)}`
|
|
112
|
+
: `${pad}let ${escapeName(s.name)} := ${emitExpr(s.value)}`;
|
|
113
|
+
case "assign": return `${pad}${escapeName(s.target)} := ${emitExpr(s.value)}`;
|
|
114
|
+
case "bind": return `${pad}${escapeName(s.target)} ← ${emitExpr(s.value)}`;
|
|
115
|
+
case "let-bind": return `${pad}let ${s.name} ← ${emitExpr(s.value)}`;
|
|
116
|
+
case "return": return `${pad}return ${emitExpr(s.value)}`;
|
|
117
|
+
case "break": return `${pad}break`;
|
|
118
|
+
case "continue": return `${pad}continue`;
|
|
119
|
+
case "if": {
|
|
120
|
+
let out = `${pad}if ${emitExpr(s.cond)} then\n${emitStmts(s.then, indent + 1)}`;
|
|
121
|
+
if (s.else.length > 0) {
|
|
122
|
+
if (s.else.length === 1 && s.else[0].kind === "if") {
|
|
123
|
+
const ei = s.else[0];
|
|
124
|
+
out += `\n${pad}else if ${emitExpr(ei.cond)} then\n${emitStmts(ei.then, indent + 1)}`;
|
|
125
|
+
if (ei.else.length > 0)
|
|
126
|
+
out += `\n${pad}else\n${emitStmts(ei.else, indent + 1)}`;
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
out += `\n${pad}else\n${emitStmts(s.else, indent + 1)}`;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
case "match": {
|
|
135
|
+
const lines = [`${pad}match ${s.scrutinee} with`];
|
|
136
|
+
for (const arm of s.arms) {
|
|
137
|
+
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
138
|
+
lines.push(emitStmts(arm.body, indent + 1));
|
|
139
|
+
}
|
|
140
|
+
return lines.join("\n");
|
|
141
|
+
}
|
|
142
|
+
case "while": {
|
|
143
|
+
const lines = [`${pad}while ${emitExpr(s.cond)}`];
|
|
144
|
+
for (const inv of s.invariants)
|
|
145
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
146
|
+
if (s.doneWith)
|
|
147
|
+
lines.push(`${pad} done_with ${emitExpr(s.doneWith)}`);
|
|
148
|
+
if (s.decreasing)
|
|
149
|
+
lines.push(`${pad} decreasing ${emitExpr(s.decreasing)}`);
|
|
150
|
+
lines.push(`${pad}do`);
|
|
151
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
152
|
+
return lines.join("\n");
|
|
153
|
+
}
|
|
154
|
+
case "forin": {
|
|
155
|
+
const lines = [`${pad}for ${s.idx} in [:${emitExpr(s.bound)}]`];
|
|
156
|
+
for (const inv of s.invariants)
|
|
157
|
+
lines.push(`${pad} invariant ${emitExpr(inv)}`);
|
|
158
|
+
lines.push(`${pad}do`);
|
|
159
|
+
lines.push(emitStmts(s.body, indent + 1));
|
|
160
|
+
return lines.join("\n");
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// ── Declaration emission ─────────────────────────────────────
|
|
165
|
+
function emitDecl(d) {
|
|
166
|
+
switch (d.kind) {
|
|
167
|
+
case "inductive": {
|
|
168
|
+
const lines = [`inductive ${d.name} where`];
|
|
169
|
+
for (const c of d.constructors) {
|
|
170
|
+
if (c.fields.length === 0) {
|
|
171
|
+
lines.push(` | ${c.name} : ${d.name}`);
|
|
172
|
+
}
|
|
173
|
+
else {
|
|
174
|
+
const params = c.fields.map(f => `(${escapeName(f.name)} : ${f.type})`).join(" ");
|
|
175
|
+
lines.push(` | ${c.name} ${params} : ${d.name}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (d.deriving.length > 0)
|
|
179
|
+
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
180
|
+
return lines.join("\n");
|
|
181
|
+
}
|
|
182
|
+
case "structure": {
|
|
183
|
+
const lines = [`structure ${d.name} where`];
|
|
184
|
+
for (const f of d.fields)
|
|
185
|
+
lines.push(` ${escapeName(f.name)} : ${f.type}`);
|
|
186
|
+
if (d.deriving.length > 0)
|
|
187
|
+
lines.push(`deriving ${d.deriving.join(", ")}`);
|
|
188
|
+
return lines.join("\n");
|
|
189
|
+
}
|
|
190
|
+
case "def": {
|
|
191
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
192
|
+
return `def ${d.name} ${params} : ${d.returnType} :=\n${emitPureExpr(d.body, 1)}`;
|
|
193
|
+
}
|
|
194
|
+
case "method": {
|
|
195
|
+
const params = d.params.map(p => `(${escapeName(p.name)} : ${p.type})`).join(" ");
|
|
196
|
+
const lines = [`method ${d.name} ${params} return (res : ${d.returnType})`];
|
|
197
|
+
for (const r of d.requires)
|
|
198
|
+
lines.push(` require ${emitExpr(r)}`);
|
|
199
|
+
for (const e of d.ensures)
|
|
200
|
+
lines.push(` ensures ${emitExpr(e)}`);
|
|
201
|
+
lines.push(" do");
|
|
202
|
+
lines.push(emitStmts(d.body, 2));
|
|
203
|
+
return lines.join("\n");
|
|
204
|
+
}
|
|
205
|
+
case "namespace": {
|
|
206
|
+
const lines = [`namespace ${d.name}`];
|
|
207
|
+
for (const inner of d.decls)
|
|
208
|
+
lines.push("", emitDecl(inner));
|
|
209
|
+
lines.push("", `end ${d.name}`);
|
|
210
|
+
return lines.join("\n");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/** Emit a pure expression with indented if/match blocks. */
|
|
215
|
+
function emitPureExpr(e, indent) {
|
|
216
|
+
const pad = " ".repeat(indent);
|
|
217
|
+
switch (e.kind) {
|
|
218
|
+
case "if":
|
|
219
|
+
return `${pad}if ${emitExpr(e.cond)} then\n${emitPureExpr(e.then, indent + 1)}\n${pad}else\n${emitPureExpr(e.else, indent + 1)}`;
|
|
220
|
+
case "match": {
|
|
221
|
+
const lines = [`${pad}match ${e.scrutinee} with`];
|
|
222
|
+
for (const arm of e.arms) {
|
|
223
|
+
lines.push(`${pad}| ${arm.pattern} =>`);
|
|
224
|
+
lines.push(emitPureExpr(arm.body, indent + 1));
|
|
225
|
+
}
|
|
226
|
+
return lines.join("\n");
|
|
227
|
+
}
|
|
228
|
+
case "let":
|
|
229
|
+
return `${pad}let ${e.name} := ${emitExpr(e.value)}\n${emitPureExpr(e.body, indent)}`;
|
|
230
|
+
default:
|
|
231
|
+
return `${pad}${emitExpr(e)}`;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// ── File emission ────────────────────────────────────────────
|
|
235
|
+
export function emitFile(file) {
|
|
236
|
+
const lines = [];
|
|
237
|
+
if (file.comment) {
|
|
238
|
+
lines.push("/-");
|
|
239
|
+
lines.push(file.comment);
|
|
240
|
+
lines.push("-/");
|
|
241
|
+
}
|
|
242
|
+
for (const imp of file.imports)
|
|
243
|
+
lines.push(`import ${imp}`);
|
|
244
|
+
if (file.options.length > 0)
|
|
245
|
+
lines.push("");
|
|
246
|
+
for (const opt of file.options)
|
|
247
|
+
lines.push(`set_option ${opt.key} ${opt.value}`);
|
|
248
|
+
for (const decl of file.decls) {
|
|
249
|
+
lines.push("");
|
|
250
|
+
lines.push(emitDecl(decl));
|
|
251
|
+
}
|
|
252
|
+
return lines.join("\n") + "\n";
|
|
253
|
+
}
|
package/tools/dist/narrow.js
CHANGED
|
@@ -44,25 +44,29 @@ let _typeDecls = [];
|
|
|
44
44
|
* pure-access-path optional-typed e. `!e` is equivalent to `=== undefined`.
|
|
45
45
|
* Following TS, only pure access paths narrow; complex scrutinees return null. */
|
|
46
46
|
function parseOptionalCheck(cond) {
|
|
47
|
-
// `!e` where e is optional —
|
|
47
|
+
// `!e` where e is optional — a truthiness form: false iff e is absent OR its
|
|
48
|
+
// inner value is itself falsy (so `Some(0)`/`Some("")` count as falsy too).
|
|
48
49
|
if (cond.kind === "unop" && cond.op === "!" && cond.expr.ty.kind === "optional") {
|
|
49
50
|
const e = cond.expr;
|
|
50
51
|
const innerTy = cond.expr.ty.inner;
|
|
51
52
|
const hint = binderHintFor(e);
|
|
52
53
|
if (hint === null)
|
|
53
54
|
return null;
|
|
54
|
-
return { scrutinee: e, innerTy, negated: true, binderHint: hint };
|
|
55
|
+
return { scrutinee: e, innerTy, negated: true, binderHint: hint, truthiness: true };
|
|
55
56
|
}
|
|
56
57
|
if (cond.kind !== "binop" || (cond.op !== "!==" && cond.op !== "===")) {
|
|
57
|
-
// Bare optional truthiness: `if (e)` where e: T | undefined —
|
|
58
|
+
// Bare optional truthiness: `if (e)` where e: T | undefined — true iff e is
|
|
59
|
+
// present AND its inner value is truthy.
|
|
58
60
|
if (cond.ty.kind === "optional") {
|
|
59
61
|
const hint = binderHintFor(cond);
|
|
60
62
|
if (hint === null)
|
|
61
63
|
return null;
|
|
62
|
-
return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: hint };
|
|
64
|
+
return { scrutinee: cond, innerTy: cond.ty.inner, negated: false, binderHint: hint, truthiness: true };
|
|
63
65
|
}
|
|
64
66
|
return null;
|
|
65
67
|
}
|
|
68
|
+
// Explicit `e === undefined` / `e !== undefined` — a pure presence check,
|
|
69
|
+
// independent of the inner value (so NOT a truthiness form).
|
|
66
70
|
let e = null;
|
|
67
71
|
if (cond.right.kind === "var" && cond.right.name === "undefined")
|
|
68
72
|
e = cond.left;
|
|
@@ -73,7 +77,7 @@ function parseOptionalCheck(cond) {
|
|
|
73
77
|
const hint = binderHintFor(e);
|
|
74
78
|
if (hint === null)
|
|
75
79
|
return null;
|
|
76
|
-
return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: hint };
|
|
80
|
+
return { scrutinee: e, innerTy: e.ty.inner, negated: cond.op === "===", binderHint: hint, truthiness: false };
|
|
77
81
|
}
|
|
78
82
|
function binderHintFor(e) {
|
|
79
83
|
// Pure access paths: var(x) or field(purePath, name).
|
|
@@ -205,6 +209,8 @@ function recurseStmt(s) {
|
|
|
205
209
|
fallthrough: rs(s.fallthrough) };
|
|
206
210
|
}
|
|
207
211
|
}
|
|
212
|
+
const canBeFalsy = (c) => c.truthiness && ["int", "nat", "string", "bool"].includes(c.innerTy.kind);
|
|
213
|
+
const bound = (c) => ({ kind: "var", name: c.binderHint, ty: c.innerTy });
|
|
208
214
|
// ── Rules ───────────────────────────────────────────────────
|
|
209
215
|
/** Rule: `if (e !== undefined) then else` where e is a simple optional var or
|
|
210
216
|
* `obj.field` chain, and the Some branch is non-empty.
|
|
@@ -223,7 +229,8 @@ function ruleIfOptionalSimple(s) {
|
|
|
223
229
|
kind: "someMatch",
|
|
224
230
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
225
231
|
binder: check.binderHint,
|
|
226
|
-
someBody, noneBody,
|
|
232
|
+
someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: someBody, else: noneBody }] : someBody,
|
|
233
|
+
noneBody,
|
|
227
234
|
};
|
|
228
235
|
}
|
|
229
236
|
/** Rule: `if (e === undefined) terminate; rest` (early return / throw / break).
|
|
@@ -246,7 +253,7 @@ function ruleEarlyReturnConsume(s, rest) {
|
|
|
246
253
|
kind: "someMatch",
|
|
247
254
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
248
255
|
binder: check.binderHint,
|
|
249
|
-
someBody: rest,
|
|
256
|
+
someBody: canBeFalsy(check) ? [{ kind: "if", cond: bound(check), then: rest, else: noneBranch }] : rest,
|
|
250
257
|
noneBody: noneBranch,
|
|
251
258
|
};
|
|
252
259
|
}
|
|
@@ -308,7 +315,8 @@ function ruleConditionalOptionalSimple(e) {
|
|
|
308
315
|
kind: "someMatch",
|
|
309
316
|
scrutinee: check.scrutinee, binderTy: check.innerTy,
|
|
310
317
|
binder: check.binderHint,
|
|
311
|
-
someBody, noneBody,
|
|
318
|
+
someBody: canBeFalsy(check) ? { kind: "conditional", cond: bound(check), then: someBody, else: noneBody, ty: e.ty } : someBody,
|
|
319
|
+
noneBody,
|
|
312
320
|
ty: e.ty,
|
|
313
321
|
};
|
|
314
322
|
}
|
package/tools/dist/peephole.js
CHANGED
|
@@ -440,7 +440,8 @@ function peepholeDecl(d) {
|
|
|
440
440
|
decreases: d.decreases ? peepholeExpr(d.decreases) : null };
|
|
441
441
|
case "method":
|
|
442
442
|
return { ...d, body: peepholeStmts(d.body),
|
|
443
|
-
requires: d.requires.map(peepholeExpr), ensures: d.ensures.map(peepholeExpr)
|
|
443
|
+
requires: d.requires.map(peepholeExpr), ensures: d.ensures.map(peepholeExpr),
|
|
444
|
+
decreases: d.decreases ? peepholeExpr(d.decreases) : null };
|
|
444
445
|
case "namespace": return { ...d, decls: d.decls.map(peepholeDecl) };
|
|
445
446
|
case "class": return { ...d, methods: d.methods.map(m => peepholeDecl(m)) };
|
|
446
447
|
case "const": return { ...d, value: peepholeExpr(d.value) };
|
package/tools/dist/transform.js
CHANGED
|
@@ -248,15 +248,18 @@ function flattenLambdaBody(stmts) {
|
|
|
248
248
|
* field, index, record, forall, or exists sub-expressions.
|
|
249
249
|
*/
|
|
250
250
|
/** JS truthiness coercion for `if`/`while`/`?:` conditions.
|
|
251
|
-
* Dafny requires bool;
|
|
251
|
+
* Dafny requires bool; coerce number→`≠0`, string→non-empty, array→`true`
|
|
252
|
+
* (every array, even `[]`, is truthy in JS).
|
|
252
253
|
* Optional conds are handled separately by narrow.ts (rewritten to someMatch). */
|
|
253
254
|
function coerceCondToBool(cond, ty) {
|
|
254
255
|
if (ty.kind === "bool")
|
|
255
256
|
return cond;
|
|
256
257
|
if (ty.kind === "int" || ty.kind === "nat")
|
|
257
|
-
return { kind: "binop", op: "
|
|
258
|
-
if (ty.kind === "string"
|
|
259
|
-
return { kind: "binop", op: ">", left: { kind: "field", obj: cond, field: "
|
|
258
|
+
return { kind: "binop", op: "≠", left: cond, right: { kind: "num", value: 0 } };
|
|
259
|
+
if (ty.kind === "string")
|
|
260
|
+
return { kind: "binop", op: ">", left: { kind: "field", obj: cond, field: "length" }, right: { kind: "num", value: 0 } };
|
|
261
|
+
if (ty.kind === "array")
|
|
262
|
+
return { kind: "bool", value: true };
|
|
260
263
|
return cond;
|
|
261
264
|
}
|
|
262
265
|
/** Wrap an expression in Some/None for optional-typed conditionals.
|
|
@@ -309,6 +312,14 @@ function lowerExpr(e, binds) {
|
|
|
309
312
|
],
|
|
310
313
|
};
|
|
311
314
|
}
|
|
315
|
+
// Number truthiness: !n → n == 0
|
|
316
|
+
if (e.op === "!" && (e.expr.ty.kind === "int" || e.expr.ty.kind === "nat"))
|
|
317
|
+
return { kind: "binop", op: "=", left: lowerExpr(e.expr, binds), right: { kind: "num", value: 0 } };
|
|
318
|
+
// Array truthiness: every array is truthy in JS, so !xs is always false.
|
|
319
|
+
if (e.op === "!" && e.expr.ty.kind === "array") {
|
|
320
|
+
lowerExpr(e.expr, binds); // preserve any lifted side effects; value is the constant false
|
|
321
|
+
return { kind: "bool", value: false };
|
|
322
|
+
}
|
|
312
323
|
return { kind: "unop", op: e.op === "!" ? "¬" : e.op, expr: lowerExpr(e.expr, binds) };
|
|
313
324
|
case "binop": {
|
|
314
325
|
// Implication: flatten (A && B) ==> C → implies [A, B] C
|
|
@@ -327,8 +338,10 @@ function lowerExpr(e, binds) {
|
|
|
327
338
|
right: { kind: "constructor", name: e.right.value, type: objTy },
|
|
328
339
|
};
|
|
329
340
|
}
|
|
330
|
-
// String literal comparison — constructor if user type, string literal if string
|
|
331
|
-
|
|
341
|
+
// String literal comparison — constructor if user type, string literal if string.
|
|
342
|
+
// Skip when the left is optional: fall through to the optional-comparison rule,
|
|
343
|
+
// which unwraps and compares the inner value (`Some(v) => v == "")`.
|
|
344
|
+
if ((e.op === "===" || e.op === "!==") && e.right.kind === "str" && e.left.ty.kind !== "optional") {
|
|
332
345
|
const left = lowerExpr(e.left, binds);
|
|
333
346
|
const leftTy = e.left.ty.kind === "user" ? e.left.ty.name : undefined;
|
|
334
347
|
const right = isUser(e.left.ty)
|
|
@@ -352,7 +365,13 @@ function lowerExpr(e, binds) {
|
|
|
352
365
|
],
|
|
353
366
|
};
|
|
354
367
|
}
|
|
355
|
-
|
|
368
|
+
// A string literal compared against an optional user type is the variant
|
|
369
|
+
// constructor (`o === "red"` → `Some(v) => v == Color.red`), mirroring the
|
|
370
|
+
// non-optional string-literal rule above.
|
|
371
|
+
const innerTy = optSide.ty.kind === "optional" ? optSide.ty.inner : optSide.ty;
|
|
372
|
+
const valExpr = valSide.kind === "str" && innerTy.kind === "user"
|
|
373
|
+
? { kind: "constructor", name: valSide.value, type: innerTy.name }
|
|
374
|
+
: lowerExpr(valSide, binds);
|
|
356
375
|
const cmpOp = BOOL_OP_MAP[e.op] ?? e.op;
|
|
357
376
|
const noneVal = e.op === "!==" ? true : false;
|
|
358
377
|
const bound = matchBinder("value");
|
|
@@ -402,7 +421,9 @@ function lowerExpr(e, binds) {
|
|
|
402
421
|
const rightIsUndef = e.right.kind === "var" && e.right.name === "undefined";
|
|
403
422
|
return {
|
|
404
423
|
kind: "if",
|
|
405
|
-
|
|
424
|
+
// strings carry the `length` marker, arrays `size` — both render to `|x|`
|
|
425
|
+
// in Dafny, but Lean's String has no `.size` field (it's `.length`).
|
|
426
|
+
cond: { kind: "binop", op: ">", left: { kind: "field", obj: left, field: e.left.ty.kind === "string" ? "length" : "size" }, right: { kind: "num", value: 0 } },
|
|
406
427
|
then: rightIsUndef ? { kind: "app", fn: "Some", args: [left] } : left,
|
|
407
428
|
else: right,
|
|
408
429
|
};
|
|
@@ -676,7 +697,14 @@ function lowerExpr(e, binds) {
|
|
|
676
697
|
})),
|
|
677
698
|
};
|
|
678
699
|
}
|
|
679
|
-
|
|
700
|
+
// Carry the resolved record type so the emitter can pick the right
|
|
701
|
+
// constructor when two datatypes share a field-name set (Event vs
|
|
702
|
+
// SparseEvent) — structural matching alone would take the first-declared.
|
|
703
|
+
const recName = e.ty.kind === "user"
|
|
704
|
+
? (e.ty.name.includes("<") ? e.ty.name.slice(0, e.ty.name.indexOf("<")) : e.ty.name)
|
|
705
|
+
: undefined;
|
|
706
|
+
const ctor = recName && _typeDecls.find(d => d.name === recName && d.kind === "record") ? recName : undefined;
|
|
707
|
+
return { kind: "record", spread: null, ctor, fields: e.fields.map(f => ({ name: f.name, value: lowerExpr(f.value, binds) })) };
|
|
680
708
|
}
|
|
681
709
|
case "arrayLiteral":
|
|
682
710
|
if (e.ty.kind === "map" && e.elems.length === 0)
|
|
@@ -1514,7 +1542,7 @@ function transformPureBody(stmts, typeDecls) {
|
|
|
1514
1542
|
const elseExpr = transformPureBody(elseStmts, typeDecls);
|
|
1515
1543
|
if (!elseExpr)
|
|
1516
1544
|
return null;
|
|
1517
|
-
return { kind: "if", cond: transformExpr(s.cond), then: thenExpr, else: elseExpr };
|
|
1545
|
+
return { kind: "if", cond: coerceCondToBool(transformExpr(s.cond), s.cond.ty), then: thenExpr, else: elseExpr };
|
|
1518
1546
|
}
|
|
1519
1547
|
case "switch": return transformPureSwitch(s, typeDecls);
|
|
1520
1548
|
case "someMatch": {
|
|
@@ -1878,6 +1906,7 @@ export function transformModule(mod, specImport) {
|
|
|
1878
1906
|
returnType: fn.returnTy,
|
|
1879
1907
|
requires: fn.requires.map(transformExpr),
|
|
1880
1908
|
ensures,
|
|
1909
|
+
decreases: fn.decreases ? transformExpr(fn.decreases) : null,
|
|
1881
1910
|
body,
|
|
1882
1911
|
};
|
|
1883
1912
|
});
|
|
@@ -1895,6 +1924,7 @@ export function transformModule(mod, specImport) {
|
|
|
1895
1924
|
returnType: fn.returnTy,
|
|
1896
1925
|
requires: fn.requires.map(transformExpr),
|
|
1897
1926
|
ensures,
|
|
1927
|
+
decreases: fn.decreases ? transformExpr(fn.decreases) : null,
|
|
1898
1928
|
body,
|
|
1899
1929
|
};
|
|
1900
1930
|
});
|