LemmaScript formal verification for TypeScript
LemmaScript is a verification toolchain for TypeScript. Ordinary TypeScript
carries //@ specification comments; the lsc CLI generates formal
artefacts that a backend prover (Dafny, or Lean via Velvet/Loom) checks. The
annotations are comments — invisible to tsc, bundlers, and the runtime — and
the annotated TypeScript source remains the shipped production code.
LemmaScript verifies a formal model of that source; proofs apply only within
the supported TypeScript fragment and documented modelling assumptions. See
references/annotations.md for semantic limits:
number uses mathematical-integer rather than IEEE-754 semantics, Map/Record
values can differ at runtime, lifted method calls lose JavaScript short-circuit
behaviour, and cross-file calls are axiomatized rather than body-verified.
UI/I/O/auth/clock/adapter code is trusted unless separately covered. Think
"Verus is to Rust as
LemmaScript is to TypeScript".
Where fast-check samples inputs and can only find bugs, LemmaScript proves
properties for every input. It costs far more effort per property, so reserve
it for load-bearing logic; use the fast-check skill for the broad regression
net beneath the proofs.
When to apply
Apply when a property must hold unconditionally and a violation is severe:
invariant preservation across a state machine's actions, conservation ("money
never leaks"), soundness and completeness of a decision procedure, security
predicates (path-traversal containment, open-redirect exclusion,
permission-gate correctness), parser conservation, or bounded-resource
guarantees (no overbooking, rate-limit bounds).
Do not apply to code outside the supported fragment (heavy async,
this-dispatch, closures over mutable state, regex, real I/O), to
float-sensitive numerics (the model treats number as a mathematical integer),
or when a sampled property-based test gives sufficient confidence — proofs are
expensive to write and to maintain.
Installation and prerequisites
Node.js ≥ 18. Dafny ≥ 4.x for the primary backend; elan plus the Loom/Velvet
forks for the Lean backend.
npm install lemmascript@0.5.22
For brownfield work, clone LemmaScript as a sibling of the target project
and invoke it from source (npx tsx ../LemmaScript/tools/src/lsc.ts ...) — it
is a tech preview, and the cleanest fix for a gap is sometimes in the toolchain
itself. The midspiral/hono-lemmascript repository is a complete worked
example with annotations, generated Dafny, proofs, and CI.
Core concepts
Annotate the function, then generate and verify:
export function firstIndexOf(arr: number[], target: number): number {
//@ verify
//@ requires arr.length > 0
//@ ensures \result >= -1 && \result < arr.length
//@ ensures \result === -1 ==> forall(k: nat, k < arr.length ==> arr[k] !== target)
//@ ensures \result >= 0 ==> arr[\result] === target
//@ ensures \result >= 0 ==> forall(k: nat, k < \result ==> arr[k] !== target)
let i = 0;
while (i < arr.length) {
//@ invariant 0 <= i && i <= arr.length
//@ invariant forall(k: nat, k < i ==> arr[k] !== target)
//@ decreases arr.length - i
if (arr[i] === target) return i;
i = i + 1;
}
return -1;
}
npx --yes --package=lemmascript@0.5.22 -- lsc gen --backend=dafny src/find.ts # generate artefacts
npx --yes --package=lemmascript@0.5.22 -- lsc check --backend=dafny src/find.ts # generate + verify
npx --yes --package=lemmascript@0.5.22 -- lsc regen --backend=dafny src/find.ts # regenerate, 3-way merge
Key pieces:
//@ verify opts a function in; once any function in a file has it,
only marked functions are extracted.
//@ requires / //@ ensures are the contract; \result names the
return value; ==> is implication; forall(k, P) / exists(k, P) quantify.
//@ invariant and //@ decreases go at the top of the loop body.
- Pure functions (no loops, no mutation) are callable from other
functions' specs;
//@ pure forces the classification.
- Unmodellable calls are handled by
//@ extern (deterministic
axiom), //@ havoc (nondeterministic value), or file-level //@ autohavoc
(abstract everything unmodellable, soundly).
The complete annotation surface, the spec expression language, and the semantic
gotcha list live in references/annotations.md.
The edit loop (Dafny backend)
lsc gen produces two files beside the source: foo.dfy.gen (always
regeneratable — never edit) and foo.dfy (source of truth — add helper lemmas,
ghost predicates, assert nudges here). The diff between them must be
additions only; lsc check enforces this. After editing the TypeScript, run
regen (never delete and gen fresh — that discards every proof). When Dafny
complains, the fix belongs either in the .ts (tighten requires, weaken
ensures, add invariant/decreases) or in the .dfy (helper lemma, ghost
predicate, assert).
The full workflow — backend choice, Lean's four-file scheme, proof debugging
flags, CI wiring, and brownfield strategy — lives in
references/workflow.md.
Anti-patterns
- Reaching for
//@ assume to silence a failure. It tells the
prover to trust the obligation unconditionally; the proof stops meaning
anything. Restructure, or prove a helper lemma. Its one sanctioned use is
constraining a deliberately havoced value.
- Refactoring production code "for clarity" during brownfield
verification. In-place verification is the point; an unchanged diff is the
evidence the verified code is the shipped code.
- Editing
.dfy.gen. It is regenerated; changes vanish. Edit the
.dfy.
- Deleting
.dfy files to "start clean". Proof work lives there;
regen's three-way merge preserves it.
- Verifying a parallel model instead of the real function. Prefer
in-place annotation; where a type can't be imported, shadow it with
//@ declare-type so the actual function stays the proof target.
- Proving what the spec doesn't say. Write the
//@ contract
intent line and check the ensures actually expresses it — a theorem about
the wrong predicate verifies happily.
Project integration
- State the trust boundary in the README: which functions are
proved, and which surrounding layers (UI, I/O, auth, clock) are trusted. Case
studies do this plainly; follow suit.
- Track verified files in
LemmaScript-files.txt (one file per
line, optional per-file timeout and flags) so tools/check.sh and CI verify
them; copy hono-lemmascript's GitHub Actions workflow as the template.
- Start small in brownfield code: pure helpers, predicates,
parsers without I/O. Grow towards the invariant-bearing core.
- Keep fast-check properties alongside proofs. They run in the
inner loop in milliseconds, catch spec regressions before a prover run, and
cover the unverified boundary code.
- Land toolchain fixes separately. When the tech preview needs a
patch (unsupported method, missed narrowing), fix
LemmaScript/tools/src/ in
its own PR.
Hard-won lessons
- The spec is the product. Provers check the written specification, not
the intended one. Review
ensures clauses as adversarially as code.
number is a mathematical integer in the model (floats become
real; overflow at 2^53 is out of scope). Do not verify float-sensitive
numerics; do flag integer-encoding overflow risks — a case study's
injectivity proof surfaced a real ≥1000-votes overflow in an existing
encoding.
- Missing invariants live in the TypeScript. The LLM/prover loop
can supply tactics and lemmas in the
.dfy, but a missing //@ invariant
must be added in the source and regenerated.
- Refinement scales. For loop-heavy code, prove the method equals
a pure recursive spec (
result == range_spec(...)), then prove properties of
the spec — every property transfers automatically.
- Verification conditions are cheap to split.
dafny verify --filter-symbol=... and --isolate-assertions turn one opaque
timeout into named, tractable obligations.
References
- LemmaScript repository
— SPEC.md, DESIGN.md, GETTING_STARTED.md, AGENTS.md, examples.
- Announcement post
and lemmascript.com.
- Worked case studies:
hono-lemmascript
(brownfield, CVE-driven),
clear-split-lemmascript
(greenfield, dual-backend),
collab-todo-lemmascript
(verified domain model behind a React app).
references/annotations.md for the
annotation surface, spec language, and gotchas.
references/workflow.md for the edit
loop, backends, CI, and brownfield strategy.
- Sampled-input testing as the complementary adversary lives in
../fast-check/SKILL.md.
1---2name: lemmascript3description: Verify TypeScript formally with LemmaScript: write `//@` specification annotations in ordinary TypeScript, generate Dafny or Lean artefacts with `lsc`, and discharge proof obligations so properties hold for all inputs, not just sampled ones. Trigger whenever the user mentions LemmaScript, lsc, formal verification or model checking of TypeScript or JavaScript, proving a TypeScript function correct, `//@ requires` / `//@ ensures` annotations, .dfy.gen files, or wants machine-checked guarantees (invariant preservation, conservation, soundness, completeness) for TypeScript code. Covers LemmaScript 0.5.x (tech preview) with the Dafny and Lean backends.4---56# LemmaScript formal verification for TypeScript78LemmaScript is a verification toolchain for TypeScript. Ordinary TypeScript9carries `//@` specification comments; the `lsc` CLI generates formal10artefacts that a backend prover (Dafny, or Lean via Velvet/Loom) checks. The11annotations are comments — invisible to tsc, bundlers, and the runtime — and12the annotated TypeScript source remains the shipped production code.13LemmaScript verifies a formal model of that source; proofs apply only within14the supported TypeScript fragment and documented modelling assumptions. See15[`references/annotations.md`](references/annotations.md) for semantic limits:16`number` uses mathematical-integer rather than IEEE-754 semantics, Map/Record17values can differ at runtime, lifted method calls lose JavaScript short-circuit18behaviour, and cross-file calls are axiomatized rather than body-verified.19UI/I/O/auth/clock/adapter code is trusted unless separately covered. Think20"Verus is to Rust as21LemmaScript is to TypeScript".2223Where fast-check samples inputs and can only find bugs, LemmaScript proves24properties for every input. It costs far more effort per property, so reserve25it for load-bearing logic; use the `fast-check` skill for the broad regression26net beneath the proofs.2728## When to apply2930Apply when a property must hold unconditionally and a violation is severe:31invariant preservation across a state machine's actions, conservation ("money32never leaks"), soundness and completeness of a decision procedure, security33predicates (path-traversal containment, open-redirect exclusion,34permission-gate correctness), parser conservation, or bounded-resource35guarantees (no overbooking, rate-limit bounds).3637Do not apply to code outside the supported fragment (heavy `async`,38`this`-dispatch, closures over mutable state, regex, real I/O), to39float-sensitive numerics (the model treats `number` as a mathematical integer),40or when a sampled property-based test gives sufficient confidence — proofs are41expensive to write and to maintain.4243## Installation and prerequisites4445Node.js ≥ 18. Dafny ≥ 4.x for the primary backend; `elan` plus the Loom/Velvet46forks for the Lean backend.4748```sh49npm install lemmascript@0.5.2250```5152For brownfield work, clone LemmaScript as a **sibling** of the target project53and invoke it from source (`npx tsx ../LemmaScript/tools/src/lsc.ts ...`) — it54is a tech preview, and the cleanest fix for a gap is sometimes in the toolchain55itself. The `midspiral/hono-lemmascript` repository is a complete worked56example with annotations, generated Dafny, proofs, and CI.5758## Core concepts5960Annotate the function, then generate and verify:6162```typescript63export function firstIndexOf(arr: number[], target: number): number {64 //@ verify65 //@ requires arr.length > 066 //@ ensures \result >= -1 && \result < arr.length67 //@ ensures \result === -1 ==> forall(k: nat, k < arr.length ==> arr[k] !== target)68 //@ ensures \result >= 0 ==> arr[\result] === target69 //@ ensures \result >= 0 ==> forall(k: nat, k < \result ==> arr[k] !== target)70 let i = 0;71 while (i < arr.length) {72 //@ invariant 0 <= i && i <= arr.length73 //@ invariant forall(k: nat, k < i ==> arr[k] !== target)74 //@ decreases arr.length - i75 if (arr[i] === target) return i;76 i = i + 1;77 }78 return -1;79}80```8182```sh83npx --yes --package=lemmascript@0.5.22 -- lsc gen --backend=dafny src/find.ts # generate artefacts84npx --yes --package=lemmascript@0.5.22 -- lsc check --backend=dafny src/find.ts # generate + verify85npx --yes --package=lemmascript@0.5.22 -- lsc regen --backend=dafny src/find.ts # regenerate, 3-way merge86```8788Key pieces:8990- `//@ verify` opts a function in; once any function in a file has it,91 only marked functions are extracted.92- `//@ requires` / `//@ ensures` are the contract; `\result` names the93 return value; `==>` is implication; `forall(k, P)` / `exists(k, P)` quantify.94- `//@ invariant` and `//@ decreases` go at the top of the loop body.95- Pure functions (no loops, no mutation) are callable from other96 functions' specs; `//@ pure` forces the classification.97- Unmodellable calls are handled by `//@ extern` (deterministic98 axiom), `//@ havoc` (nondeterministic value), or file-level `//@ autohavoc`99 (abstract everything unmodellable, soundly).100101The complete annotation surface, the spec expression language, and the semantic102gotcha list live in [`references/annotations.md`](references/annotations.md).103104## The edit loop (Dafny backend)105106`lsc gen` produces two files beside the source: `foo.dfy.gen` (always107regeneratable — never edit) and `foo.dfy` (source of truth — add helper lemmas,108ghost predicates, `assert` nudges here). The diff between them must be109**additions only**; `lsc check` enforces this. After editing the TypeScript, run110`regen` (never delete and `gen` fresh — that discards every proof). When Dafny111complains, the fix belongs either in the `.ts` (tighten `requires`, weaken112`ensures`, add `invariant`/`decreases`) or in the `.dfy` (helper lemma, ghost113predicate, assert).114115The full workflow — backend choice, Lean's four-file scheme, proof debugging116flags, CI wiring, and brownfield strategy — lives in117[`references/workflow.md`](references/workflow.md).118119## Anti-patterns120121- **Reaching for `//@ assume` to silence a failure.** It tells the122 prover to trust the obligation unconditionally; the proof stops meaning123 anything. Restructure, or prove a helper lemma. Its one sanctioned use is124 constraining a deliberately `havoc`ed value.125- **Refactoring production code "for clarity" during brownfield126 verification.** In-place verification is the point; an unchanged diff is the127 evidence the verified code is the shipped code.128- **Editing `.dfy.gen`.** It is regenerated; changes vanish. Edit the129 `.dfy`.130- **Deleting `.dfy` files to "start clean".** Proof work lives there;131 `regen`'s three-way merge preserves it.132- **Verifying a parallel model instead of the real function.** Prefer133 in-place annotation; where a type can't be imported, shadow it with134 `//@ declare-type` so the actual function stays the proof target.135- **Proving what the spec doesn't say.** Write the `//@ contract`136 intent line and check the `ensures` actually expresses it — a theorem about137 the wrong predicate verifies happily.138139## Project integration140141- **State the trust boundary in the README**: which functions are142 proved, and which surrounding layers (UI, I/O, auth, clock) are trusted. Case143 studies do this plainly; follow suit.144- **Track verified files in `LemmaScript-files.txt`** (one file per145 line, optional per-file timeout and flags) so `tools/check.sh` and CI verify146 them; copy hono-lemmascript's GitHub Actions workflow as the template.147- **Start small in brownfield code**: pure helpers, predicates,148 parsers without I/O. Grow towards the invariant-bearing core.149- **Keep fast-check properties alongside proofs.** They run in the150 inner loop in milliseconds, catch spec regressions before a prover run, and151 cover the unverified boundary code.152- **Land toolchain fixes separately.** When the tech preview needs a153 patch (unsupported method, missed narrowing), fix `LemmaScript/tools/src/` in154 its own PR.155156## Hard-won lessons157158- **The spec is the product.** Provers check the written specification, not159 the intended one. Review `ensures` clauses as adversarially as code.160- **`number` is a mathematical integer** in the model (floats become161 `real`; overflow at 2^53 is out of scope). Do not verify float-sensitive162 numerics; do flag integer-encoding overflow risks — a case study's163 injectivity proof surfaced a real ≥1000-votes overflow in an existing164 encoding.165- **Missing invariants live in the TypeScript.** The LLM/prover loop166 can supply tactics and lemmas in the `.dfy`, but a missing `//@ invariant`167 must be added in the source and regenerated.168- **Refinement scales.** For loop-heavy code, prove the method equals169 a pure recursive spec (`result == range_spec(...)`), then prove properties of170 the spec — every property transfers automatically.171- **Verification conditions are cheap to split.**172 `dafny verify --filter-symbol=...` and `--isolate-assertions` turn one opaque173 timeout into named, tractable obligations.174175## References176177- [LemmaScript repository](https://github.com/midspiral/LemmaScript)178 — SPEC.md, DESIGN.md, GETTING_STARTED.md, AGENTS.md, examples.179- [Announcement post](https://midspiral.com/blog/lemmascript-a-verification-toolchain-for-typescript/)180 and [lemmascript.com](https://lemmascript.com/).181- Worked case studies:182 [hono-lemmascript](https://github.com/midspiral/hono-lemmascript)183 (brownfield, CVE-driven),184 [clear-split-lemmascript](https://github.com/midspiral/clear-split-lemmascript)185 (greenfield, dual-backend),186 [collab-todo-lemmascript](https://github.com/midspiral/collab-todo-lemmascript)187 (verified domain model behind a React app).188- [`references/annotations.md`](references/annotations.md) for the189 annotation surface, spec language, and gotchas.190- [`references/workflow.md`](references/workflow.md) for the edit191 loop, backends, CI, and brownfield strategy.192- Sampled-input testing as the complementary adversary lives in193 [`../fast-check/SKILL.md`](../fast-check/SKILL.md).