Lasm:利用 Lean 4 編譯 WebAssembly,支援 Node、Serverless 與 Edge 即時部署
Show HN: Lasm – Write formally verified webapps in Lean for Node/serverless/edge
Lasm 讓 Lean 4 程式直接編譯成 WebAssembly,支援 Node、Serverless、Edge 與瀏覽器即時部署。
- 編譯與部署:一份 Lean 程式碼可產生 Node 伺服器、Lambda、Cloudflare Workers、Bun、Deno、WASI、瀏覽器等多種平臺輸出。
瞭解如何用 Lean 4 直接編譯 WebAssembly,並在 Node、Serverless 與 Edge 即時部署,對開發者皆具參考價值。
Lean 4 for the JavaScript and WebAssembly world. Lasm compiles ordinary Lean 4 programs ahead of time to WebAssembly. One Lean code base can become a Node server, a serverless or edge function, a WASI/Fastly module or a browser app. Because it is Lean, you can prove properties of the code you deploy. The flagship example, Conduit, is the RealWorld app with 117 proved theorems. Lasm ships on npm and brings Lean, Lake and its build tools with it; you only need Node (or Bun or Deno).
npm create @lasm@latest my-app -- --template hono # then: cd my-app && npm install && npm run devAI agents: this README is complete on its own. llms.txt is the short
index, and docs/AI.md lists the rules that save you from wrong turns.
If Lasm is useful to you, I'd appreciate your sponsorship (more below).
Quick facts
| Fact | Value |
|---|---|
| Current version | @lasm/cli 0.3.5 (release notes); npm view @lasm/cli version |
| Install | npm install -D @lasm/cli (bin: lasm), or no install: npx @lasm/cli …; new project: npm create @lasm@latest |
| Lean | exactly 4.35.0 (lean-toolchain: leanprover/lean4:v4.35.0); Lasm installs it, and you need no elan to build |
| Develop on | Linux x64/arm64 (glibc 2.34+), macOS x64/arm64, Windows x64 |
| Compiler runs on | Node ≥ 24 (tested 24.0.0, 24.22.1, 26.12.1); Bun 1.4.4 and Deno 2.9.6 (tested, as the only runtime) |
| Output | dist/ with program.wasm + a JS launcher (Node); or a platform package (--target …) |
| Node apps run on | Node ≥ 24, Linux x64/arm64 (glibc 2.28+ or musl) from any build machine, plus the build machine's own OS; ≥ 512 MB memory |
| JS/npm interop | import Lasm.JS; one @[js "pkg" "export"] line per function you call; no premade bindings (guide) |
| Frameworks in Lean | Lasm.Fetch (HTTP, every server target), Lasm.UI (browser, Elm architecture, proved diff), Lasm.RPC (typed client/server API), Lasm.Web (DOM and web APIs), Lasm.KV |
| Not available at run time | elaboration, #eval-style evaluation, loading .olean, Lean's kernel (exact list); all of these work at build time |
| License | MIT (Lasm); templates, examples and README code samples also MIT-0 |
| Repository / issues | https://github.com/Millillion/lasm · report a bug (include npx lasm --version) |
Platforms
Every row is tested in CI with the platform's own local runtime, image or emulator. By project policy CI never deploys to a real cloud account (how each is tested).
| Deploy to | lasm build Main.lean --target … |
Output | Since |
|---|---|---|---|
| Node server or program (any host, Docker/Kubernetes, Cloud Run, Fly.io, Railway, Render, Heroku, DigitalOcean) | node (default) |
dist/ → node dist/main.mjs |
0.1.0 |
AWS Lambda (.zip, nodejs24.x) |
lambda |
lambda.zip |
0.1.0 |
| AWS Lambda (container image) | lasm init … --deploy lambda |
Dockerfile.lambda |
0.1.0 |
| Lambda@Edge | lambda-edge |
lambda-edge.zip |
0.1.0 |
| Vercel Functions | vercel |
.vercel/output |
0.1.0 |
| Netlify Functions | netlify |
netlify/functions/lasm |
0.1.0 |
| Azure Functions | azure |
azure-functions/ |
0.1.0 |
| Google Cloud Functions (gen2) | gcf |
gcf/ |
0.1.0 |
| Cloudflare Workers | workers |
workers/ |
0.2.0 |
| Vercel Edge | vercel-edge |
api/lasm.mjs, api/_lasm/ |
0.2.0 |
| Netlify Edge | netlify-edge |
netlify/edge-functions/lasm.mjs |
0.2.0 |
| Deno Deploy | deno-deploy |
deno/main.mjs |
0.2.0 |
| Supabase Edge Functions | supabase-edge |
supabase/functions/<name>/ |
0.2.0 |
| Bun (≥ 1.3.14) / Deno (2.x) | bun / deno |
bun/main.mjs / deno/main.mjs |
0.2.0 |
| Fastly Compute | fastly |
bin/main.wasm, pkg/<name>.tar.gz |
0.3.0 |
WASI (wasmtime, wasmer, node:wasi) |
wasi |
wasi/main.wasm |
0.3.0 |
wasi-http (wasmtime serve, Spin) |
wasi-http |
wasi-http/main.wasm |
0.3.0 |
| Browsers (Chrome, Edge, Firefox, Safari) | browser (or the Vite plugin @lasm/cli/vite) |
dist/ static site |
0.3.0 |
Node builds have Lean's full runtime (threads, files, processes, sockets). The edge, Bun/Deno, WASI and browser builds run Lean single-threaded, without sockets or processes (and without files, except on WASI); the rest of Lean works.
Start here
| I want to build… | Run | Then look at |
|---|---|---|
| A command-line program | npm create @lasm@latest my-tool |
examples/hello, examples/wasi-cli |
| A REST API on Node with an npm framework | npm create @lasm@latest my-api -- --template hono (or express, fastify) |
examples/js-crud (Postgres + JWT, Express and Hono) |
| A server in pure Lean (no npm) | npm create @lasm@latest my-api -- --template fetch (any target) or --template std-http (Node, also native Lean) |
examples/fetch; the std-http template |
| A serverless function (Lambda, Vercel, Netlify, Azure, GCF) | npm create @lasm@latest my-fn -- --template hono --deploy lambda-zip (or vercel, netlify, azure, gcf, lambda-edge) |
docs/deploy |
| An edge function (Workers, Vercel/Netlify Edge, Deno Deploy, Supabase) | npm create @lasm@latest my-edge -- --template hono --deploy workers (or fetch) |
examples/edge-pure, docs/EDGE.md |
| A Fastly, WASI or Spin service | npm create @lasm@latest my-svc -- --template fetch --deploy fastly (or wasi-http, spin, wasi) |
examples/fetch, docs/WASI.md |
| A browser app | npm create @lasm@latest my-web -- --template web |
examples/ui (counter, TodoMVC with proofs, feature demo, benchmark) |
| A complete real-world app (API, browser frontend, SQL, auth) with proofs, on Node, Workers or Bun | cd examples/conduit && npm install && npm run build && npm start |
examples/conduit, the flagship (below) |
| A full-stack app with a verified API | npm create @lasm@latest my-app -- --template fullstack |
templates/fullstack |
To call npm packages or fetch from Lean |
add import Lasm.JS and one @[js] line per function |
examples/js-fetch, JS bindings |
| To run on Bun or Deno | bun create @lasm my-app --template hono / deno run -A npm:@lasm/create my-app --template hono |
docs/BUN_DENO.md |
Every example has a README with a one-command run; examples/README.md lists them all.
Flagship example: Conduit, with 117 proved theorems
examples/conduit is Conduit, the RealWorld app (a Medium clone), written entirely in Lean: the REST API, the server-rendered browser app and the proofs.
- 117 theorems about the code that actually runs, checked with no
sorryand only Lean's standard axioms (npm run proofs; PROOFS.md lists each one in plain English). They cover authorization (only authors change their articles and comments), follows and favorites, slugs that never collide, pagination that never skips or repeats, validation shared by client and server (the app never sends what the server would reject), JSON round trips and Markdown link safety. The endpoints are written against aStoreof functions, and the proofs run that same code on a model of the tables. - What is trusted, not proved: that each SQL statement does what its model says, the JSON text layer, concurrency between requests, and Lean, Lasm and the JavaScript runtime. Conformance to the whole RealWorld spec is tested, not proved.
- API: the official RealWorld API suite passes unmodified (13 of 13 files,
154 requests) on Node (
node:sqlite), Cloudflare Workers (D1) and Bun (bun:sqlite). Each target has one hand-written@[js]binding module. - Frontend:
Lasm.UIon RealWorld's official templates, with server-side rendering, hydration and Markdown parsed in Lean. The official Playwright end-to-end suite passes with no retries on Node with Chromium, Firefox and WebKit, and on Workers and Bun with Chromium.
All of this runs in CI (conduit.yml, run 40615274657,
with @lasm/cli 0.3.4).
cd examples/conduit npm install # @lasm/cli, Hurl and Playwright (the official suites' runners) npm run build && npm start # http://localhost:3000: the app and its API on Node npm run proofs # check all 117 theorems npm test # the official API suite (also test:workers, test:bun) npx playwright install chromium && npm run test:e2e # the official end-to-end suite npm run build:workers && npx wrangler dev # Cloudflare Workers with local D1 npm run build:bun && npm run start:bun # Bun with bun:sqlite
60-second start
A single file needs no project. Save this complete program as Main.lean:
def main : IO Unit := do IO.println s!"Hello from Lean! 2 + 3 = {2 + 3}"
npx @lasm/cli Main.lean # builds and runs: Hello from Lean! 2 + 3 = 5 npx @lasm/cli build Main.lean # writes dist/; run it with: node dist/main.mjs
The first build on a new machine fetches Lean and the build tools once (about 250–275 MB; 19 s for that first build on the Linux CI runner). Later dev builds of a one-line change take about 1–2 s.
Recipes
Each recipe is complete: create, develop, build, deploy. Lasm never installs or
starts a platform's own CLI (wrangler, vercel, fastly, …); you run those.
npm run dev is lasm dev: it rebuilds about 2 s after each save and restarts
the server, and keeps the last good build running when there is a type error.
Node server (Hono; also express, fastify, std-http, fetch).
npm create @lasm@latest my-api -- --template hono cd my-api && npm install npm run dev # http://localhost:3000/hello/Ada npm run build && npm start # dist/ needs only Node 24+ docker build -t my-api . && docker run -p 8080:8080 my-api # the generated Dockerfile
AWS Lambda / Vercel / Netlify / Azure / Google Cloud Functions.
npm create @lasm@latest my-fn -- --template hono --deploy lambda-zip # or vercel, netlify, azure, gcf, lambda-edge cd my-fn && npm install npm run build:lambda # dist/ and lambda.zip (handler lasm-lambda.handler) # deploy lambda.zip with the AWS console, aws CLI or SAM; recipes: docs/deploy/lambda.md
Cloudflare Workers (and Vercel Edge, Netlify Edge, Deno Deploy, Supabase).
npm create @lasm@latest my-edge -- --template hono --deploy workers cd my-edge && npm install npm run build:workers # workers/ (wrangler.toml came from lasm init) npx wrangler dev # local workerd; npx wrangler deploy to publish
Fastly Compute, WASI, Spin.
npm create @lasm@latest my-svc -- --template fetch --deploy fastly --deploy spin cd my-svc && npm install npm run build:fastly && fastly compute serve # Viceroy on http://127.0.0.1:7676 npm run build:wasi-http && spin up # or: wasmtime serve wasi-http/main.wasm
Browser app.
npm create @lasm@latest my-web -- --template web cd my-web && npm install npm run dev # http://127.0.0.1:5173, live reload npm run build # dist/: a static site for any host or CDN
Full stack (server + browser + verified API).
npm create @lasm@latest my-app -- --template fullstack cd my-app && npm install npm run dev # one URL: http://127.0.0.1:3000 npm run build && npm start # Node; also build:workers, build:bun, build:deno
Editor. Run npx lasm editor once per project; lasm init, lasm dev and
lasm build also do it. The Lean editor (VS Code's Lean 4 extension) then resolves
import Lasm.* (details).
The whole stack in Lean, verified
The fullstack template is one Lean project with a server (Lasm.Fetch), a
browser app (Lasm.UI) and the API between them (Lasm.RPC). The rule a request
must satisfy is written and proved once, in Shared.lean, which both sides import
(templates/fullstack):
-- Shared.lean @[irreducible] def checkTitle (title : String) : Except String Unit := let n := (clean title).length if n = 0 then .error "Write something first." else if n > maxTitle then .error s!"Keep it under {maxTitle} characters." else .ok () /-- Proved once, holds in the browser and on the server. -/ theorem checkTitle_ok {title : String} (h : checkTitle title = .ok ()) : 0 < (clean title).length ∧ (clean title).length ≤ maxTitle := by unfold checkTitle at h by_cases h0 : (clean title).length = 0 · simp [h0] at h · by_cases h1 : (clean title).length > maxTitle · simp [h0, h1] at h · omega def addTodo : Endpoint NewTodo Todo := { name := "addTodo", validate := fun r => checkTitle r.title }
-- Server.lean: storing a todo demands the proof that its title passed the check, -- and `addTodo.serve` hands the handler that proof (`ok`) after re-validating. def Store.add (s : Store) (title : String) (_ : checkTitle title = .ok ()) : Store × Todo := let t := { id := s.next, title := clean title, done := false } ({ todos := s.todos.push t, next := s.next + 1 }, t) addTodo.serve fun req ok => do let (s, t) := (← load kv).add req.title ok save kv s return .ok t
-- Client.lean: a theorem about the browser app: it never sends an invalid title. theorem add_invalid (m : Model) (e : String) (h : checkTitle m.draft = .error e) : (update .add m).2.effects = [.msg (.added (.error (.invalid e)))] := addTodo.call_invalid { title := m.draft } Msg.added "" e h
Where proofs live and how they are checked.
| Proof | Where | Checked by |
|---|---|---|
Your theorems (like checkTitle_ok, add_invalid) |
your .lean files |
every lasm build/lasm dev (Lean elaborates them; a broken proof is a build error); pure modules also with lake build |
Lasm.RPC: the handler only runs on requests that pass validate (Endpoint.decodeRequest_valid); the client never sends one that fails it (Endpoint.call_invalid) |
library/Lasm/RPC | compiled into Lasm's library on every build |
Lasm.UI: the DOM diff is correct against a DOM model, keyed moves included (diff_correct, mount_correct, session_correct) |
library/Lasm/UI/Proofs (about 1,260 lines, no sorry, standard axioms only) |
node scripts/ui/lean-tests.mjs in a checkout (CI: ui.yml) |
| Conduit: 117 theorems about authorization, follows, favorites, slugs, pagination, shared validation, JSON round trips and Markdown link safety | examples/conduit/Proofs, listed in PROOFS.md | npm run proofs in examples/conduit (no sorry, standard axioms only; CI conduit.yml) |
| TodoMVC: every reachable model is valid | examples/ui/todomvc/Todo.lean | lasm build; CI ui.yml |
Not proved, tested in CI instead: the JSON/HTTP transport between client and server, the JavaScript that applies patches to the real DOM, the Lasm compiler and runtime, and the browsers. Lean's own compiler is trusted, as in any Lean program (UI.md, RPC.md).
Calling JavaScript and npm from Lean
Declare each JavaScript function you call with one line; the Lean type says how
values convert. A complete program (Node's global fetch):
import Lasm.JS open Lasm.JS /-- `await fetch(url, init)` (Node's global fetch) -/ @[js "globalThis" "fetch"] opaque fetch : String → Data → IO Value def main : IO Unit := do let response ← fetch "https://example.com" (obj []) IO.println s!"{← response.get "status" Nat}: {(← response.callMethodAwait "text" #[] String).length} characters"
The rules for writing bindings:
@[js "module" "export"] opaque name : A → B → IO Rbindsmodule.export. The module is an npm package ("marked"), a Node built-in ("node:fs"), or"globalThis"for globals."new Hono"calls a constructor, and""is the module's own value.- End the type in
IO Rfor a value returned at once and inAsync Rfor a promise. AnAsyncbinding is awaited without holding a Lean thread. - Use
String,Nat,Int,Float,Bool,ByteArray,Array,List,Option, tuples,Data(any JS data) orValue(a JS object reference:get,callMethod,callMethodAwait). Your own structures convert withderiving ToJS, FromJS. Function.ofFn/Function.ofFnReplyturn Lean functions into JS callbacks. Framework handlers use them (see thehono,expressandfastifytemplates).npm installthe package next to the lakefile.lasm buildchecks every binding against the installed package (Lasm.JS binding check failed: … export "x" is missing) and copies production dependencies intodist/node_modules.- Each call crosses from a Lean thread to Node's main thread (about 24 µs), so prefer one coarse call over many small ones.
Full guide, with Express, Hono, pg, jose and fetch examples and an agent
checklist: docs/JS_BINDINGS.md.
Measured numbers
All numbers are from CI on a free GitHub runner (4-CPU AMD EPYC, Linux x64), candidate 0.1.0-experimental.40494203854 (2026-10-27) unless another run is named. Full tables, method and caveats: BENCHMARKS.md.
| What | Number |
|---|---|
| Node: Hono + Lean handler, cold start to first HTTP response | 159 ms (plain Node http: 4.1× faster) |
| Node: Hono + Lean handler, throughput / p99 latency | 24,911 req/s / 4.2 ms (plain Node http: 103,134 req/s, so 0.24×) |
| Node: Express + Lean handler, throughput | 18,678 req/s |
Node: Hono app program.wasm (brotli) / dist/ |
166 KiB / 8.5 MiB plus 1.4 MiB node_modules |
| Node: memory, Hono app idle / under load | 148 MiB / 329 MiB RSS |
| Lean → JS call round trip (p50) | 24 µs |
| Cloudflare Workers: Hono app bundle | 1.40 MiB, 245 KiB gzip (limit 64 MiB; workers run 40128600109) |
| Browser: counter app total transfer / first frame (Chromium) | 152 KiB brotli / 30 ms (browser run 40288509500) |
| Browser: js-framework-benchmark vs hand-written JS | within 1.7× on every operation, most within 1.2× (browser run 40302274897) |
WASI: Lasm.Fetch module / per request on Fastly's Viceroy |
0.90 MiB / 1.17 ms with a fresh instance per request (wasi run 40293231963) |
| Build: clean Hono build / dev rebuild after a one-line change | 4.7 s / 2.0 s |
npm install @lasm/cli: download / installed; tool cache after the first build |
71 MiB / 403 MiB; 2.5 GB |
Lean on Node pays for WebAssembly and its threads. A cold start is CPU-bound, so it
grows on small serverless CPU shares, and every JavaScript call crosses threads.
Pure-Lean Std.Http on Node is slow (1,290 req/s), so use Hono, Express, Fastify
or Lasm.Fetch for production HTTP. Arithmetic on numbers with thousands of digits
is slower than native Lean (no GMP).
Known limitations
- Ahead of time only. A deployed program cannot elaborate, evaluate or compile
Lean, load module data or plugins, or use Lean's C++ compiler/kernel at run time
(exact list). Proofs, tactics, macros and
#evalrun while compiling, as usual. - Lean 4.35.0 exactly. Other
lean-toolchainversions are rejected with a message. - Node: give an app at least 512 MB; it reserves about 22 GB of virtual address
space. Prebuilt native libraries (
-l, shared libraries) cannot be linked; a package's bundled C source is compiled for you. - Edge, Bun/Deno, browser, WASI: single-threaded Lean; no sockets, processes or
signals; no files except on WASI. In the browser, blocking waits stop the app
unless it is built with
--suspend jspifor a browser with JSPI; useAsync. - No premade bindings for npm packages;
Lasm.Webcovers the core web platform. - Editor:
Lasm.*imports resolve in the editor afternpx lasm editor(above). - Platforms: no Windows ARM64 and no Alpine/musl build machines (built apps run on Alpine). Hosted platforms are tested with their local tools, never with a real account.
Documentation
- Start: Getting started · All documentation · For AI agents · llms.txt
- Guides: servers and npm interop · serverless and containers · edge · Bun and Deno · browser and Lasm.UI · full stack (Lasm.RPC) · Lasm.Fetch · WASI and Fastly · the edit loop
- Evidence: benchmarks · where apps run · language coverage · compatible Lean packages · decisions
Support Lasm
If Lasm is useful to you or you want to see it succeed, I'd appreciate your sponsorship: github.com/sponsors/lastmjs. It supports me and the work on Lasm and the other Millillion projects. My first goal is $250 a month, enough for one dedicated Claude Code account for Lasm. You can also help by starring the repo, trying it, and reporting what breaks.
Reference
Everything below is the precise contract: every command and option, supported machines and platforms, and exactly what a deployed Lean program cannot do.
CLI reference
Install in a project with npm install -D @lasm/cli (the bin is lasm), or run
without installing: npx @lasm/cli Main.lean. npm create @lasm runs lasm init
of the same @lasm/cli version with the arguments unchanged. With the Main.lean
from the quick start:
npx lasm Main.lean # build (dev profile) and run: Hello from Lean! 2 + 3 = 5 npx lasm build Main.lean # write dist/ (release profile) npx lasm --version # version, Lean, runtime and platform (for bug reports) node dist/main.mjs # run the deployment: only Node is needed npx lasm build Main.lean --target lambda # dist/ and an AWS Lambda zip
| Command or option | Example | Effect |
|---|---|---|
| Run (default) | npx lasm Main.lean |
Build (dev profile) and run Lean main. npm run dev in a plain project runs this. |
| Explicit run | npx lasm run Main.lean |
Same as the default command. |
| Run a release build | npx lasm Main.lean --release |
Build and run exactly what lasm build deploys. |
| Program arguments | npx lasm Main.lean -- hello "two words" |
Pass arguments to main (args : List String). |
| Watch, rebuild, restart (0.3.0) | npx lasm dev, npx lasm dev Main.lean --target workers -- arg |
Watch the Lean sources and the Lake/npm configuration, rebuild after each change (dev profile, one long-lived build worker) and restart the program (--target node, bun, deno; default: the runtime running Lasm). A build error is printed with its file, line and source line; the last good program keeps running. Edge targets only rebuild their output directory, which the platform's own local server serves (Lasm never starts platform tools or browsers). --target browser serves the app with live reload (--port, --host, --no-watch); several outputs build together: --target node=Server.lean,browser=Client.lean. --release builds the release profile, --no-run only rebuilds, rs + Enter rebuilds and restarts. Details. |
| Build | npx lasm build Main.lean |
Write dist/ (release profile); do not run the application. |
| Dev build | npx lasm build Main.lean --dev |
Write dist/ faster, without the size optimizations. |
| Output directory | npx lasm build Main.lean --output "app dist" |
Choose the deployment directory. lasm build Main.lean "app dist" is an alias. |
| Function platforms | npx lasm build Main.lean --target lambda |
Build dist/, then package it: node (default; dist/ only), lambda (lambda.zip), lambda-edge (lambda-edge.zip), vercel (.vercel/output), netlify (netlify/functions/lasm), azure (azure-functions/), gcf (gcf/). --target=vercel also works. Lasm never installs or starts the platform's CLI. Recipes. |
| Edge isolates (0.2.0) | npx lasm build Main.lean --target workers |
Relink single-threaded with a pure-JS host: workers (workers/), vercel-edge (api/lasm.mjs, api/_lasm/), netlify-edge (netlify/edge-functions/lasm.mjs, netlify/lasm/), deno-deploy (deno/main.mjs), supabase-edge (supabase/functions/<name>/; Asyncify, since Supabase's runtime has no JSPI). The edge runtime ships inside @lasm/cli. Guides: Workers, Vercel Edge, Netlify Edge, Deno Deploy, Supabase; design and limits. |
| Bun and Deno (0.2.0) | bunx --bun @lasm/cli Main.lean, deno run -A npm:@lasm/cli Main.lean, bun create @lasm my-app --template hono |
The compiler runs with Bun or Deno as its only JavaScript runtime and produces the same bytes. Under Bun or Deno the default target is that runtime: --target bun writes bun/main.mjs, --target deno writes deno/main.mjs (portable profile). Deno skips npm versions younger than 24 hours: right after a release, pass --minimum-dependency-age=0. Bun and Deno. |
| Browser (0.3.0) | npx lasm build Main.lean --target browser |
Write dist/ as a static site: app.<hash>.js (the loader: mount(element, { flags, hydrate }), auto-mount of <div data-lasm-app>), app.<hash>.wasm, brotli and gzip copies, app.js, index.html (yours with the loader injected, or a generated page), your public/ files and lasm-manifest.json. --suspend jspi lets blocking waits suspend where the browser has JSPI. --bundler leaves npm imports for a bundler; in Vite: import lasm from '@lasm/cli/vite', then import app from './Main.lean?lasm'. Web APIs from Lean: import Lasm.Web. Guides: BROWSER.md, UI.md, deploying. |
| WASI, wasi-http, Fastly (0.3.0) | npx lasm build Main.lean --target wasi (or wasi-http, fastly) |
No JavaScript. wasi writes wasi/main.wasm (wasi_snapshot_preview1: wasmtime run --dir . wasi/main.wasm, wasmer, node:wasi); wasi-http writes wasi-http/main.wasm, a wasi:http/proxy@0.2 component (wasmtime serve, Spin); fastly writes bin/main.wasm and pkg/<name>.tar.gz (fastly compute serve/deploy). No sockets, processes or signals. WASI.md, Fastly. |
| HTTP in pure Lean (0.3.0) | import Lasm.Fetch … def main := Lasm.Fetch.serve handle |
One Request → Async Response handler serves Node ($HOST:$PORT), the edge targets, Bun, Deno, Fastly, wasi-http and WASI as CGI; Lasm.Fetch.fetch sends requests; Response.streaming streams. FETCH.md. |
| Force rebuilding | npx lasm build Main.lean --rebuild |
Ignore a cached application build. |
| Detailed diagnostics | npx lasm Main.lean --verbose |
Show compiler and linker details. |
| Help | npx lasm --help, npx lasm init --help |
Show usage; -h is an alias. |
| Version | npx lasm --version |
Print lasm 0.3.5 (@lasm/cli; Lean 4.35.0; Node 26.12.1; linux-x64); -v is an alias. |
| New project | npm create @lasm@latest my-app -- --template hono or npx @lasm/cli init my-app --template hono |
Create a Lake project in a new or empty directory (Main.lean, App.lean, lakefile, lean-toolchain, package.json, README.md, .gitignore, and for servers a Dockerfile and .dockerignore). Templates: see Start here. --deploy (repeatable) adds a platform's files and npm run build:<target>: lambda (Dockerfile.lambda, a container image), lambda-zip, lambda-edge, vercel, netlify, azure, gcf; workers, vercel-edge, netlify-edge, deno-deploy (templates hono, fetch, fullstack), supabase-edge (hono); fastly, wasi-http, spin (fetch, fullstack), wasi (fetch, plain). The fetch and fullstack templates and their targets are 0.3.0. Writes files only; run npm install next. -t is an alias of --template; --template=hono also works. |
| Package an existing build | npx lasm package --target lambda |
Package a built server (default dist/) for a function platform without rebuilding: the same targets and outputs as lasm build --target, except node. --output PATH chooses the output. Checks the platform's size limit. Refuses a dev build unless --allow-dev. For a browser build, npx lasm package dist --target browser checks every file against lasm-manifest.json and writes _headers. |
| Editor support (0.3.0) | npx lasm editor |
Make the Lean editor (VS Code's Lean 4 extension, any Lean language server client) resolve import Lasm.* in this Lake project: links Lasm's Lean 4.35.0 with its library as the elan toolchain lasm-4.35.0-… and sets it as the directory's elan override (elan override unset undoes it). lasm init (when Lasm's Lean is already installed), lasm dev and lasm build do the same, printing one line per project, except on CI or with LASM_EDITOR=0. Editor setup. |
| Cache repair | npx lasm cache repair |
Offline verification and removal of invalid tools and abandoned staging. Refuses an active build. |
--rebuild and --verbose work with run and build. --output and --target are
build-only; arguments after -- are run-only. The application's exit status is
preserved. npx lasm --version (-v) prints the version, Lean release, JavaScript
runtime and platform (include it in bug reports). There is no install, clean or
uninstall subcommand.
Build profiles. Both link the same program with the same libraries and are
deterministic. release (lasm build, lasm Main.lean --release): the full
Binaryen size optimization; byte-reproducible; the one to deploy. dev
(lasm Main.lean, lasm dev, lasm build --dev): leaves those passes out and
compiles your modules at -O1; it behaves the same, builds several seconds faster,
and its program.wasm is about 15–20 % larger. A dev rebuild starts from the
project's previous build, so its bytes can depend on that build. build-info.json
records "profile". lasm package and lasm build --dev --target <platform>
refuse a dev build unless you pass --allow-dev.
Environment variables (all optional):
| Variable | Example | Effect |
|---|---|---|
LASM_TOOLCHAIN_CACHE |
LASM_TOOLCHAIN_CACHE=/data/lasm-cache npx lasm Main.lean |
Move the managed tool cache (default locations). |
LASM_BUILD_JOBS |
LASM_BUILD_JOBS=2 npx lasm build Main.lean |
Threads for Binaryen's optimizer (default: the CPUs, at most 8). The output is the same for any value. |
LASM_EDITOR |
LASM_EDITOR=0 npx lasm build Main.lean |
0: lasm init, lasm dev and lasm build leave the editor setup alone (editor setup). |
LASM_THREAD_POOL (built apps; 0.3.0) |
LASM_THREAD_POOL=0 node dist/main.mjs |
Lean thread Workers started before main (0–64, default 1). Measurements. |
LASM_ARTIFACT_SOURCE |
LASM_ARTIFACT_SOURCE=npm npx lasm Main.lean |
auto (default): @lasm/… npm tool packages when present, else HTTPS; npm: never download directly; https: ignore the npm packages. |
LASM_DOWNLOAD_ATTEMPTS, LASM_DOWNLOAD_CONNECT_MS, LASM_DOWNLOAD_IDLE_MS, LASM_DOWNLOAD_TOTAL_MS |
LASM_DOWNLOAD_TOTAL_MS=14400000 npx lasm Main.lean |
Download retries and timeouts (defaults and ranges). |
NODE_USE_ENV_PROXY, HTTPS_PROXY, NO_PROXY, NODE_EXTRA_CA_CERTS (Node's own) |
NODE_USE_ENV_PROXY=1 HTTPS_PROXY=http://proxy:8080 npx lasm Main.lean |
Proxies and private CAs (corporate networks). |
Other LASM_* variables in the source are maintainer diagnostics, not a supported
interface.
Installation. npm install @lasm/cli brings Lean and the build tools as
exact-pinned @lasm/… npm packages for your platform through your own registry,
proxy and cache. The Lake/Lean-import data and, only for Lake projects with C
code, the C tier (@lasm/lean-c-<platform>) are fetched with your npm on first use.
The terminal shows progress; later builds reuse verified tools and unchanged
outputs. On a new machine, without the download, a first Hono build took 25.7 s
(Linux x64), 27.8 s (Linux arm64), 44.4 s (Windows), 58.7 s (Apple Silicon) and
141.9 s (Intel Mac) on the CI runners
(build time, run 40131930601).
Cache locations and recovery: NODE_SUPPORT.md,
INSTALLATION_HARDENING.md.
Git is needed only for Lake projects with Git or Reservoir dependencies; Lasm
uses the git on your PATH and says how to install it when it is missing.
Existing Lake projects build with npx lasm build Main.lean from the project
directory. Pin leanprover/lean4:v4.35.0 in lean-toolchain; other versions are
rejected with Unsupported Lean toolchain … Lasm supports Lean v4.35.0. A
per-package compatibility list covers Batteries, Cli,
LSpec, Plausible, Aesop, Qq, leansqlite, UnicodeBasic, MD4Lean, lean-zip,
lean4-parser, lean-redis and others; packages not on it are untested.
Supported environments
Machines that build with Lasm (the compiler):
| Environment | Architectures | Status |
|---|---|---|
| Ubuntu 24.04 LTS | x86-64, ARM64 | Installed-package and copied-deployment acceptance in CI; the reference platform. |
| Other Linux distributions | x86-64, ARM64 | Unverified as build machines; the tools need glibc 2.34. Alpine/musl is not a build machine (built apps run there). |
| macOS 15 | x86-64, ARM64 | Installed-package and copied-deployment acceptance in CI. Lake projects with C code need the Xcode Command Line Tools SDK. |
| Windows Server 2025 / Windows x64 | x86-64 | Installed-package and copied-deployment acceptance in CI. |
| Windows ARM64 | ARM64 | Not supported: neither Lean nor Emscripten publishes Windows ARM64 prebuilts. |
| Bun 1.4.4 and Deno 2.9.6 as the compiler's runtime (0.2.0) | all of the above | Tested in CI on all five platforms, also with no Node or npm on PATH (BUN_DENO.md). |
Node 24 or newer (tested: 24.0.0, 24.22.1, 26.12.1). No engine flags. Exact tested artifacts: NODE_ACCEPTANCE.md.
Supported platforms
A Node dist/ needs Node 24 or newer (Node 22 and older stop with This application requires Node 24.0.0 or newer) and at least 512 MB of memory. It
runs on Linux x64 and arm64 (glibc 2.28+, or musl/Alpine) from a build on any
supported platform, and on the build machine's own platform. Copy all of dist/; no
Lean source, npm install or build tools are needed there. Each platform below is
tested in CI with the local runtime, image or emulator named; none with a real
cloud account. Full matrix: DEPLOYMENT.md.
| Platform | Tested in CI with |
|---|---|
| Linux x64 and arm64 (glibc 2.28+), Node 24+ | Node 24.0.0, 24.22.1 and 26.12.1 on ubuntu-24.04 and ubuntu-24.04-arm |
| macOS (x64, arm64) | Node 24.0.0 and 24.22.1 on macos-15-intel; Node 24.22.1 on macos-15; installed acceptance with Node 26.12.1 |
| Windows x64 | Node 24.0.0 and 24.22.1 on windows-2025; installed acceptance with Node 26.12.1 |
| Docker / Kubernetes | node:24-bookworm-slim, node:26-bookworm-slim, node:24-bullseye-slim, Ubuntu 22.04/24.04; read-only, 512 MB, x64 and arm64 (Kubernetes manifests are not applied in CI) |
| Google Cloud Run, Fly.io, Railway, Render, Heroku | the same container images and Dockerfiles; the platforms themselves are never deployed to |
| AWS Lambda (container image) | public.ecr.aws/lambda/nodejs:24 + AWS Lambda Web Adapter through the Runtime Interface Emulator, x86_64 and arm64; amazonlinux:2023 |
AWS Lambda (.zip, nodejs24.x) |
every server template's --target lambda zip in public.ecr.aws/lambda/nodejs:24 through the Runtime Interface Emulator, x86_64 and arm64, with API Gateway REST/HTTP, function URL and ALB events; sam local invoke and sam local start-api |
| Lambda@Edge (origin request, x86_64) | the same image and emulator with CloudFront origin-request events |
| Vercel Functions (Node.js 24) | vercel build offline; the function under @vercel/node in public.ecr.aws/lambda/nodejs:24 |
| Netlify Functions (v2) | netlify build --offline and netlify dev --offline; the deploy zip in public.ecr.aws/lambda/nodejs:24 |
| Azure Functions (Node.js v4 model, Linux) | Azure Functions Core Tools func start |
Google Cloud Functions (gen2, nodejs24) |
Functions Framework on Ubuntu 24.04; Google Cloud's buildpacks image |
| Alpine/musl containers | node:24-alpine, node:26-alpine, x64 and arm64, read-only, 512 MB |
| DigitalOcean App Platform (gVisor) | the container images under gVisor's runsc, x64 |
| Cloudflare Workers (0.2.0) | wrangler dev 4.152.0 (workerd), with the Free plan's budgets gated |
| Vercel Edge (0.2.0) | vercel build offline and Vercel's edge-runtime |
| Netlify Edge (0.2.0) | netlify build --offline and netlify dev --offline |
| Deno Deploy (0.2.0) | Deno 2.9.6 running deno/main.mjs |
| Supabase Edge Functions (0.2.0) | Supabase's edge-runtime v1.77.8 image in Docker |
| Bun and Deno programs (0.2.0) | Bun 1.4.4 and Deno 2.9.6 on all five platforms |
| WASI, wasi-http, Spin, Fastly (0.3.0) | wasmtime 49.0.4 (run, serve), wasmer 7.4.2, Node's node:wasi, Spin 4.2.2, Viceroy 0.21.1 (wasi.yml) |
| Browsers (0.3.0) | Playwright 1.63.0: Chromium 153 and Firefox 155 on ubuntu-24.04; WebKit 26.6 on ubuntu-24.04 and macos-15 (browser.yml) |
A dist/ built on Windows and copied to Linux other than through docker build
needs chmod 755 dist/host/native/process/linux-*. Node's --jitless cannot work
(it disables WebAssembly). Hosts that cap virtual memory (ulimit -v,
vm.overcommit_memory=2) refuse the Node build, which reserves about 22 GB of
address space. Node 26's official binary needs libatomic1 on minimal images that
install the tarball themselves.
Lean support and restrictions
Builds enforce ahead-of-time execution. Ordinary functions, recursion,
closures, inductive types, pattern matching, type classes, dependent types and
compiled IO are supported. Imports, macros, elaboration, tactics, deriving, proofs
and #eval run during compilation in managed native Lean. Initializers count
as runtime dependencies: a broad import Lean can retain the compiler; use
meta import Lean when only macros or tactics need it, and narrower imports for
runtime data libraries (import Lean with Lean.Json, and deriving ToJson, FromJson, build and run). Each rejection is explained: Lasm prints the chain from
your code to the compiler code and, when a broad import is the cause, the narrower
modules your code uses. Deployed programs honor LEAN_ABORT_ON_NONLINEAR=1 as
native Lean does. Per-platform results: language coverage.
The deployed application cannot use:
| Unavailable at runtime | Examples and exact boundary |
|---|---|
| Load Lean module data or locate a compiler installation | Lean.importModules, Lean.withImportModules, module-data readers/writers, Lean.findSysroot, Lean.initSearchPath; deployed .olean/IR loading. |
| Evaluate or compile new Lean code | Lean.Environment.evalConst, interpreter evaluation, a Lean REPL, runtime elaboration/compilation, evaluation of declarations loaded by name. Calling an ordinary compiled function is allowed. |
| Use Lean's compiler/kernel runtime state | Runtime elaboration, kernel proof checking, compiler environments, language-server machinery needing Lean's C++ compiler/kernel. Pure Lean data operations are allowed if their reachable dependencies pass the check. LeanExport.parseStream builds Lean.Expr values through the kernel and is rejected. |
| Load executable plugins dynamically | Lean/Wasm plugin loading, dlopen/dlsym (C code calling dlopen gets an implementation that always fails), runtime Lean symbol lookup. |
The build checks reachable runtime dependencies, including imported libraries and indirect calls; a reachable forbidden call is rejected even if your input would not take that branch, and dead code removed by the linker does not count. The launcher also rejects missing, stale or incompatible capability records before loading the application (a consistency check, not a security sandbox). There is no large compatibility fallback.
Lake packages. Modules imported only for compilation (tactics, elaborators,
for example Aesop, Qq or import Lean for macros) do not keep Lean's compiler in
the deployment. Lasm runs a module's initializers only when the program uses
something they define or register, or when they are the program's own code doing
IO or calling C (--verbose shows how many were skipped). C source that a package
bundles (extern_lib, moreLinkObjs, for example leansqlite's SQLite) is compiled
with Lean's own clang, fetched once as the C tier (39–52 MB); no system C compiler
is needed (macOS needs the Xcode Command Line Tools SDK for C library headers).
Prebuilt native libraries (shared libraries, -l options: libcurl, OpenSSL,
libpq, BLAS) cannot be linked; the build names the library and the package.
Not release guarantees. Console output, arguments, exit codes, local Lake
imports and selected language/filesystem fixtures have native-versus-Node tests,
and before 0.1.0 Lean's runtime-capable upstream suite and 19 ecosystem projects
ran on all five platforms with zero regressions against the previous accepted
candidate (0.1.0 gate). Complete IO.FS, concurrency,
subprocess/signal/terminal parity, every third-party library and the full upstream
suite are not guaranteed (IO gaps). Big numbers (Nat/Int
beyond 63 bits) use Lean's own arbitrary-precision code instead of GNU MP, so apps
contain no LGPL code; results equal native Lean's, but at 10,000 digits
multiplication is about 5× and Nat.gcd about 60× slower than with GMP in Wasm
(numbers). Lake dependencies use
your system Git (system Git).
Releases and provenance
0.1.0 was the first npm release (notes); 0.2.0 adds
edge targets and Bun/Deno (notes); 0.3.0 adds the
browser, Lasm.UI, full-stack apps, WASI/Fastly and the editor support
(notes); 0.3.1 puts the starter code under MIT-0 and
adds bug-report pointers (notes); 0.3.2 fixes
Std.Internal.UV sockets, timers and signals (notes); 0.3.3 was never
published, and its changes shipped in 0.3.4 (notes); 0.3.5 updates the README and the Conduit example (notes). Lasm's npm
packages (@lasm/cli, @lasm/create and the @lasm/… tool packages) are
published only by the release.yml workflow,
through npm trusted publishing with provenance: no npm tokens exist. A release is a
pull request that bumps the package.json version and is published only after the
release-checks status (five-platform candidate checks; edge, Bun/Deno, browser,
Lasm.UI, WASI/Fastly, fullstack, unit and dev-loop checks; and an npm rehearsal of
the exact packages) passed for the released
commit. Found a bug? npx lasm --version gives the version line the
bug report form asks for. npm view @lasm/cli@<version> shows the provenance, and npm audit signatures in a project verifies it. Details: RELEASING.md.
Licenses
Lasm's own source is under the MIT License (copyright Jordan Last).
The starter code — everything lasm init and npm create @lasm put in a new
project, templates/, examples/ and the code samples in this README and
docs/ — is also under MIT No Attribution (MIT-0), in every
version: it is yours, no attribution needed. @lasm/create is MIT, and
@lasm/cli's license field is an SPDX expression of Lasm's MIT, MIT-0 and
every component it bundles, with their texts in licenses/. The
license audit lists every distributed component, its
license and how each obligation is met.
- Built
dist/.THIRD_PARTY_NOTICES.txtstarts with Lasm's MIT notice and the list of Lean files Lasm changed, then the licenses of what the build contains: Lean's runtime and standard library (Apache-2.0), Emscripten (MIT OR NCSA), musl, LLVM libc++, libc++abi, libunwind and compiler-rt, mimalloc, libuv, koffi and Lasm's host (MIT). Apps contain no copyleft code.licenses/<package>/holds the license files of every Lake dependency compiled intoprogram.wasm; with npm interop,node_modules/holds each package with its own license. Keep these files when you distribute adist/. - Edge, browser, WASI, Fastly and wasi-http outputs get the same notices,
rewritten to say what their files contain, written automatically: a
THIRD_PARTY_NOTICES.txtin every output directory, a short/*! … */licence banner on the JavaScript a platform bundles (the edge glue, the browser loader), the whole notices file inside the Wasm of server-side outputs (alasm.noticescustom section, so it travels with every deployment), the file in the Fastly package, andTHIRD_PARTY_NOTICES.lasm.txtin Vite builds. What this means for your app: docs/LICENSING.md. - Tool packages and release archives. Each
@lasm/…tool package has an SPDXlicensefield and the archive's license files. The exact sources of the (L)GPL pieces the Lean archives contain (GNU MP, the GNU C Library, GCC's runtime libraries, the Linux UAPI headers, MinGW-w64) and Lean's own source are mirrored as assets of the release that publishes them (lean-4.35.0-tiers-v2). Lasm ships no Git and no Apple SDK files. - The Lean name and logos are trademarks of Lean FRO. Lasm is not affiliated with or endorsed by Lean FRO.