lemmascript 0.3.3 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/package.json +4 -1
- package/tools/dist/dafny-commands.js +31 -14
- package/tools/dist/dafny-emit.js +302 -17
- package/tools/dist/extract.js +1087 -181
- package/tools/dist/info-command.js +38 -0
- package/tools/dist/lean-emit.js +81 -5
- package/tools/dist/lsc.js +29 -9
- package/tools/dist/narrow.js +932 -0
- package/tools/dist/peephole.js +451 -0
- package/tools/dist/resolve.js +680 -258
- package/tools/dist/specparser.js +18 -2
- package/tools/dist/transform.js +597 -441
- package/tools/dist/types.js +128 -69
package/README.md
CHANGED
|
@@ -2,10 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
A verification toolchain for TypeScript. Write ordinary TypeScript with `//@ ` specification annotations. The toolchain generates verifiable code from your TypeScript — either in Dafny or Lean 4 (with Velvet/Loom).
|
|
4
4
|
|
|
5
|
-
See [SPEC.md](SPEC.md)
|
|
5
|
+
See [SPEC.md](SPEC.md), [DESIGN.md](DESIGN.md), and [GETTING_STARTED.md](GETTING_STARTED.md).
|
|
6
6
|
|
|
7
7
|
This is a **Tech Preview**: the core idea is there, but support, semantics, and ergonomics are still evolving.
|
|
8
8
|
|
|
9
|
+
See our [blog post](https://midspiral.com/blog/lemmascript-a-verification-toolchain-for-typescript/).
|
|
10
|
+
|
|
9
11
|
## Examples and Case Studies
|
|
10
12
|
|
|
11
13
|
Each example and case study is verified in Lean 4 and/or Dafny from the same annotated TypeScript source.
|
|
@@ -16,10 +18,15 @@ See the external case studies:
|
|
|
16
18
|
- **[collab-todo-lemmascript](https://github.com/midspiral/collab-todo-lemmascript/)** — collaborative task management web app (React + Supabase) with a verified domain model. Single `domain.ts` imported directly by the UI, hooks, and edge functions — no adapter layer. 123 Dafny lemmas (120 in a separate `domain.proofs.dfy`): 16-conjunct invariant preserved across 25 single-project + 3 cross-project actions, NoOp completeness/soundness, initialization. Dafny only.
|
|
17
19
|
- **[colorwheel-lemmascript](https://github.com/midspiral/colorwheel-lemmascript/)** — verified color palette generator with mood + harmony constraints. 31 Lean proofs + 18 behavioral properties, 115 Dafny lemmas (invariant preservation, commutativity, NoOp completeness).
|
|
18
20
|
- **[clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript/)** — greenfield verified expense splitting web app. Conservation theorem, invariant preservation, delta laws — all proven in both Lean (no sorry) and Dafny (56 lemmas).
|
|
21
|
+
- **[github-star-checker-lemmascript](https://github.com/midspiral/github-star-checker-lemmascript/)** — small verified CLI that tracks GitHub star counts across repos and reports per-run deltas. Verifies: per-row diff correctness and `totalDiff == sumDiffs(rows)` (via an inductive `SumDiffs_append` lemma); three sign-classified extractors (gainers / losers / unchanged) with soundness, completeness, **ordered completeness** (gainers appear in the notification in the same order they were listed on the command line), and count/sum equalities against prefix-indexed `upTo` helpers; conservation theorem `decompose(r)` — the three splits partition every row exactly once, and `sumDiffs(increases) + sumDiffs(decreases) == totalDiff`. 33 Dafny VCs, 0 errors; proof additions include a head/tail bridge (`sumDiffs` ↔ `sumDiffsUpTo`) and two partition-on-n inductions. Dafny only.
|
|
22
|
+
- **[equality-game-lemmascript](https://github.com/midspiral/equality-game-lemmascript/)** — greenfield verified arithmetic equality card game (React + Tailwind). Sound + complete decision procedure for "can these two card lists be combined into equal expressions": `canEqualize(L, R) ⟺ ∃ eL, eR. eval(eL) == eval(eR) ∧ multiset(leaves(eL)) == multiset(L) ∧ same for R`. Algorithm is subset-DP over a bitmask `m ∈ [1, 2^n − 1)`; the proof composes a `PopCount` upper/lower bound chain (with stdlib `LemmaDivDenominator` / `LemmaFundamentalDivModConverse`), a `splitLeft`/`splitRight` ↔ imperative-loop connection, a `WitnessCombine` lemma threading existential `Expr` witnesses through the cross-product loops, and a `ChooseMask` combinatorial constructor that, given any sub-multiset of `cards`, produces the realizing mask. Capped by `CompletenessFromMaskCoverage`. 753 verification conditions, 0 errors, 0 `assume`s, 0 axioms under `--isolate-assertions --verification-time-limit 180`. Dafny only.
|
|
23
|
+
- **[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.
|
|
19
24
|
- **[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).
|
|
20
|
-
- **[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.
|
|
21
|
-
- **[charmchat](https://github.com/CHARM-BDF/charmchat/
|
|
22
|
-
- **[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` (
|
|
25
|
+
- **[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.
|
|
26
|
+
- **[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.
|
|
27
|
+
- **[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. Dafny only.
|
|
28
|
+
- **[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.
|
|
29
|
+
- **[opencode-lemmascript](https://github.com/midspiral/opencode-lemmascript/blob/lemmascript/README_LemmaScript.md)** — brownfield verification of [opencode](https://github.com/anomalyco/opencode)'s permission system and unified-diff patch parser. Highlights: (1) `Patch.parsePatch` carries conservation loop invariants over local ghost state — a parser bug here would silently corrupt user files when an AI applies a patch, and (2) the permission-engine work mechanically closes opencode bug #26514 (subagents bypassing Plan Mode's file-edit restrictions). 9 functions verified in-place, 0 errors. Dafny only.
|
|
23
30
|
|
|
24
31
|
## Setup
|
|
25
32
|
|
|
@@ -60,7 +67,7 @@ The Dafny backend generates two files per TS source: `foo.dfy.gen` (always regen
|
|
|
60
67
|
### Lean backend
|
|
61
68
|
|
|
62
69
|
```sh
|
|
63
|
-
npx lsc gen src/myModule.ts
|
|
70
|
+
npx lsc gen --backend=lean src/myModule.ts
|
|
64
71
|
lake build
|
|
65
72
|
```
|
|
66
73
|
|
|
@@ -82,17 +89,17 @@ lake build
|
|
|
82
89
|
|
|
83
90
|
| File | Generated? | Purpose |
|
|
84
91
|
|------|-----------|---------|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
92
|
+
| [**.ts**](examples/majority.ts) | — | TypeScript source with `//@ ` annotations |
|
|
93
|
+
| [**.dfy.gen**](examples/majority.dfy.gen) | Yes | Generated Dafny (merge base, always regeneratable) |
|
|
94
|
+
| [**.dfy**](examples/majority.dfy) | Yes (initial) | Annotated Dafny (gen + proof additions) |
|
|
88
95
|
|
|
89
96
|
### Lean backend
|
|
90
97
|
|
|
91
98
|
| File | Generated? | Purpose |
|
|
92
99
|
|------|-----------|---------|
|
|
93
|
-
|
|
|
94
|
-
|
|
|
95
|
-
|
|
|
96
|
-
|
|
|
97
|
-
|
|
|
100
|
+
| [**.ts**](examples/majority.ts) | — | TypeScript source with `//@ ` annotations |
|
|
101
|
+
| [**.types.lean**](examples/majority.types.lean) | Yes | Lean types, `namespace Pure` defs |
|
|
102
|
+
| [**.spec.lean**](examples/majority.spec.lean) | No | Ghost definitions, helper lemmas |
|
|
103
|
+
| [**.def.lean**](examples/majority.def.lean) | Yes | Velvet method definitions |
|
|
104
|
+
| [**.proof.lean**](examples/majority.proof.lean) | No | `prove_correct` with proof tactics |
|
|
98
105
|
|
package/package.json
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lemmascript",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A verification toolchain for TypeScript — generates Lean 4 or Dafny from annotated TS",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=18"
|
|
8
|
+
},
|
|
6
9
|
"bin": {
|
|
7
10
|
"lsc": "tools/dist/lsc.js"
|
|
8
11
|
},
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Dafny backend commands: gen, check, regen.
|
|
3
3
|
*/
|
|
4
4
|
import { existsSync, readFileSync, writeFileSync, copyFileSync, unlinkSync } from "fs";
|
|
5
|
-
import {
|
|
5
|
+
import { execFileSync } from "child_process";
|
|
6
6
|
import path from "path";
|
|
7
7
|
function writeGen(genPath, text) {
|
|
8
8
|
writeFileSync(genPath, text);
|
|
@@ -18,27 +18,44 @@ export function dafnyGen(genPath, dfyPath, text) {
|
|
|
18
18
|
export function dafnyCheckDiff(genPath, dfyPath) {
|
|
19
19
|
if (!existsSync(dfyPath))
|
|
20
20
|
return true;
|
|
21
|
+
let diff = "";
|
|
21
22
|
try {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
diff = execFileSync("git", ["diff", "--no-index", "--", genPath, dfyPath], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
24
|
+
}
|
|
25
|
+
catch (e) {
|
|
26
|
+
// git diff exits 1 when files differ; stdout still holds the diff
|
|
27
|
+
if (e && e.stdout != null) {
|
|
28
|
+
diff = typeof e.stdout === "string" ? e.stdout : e.stdout.toString("utf-8");
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
return true;
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
|
-
|
|
34
|
+
const deletions = diff.split("\n").filter(l => l.startsWith("-") && !l.startsWith("---"));
|
|
35
|
+
if (deletions.length > 0) {
|
|
36
|
+
console.error(`WARNING: ${path.basename(dfyPath)} has modifications to generated lines (not additions-only):`);
|
|
37
|
+
for (const d of deletions.slice(0, 5))
|
|
38
|
+
console.error(" " + d);
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
32
41
|
return true;
|
|
33
42
|
}
|
|
34
43
|
export function dafnyVerify(dfyPath, dir, timeLimit, extraFlags) {
|
|
35
44
|
console.log("Running dafny verify...");
|
|
36
45
|
try {
|
|
37
46
|
const content = readFileSync(dfyPath, "utf-8");
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
const args = ["verify"];
|
|
48
|
+
if (content.includes("Std."))
|
|
49
|
+
args.push("--standard-libraries");
|
|
50
|
+
if (timeLimit)
|
|
51
|
+
args.push("--verification-time-limit", String(timeLimit));
|
|
52
|
+
if (extraFlags) {
|
|
53
|
+
for (const tok of extraFlags.split(/\s+/))
|
|
54
|
+
if (tok)
|
|
55
|
+
args.push(tok);
|
|
56
|
+
}
|
|
57
|
+
args.push(dfyPath);
|
|
58
|
+
execFileSync("dafny", args, { cwd: dir, stdio: "inherit" });
|
|
42
59
|
return true;
|
|
43
60
|
}
|
|
44
61
|
catch {
|
|
@@ -70,7 +87,7 @@ export function dafnyRegen(genPath, dfyPath, basePath, text, dir) {
|
|
|
70
87
|
const mergedPath = dfyPath + ".merged";
|
|
71
88
|
console.log("Gen changed. Three-way merging...");
|
|
72
89
|
try {
|
|
73
|
-
|
|
90
|
+
execFileSync("git", ["merge-file", dfyPath, basePath, genPath], { stdio: "pipe" });
|
|
74
91
|
console.log(`Merged: ${path.basename(dfyPath)}`);
|
|
75
92
|
}
|
|
76
93
|
catch (e) {
|