[{"data":1,"prerenderedAt":4},["ShallowReactive",2],{"NTyG9QSoXm":3},"# LeanUI\n\n**Web user interfaces written in Lean 4.** An app is a pure Elm-architecture program (a model, an\n`update` function and a `view`) whose views are ordinary Lean values, or JSX-style templates that\ncompile to the same code. It runs in the browser as **single-threaded wasm64**, drives the DOM\nthrough a compact binary command buffer, and the same program renders on a native Lean server,\nwhich can also drive the page remotely for browsers without WebAssembly Memory64.\n\n```lean\nmodule\npublic import LeanUI\nmeta import LeanUI.Syntax          -- templates are compile-time only: zero runtime cost\npublic section\nopen LeanUI\n\ninductive Msg | inc | dec\n\ndef view (n : Int) : Html Msg :=\n  \u003Cp>\n    \u003Cbutton onClick={.dec}>−\u003C/button>\n    \u003Coutput class={if n \u003C 0 then \"neg\" else \"pos\"}>{n}\u003C/output>\n    \u003Cbutton onClick={.inc}>+\u003C/button>\n  \u003C/p>\n\ndef program : Program Int Msg where\n  init _ := (0, .none)\n  update msg n := (match msg with | .inc => n + 1 | .dec => n - 1, .none)\n  view := view\n```\n\n## What is in the box\n\n| | |\n|---|---|\n| **Core** (`LeanUI.Html`, `Diff`, `Patch`, `App`, `Effect`, `Input`) | views, keyed and positional reconciliation, exact `lazy`/`memo` subtrees, repeated subtrees created by cloning DOM templates, delegated events with browser-extracted payloads (values, keys, forms, files, pointers with capture, the wheel), commands (fetch, timers, storage, history, focus, title, animation frames, pixels to a canvas straight from wasm memory, file bytes and decoded images into Lean, downloads) and subscriptions (intervals, URL, keys), all as commands in one buffer per update |\n| **Templates** (`LeanUI.Syntax`, [docs/SYNTAX.md](docs/SYNTAX.md)) | JSX-style syntax with components, fragments, conditional children, class/style parts, SVG and MathML namespaces, precise compile errors; the generated C equals the hand-written view's |\n| **Kit** (`LeanUI.Json`, `LeanUI.Router`) | JSON on `Init` alone with exact numbers and `deriving ToJson, FromJson`; typed bidirectional routes (path or fragment) whose links cannot drift from the parser |\n| **Server** (`LeanUI.Server`) | a native `Std.Http` server: static files, server-side rendering with hydration, server-driven sessions, the `leanui` command (`serve`, `build`, `new`) |\n| **Proofs** ([docs/PROOFS.md](docs/PROOFS.md)) | the keyed reconciliation planner is machine-checked correct and optimal (fewest moves); TodoMVC's model keeps its list keys unique under every message; the Roster app never shows an answer its proved checker did not accept |\n| **Testing** (`LeanUI.Testing`) | a pure DOM model that applies command buffers exactly as the browser does, property tests and fuzzing against a reference model, a strict protocol checker, SSR parser checks, browser suites in Chromium and Firefox |\n| **Examples** | a counter, [TodoMVC](examples/TodoMVC.lean) (the full spec, proved invariants), [the LeanUI site](examples/Site.lean) (routed pages, components, fetch, SVG; server-rendered), [a photo editor](examples/Editor.lean) (layers, filters, painting, PNG; every pixel in Lean), [Roster](roster/README.md) (an untrusted solver whose answers a proved checker decides, rosters notarized by Lean's kernel and verified again in the browser), the js-framework-benchmark app, an effects lab, a pixels lab, [the guide's notes app](docs/GUIDE.md) |\n\n## How it works\n\nAn app is built twice from the same Lean code. Emscripten is used only at build time: the\nbrowser receives its output, never the compiler.\n\n```mermaid\nflowchart LR\n  app[\"Your app in Lean\u003Cbr/>model, update, view\"]\n  app -->|\"lake\"| c[\"C code\"]\n  c -->|\"Emscripten, in Docker,\u003Cbr/>at build time\"| wasm[\"app.wasm + app.js\u003Cbr/>the app as wasm64\"]\n  app -->|\"lake, native\"| server[\"Server binary\u003Cbr/>optional\"]\n  rt[\"runtime/leanui.js\u003Cbr/>about 6 KB, the same for every app\"]\n  wasm --> browser((\"Browser\"))\n  rt --> browser\n  server -.->|\"serves pages and files\"| browser\n```\n\nAt run time, Lean does all the computing and a small fixed JavaScript runtime makes the DOM\ncalls, because WebAssembly cannot reach the DOM directly. One page, from the first request to\nthe user's clicks:\n\n```mermaid\nsequenceDiagram\n  autonumber\n  actor U as User\n  participant B as Browser DOM\n  participant J as leanui.js\n  participant W as App in wasm (Lean)\n  participant S as Lean server (native)\n\n  B->>S: GET /examples\n  S->>S: init and view run natively, Html.render\n  S-->>B: complete HTML, visible before any script runs\n  B->>J: load leanui.js, LeanUI.boot\n  alt Browser has WebAssembly Memory64 (Chrome, Firefox)\n    J->>W: load app.wasm, leanui_boot(flags, hydrate)\n    W-->>J: command buffer: claim the existing nodes, attach listeners\n    J->>B: adopt the server-rendered nodes (hydration)\n    loop every interaction\n      U->>B: click, type, submit\n      B->>J: one delegated listener per event type\n      J->>W: leanui_event(listener slot, payload)\n      W->>W: update, view, diff\n      W-->>J: command buffer, read in place from wasm memory\n      J->>B: DOM calls: create or clone, set attribute, insert, remove\n      opt the update asked for an effect, such as fetch\n        J->>S: GET /data/benchmarks.json\n        S-->>J: response\n        J->>W: leanui_callback(id, ok, body)\n      end\n    end\n  else No Memory64 (Safari): the server runs the same program\n    J->>S: POST /leanui/session: h (hydrate)\n    S->>S: a session starts the program natively\n    S-->>J: command buffer\n    J->>B: adopt the server-rendered nodes\n    loop every interaction\n      U->>B: click, type, submit\n      B->>J: one delegated listener per event type\n      J->>S: POST /leanui/session: e, listener slot, payload\n      S->>S: update, view, diff\n      S-->>J: command buffer\n      J->>B: DOM calls\n    end\n  end\n```\n\nThe server is optional. Served by any static file host (`leanui serve`), a page starts empty\nand the wasm mounts the app, with every interaction as in the first branch above.\n\n## How it performs\n\nThe [js-framework-benchmark](https://github.com/krausest/js-framework-benchmark) keyed operations,\nagainst a hand-written vanilla JavaScript implementation with the same markup, stylesheet and data\n(both build identical tables). CPU time per operation, LeanUI ÷ vanilla: Chromium traced (script\nplus the rendering work it causes), release Chrome and Firefox self-timed (script plus layout),\non an Apple M4 Max; [docs/BENCHMARKS.md](docs/BENCHMARKS.md) has the method, every number,\nmemory, startup and the profiles.\n\n| operation | Chromium 151 (script + render) | Chrome 155 (script + layout) | Firefox 158 (script + layout) |\n|---|--:|--:|--:|\n| create 1,000 rows | 1.10× | 1.03× | 1.15× |\n| replace all 1,000 rows | 1.18× | 1.17× | 1.49× |\n| update every 10th of 1,000 rows | 1.19× | 1.35× | 1.27× |\n| highlight a row (of 1,000) | 1.35× | 5.00× | 5.70× |\n| swap 2 of 1,000 rows | 1.14× | 1.25× | 1.17× |\n| remove one of 1,000 rows | 1.07× | 1.26× | 1.12× |\n| create 10,000 rows | 1.18× | 1.11× | 1.17× |\n| append 1,000 rows to 1,000 | 1.21× | 1.18× | 1.36× |\n| clear 1,000 rows | 1.33× | 1.47× | 1.50× |\n| **geometric mean** | **1.19×** | **1.43×** | **1.50×** |\n\nChrome's `performance.now()` has 0.1 ms steps on pages without cross-origin isolation, so its\n\"highlight a row\" cell (LeanUI 0.5 ms, vanilla rounding to 0.1 ms) mostly measures resolution;\nwithout it Chrome's mean is 1.22×, and the traced Chromium column (microsecond timestamps)\ngives 1.35× for that operation.\n\nNatively, Lean selects a row in 0.08 ms and creates 1,000 rows in 0.63 ms (update, view, diff\nand encoding). The benchmark app is ready about 25 ms after navigation in Chromium and weighs\n168 KB with brotli (wasm, glue and runtime).\n\n**The race** ([`/race/`](bench/race/), on the site too) runs the benchmark app and a 1,000-cell\nGame of Life three ways side by side, timed live in your browser and checked against each other:\nhand-written JavaScript, LeanUI compiled to wasm64, and the same Lean interpreted by\n[lean-vir](https://github.com/ejgallego/lean-vir) (Lean's IR interpreter in WebAssembly; built\napart, in [`vir/`](vir/README.md), for the benchmarks only). Interpreted, the Lean side of a click\ntakes about 13× as long in the keyed operations and 48× in Life (a cloud VM; see\n[docs/BENCHMARKS.md](docs/BENCHMARKS.md#lean-vir-the-same-lean-interpreted)).\n\n## Writing apps\n\nA few idioms cover most of what an app needs; `examples/` has each of them in a real program.\n\n- **Effects** are values: `update` returns a `Cmd` (`.fetch`, `.after`, `.storageSet`,\n  `.storageGet`, `.pushUrl`, `.focus`, `.title`, `.putImage`, `.readFile`, `.decodeImage`,\n  `.download`, …) whose results come back as messages, and\n  `subscriptions` returns the event sources wanted right now (`.every ms`, `.urlChanges`, `.keys`). In a\n  module that does `open LeanUI`, write the subscription type as `LeanUI.Sub` (Lean's own `Sub`\n  is the subtraction class).\n- **Big lists**: give rows keys and make them lazy, so unchanged rows are neither rendered nor\n  diffed: `keyedLazy (toString r.id) (r.label, selected) fun _ => row r`. Dependencies are\n  compared exactly (any `ToDep` value: numbers, strings, booleans, options, tuples, lists), and\n  so is the key, which therefore need not be repeated among them.\n- **Components** are programs: embed one with `Html.map`, `Cmd.map` and `Sub.map`\n  (`examples/Site.lean` embeds the counter and TodoMVC this way).\n- **Routing**: `LeanUI.Router` routes are bidirectional, so a link and the parser cannot drift\n  apart; `router.link page attrs kids` navigates without a page load.\n- **JSON**: `deriving ToJson, FromJson` (with `meta import LeanUI.Json.Derive`), then\n  `Json.encode` / `Json.decode`.\n- **Server rendering**: `LeanUI.Server` renders any page of a program to HTML and lets the wasm\n  build hydrate it, or drives it from the server (`docs/SERVER.md`).\n\n## The LeanUI site\n\n`examples/Site.lean` is a real multi-page app: a home page with a live counter, the examples\n(the counter and TodoMVC as embedded components) and the benchmark results (fetched as JSON and\ndrawn as SVG). A native Lean server renders each page, then the wasm build hydrates it, or, in a\nbrowser without Memory64, the server runs the program and drives the page.\n\n```bash\ntools/build-app.sh SiteApp site\n./.lake/build/bin/leanui-site               # http://127.0.0.1:5360/  (?mode=wasm|server|ssr)\n```\n\n## LeanUI Photo: a photo editor\n\n`examples/Editor.lean` is a complex, highly interactive client-side app: a layered photo editor\nwhose pixel work is all Lean (`examples/Editor/*.lean`: adjustments with curves and levels,\nGaussian blur, unsharp mask, convolutions and edges, crop, rotation, resize, eight blend modes,\na soft brush, histogram, undo history, PNG encoding and decoding), compiled to wasm64. The\nbrowser decodes JPEGs and shows pixels: the canvas is filled straight from wasm memory\n(`Cmd.putImage`). Sliders preview on a downscaled copy; full-resolution work runs a slice of\nrows per animation frame. It is served as its own page, `/editor/`, on the site. Its video lab,\n`/editor/video/` (`examples/Video.lean`), plays a video file and runs the same Lean filters on\nevery frame (the frame comes from WebCodecs' `VideoFrame`), at the frame rates measured in the\nbenchmarks.\n\n```bash\ntools/build-app.sh EditorApp editor && tools/build-app.sh VideoApp editor/video\n./.lake/build/bin/leanui serve dist        # http://127.0.0.1:8000/editor/ and /editor/video/\n```\n\nKeys: B brush, E eraser, H hand, C crop (Enter applies), + − 0 1 zoom, [ ] brush size,\nCtrl+Z / Ctrl+Shift+Z undo and redo. Its benchmarks (each operation against the same algorithm in\nplain JavaScript, interaction latency, memory) are in\n[docs/BENCHMARKS.md](docs/BENCHMARKS.md#the-photo-editor).\n\n## Browsers\n\nLeanUI's wasm needs WebAssembly **Memory64** and exception handling: Chrome and Edge 133+, Firefox\n134+. Pages need no special headers (the runtime is built without threads, so no\n`SharedArrayBuffer` and no cross-origin isolation). Safari has no Memory64 yet: there the server\nruns the same program natively and drives the page over HTTP (`LeanUI.boot` picks the mode).\n\n## Building\n\nRequirements: the Lean toolchain named in `lean-toolchain` (v4.34.0) and Docker.\n\n```bash\ntoolchain/build-archives.sh                 # once: the single-threaded wasm64 Lean runtime (≈10 min)\n~/.elan/toolchains/leanprover--lean4---v4.34.0/bin/lake build   # library, examples, tests, proofs\ntools/build-app.sh TodoMVCApp todomvc       # an app: dist/todomvc/{index.html,leanui.js,app.js,app.wasm}\n./.lake/build/bin/leanui serve dist/todomvc # and open http://127.0.0.1:8000/\n```\n\n[docs/TOOLCHAIN.md](docs/TOOLCHAIN.md) explains what is linked and why it is single-threaded.\n\n## Testing\n\n```bash\n./.lake/build/bin/leanui-tests            # core: diff, keyed/positional/lazy lists, render, planner\n./.lake/build/bin/leanui-props            # property tests and fuzzing against the reference model\n./.lake/build/bin/leanui-syntax-tests     # templates\n./.lake/build/bin/leanui-kit-tests        # JSON, URLs, routes, effects\n./.lake/build/bin/leanui-server-tests     # server, sessions, CLI\n./.lake/build/bin/leanui-todomvc-tests    # TodoMVC against the DOM model\n./.lake/build/bin/leanui-notes-tests      # the guide's app\n./.lake/build/bin/leanui-editor-tests     # the photo editor: image operations, PNG, jobs, the program\ntests/browser/run-all.sh --build          # every browser suite in Chromium and Firefox (Playwright):\n                                          # todomvc, site, counter, keyed-dom, fuzz, templates,\n                                          # apply-semantics, server, effects, bench-smoke, pixels, editor, video, depth,\n                                          # race (vanilla, wasm64 and lean-vir must agree),\n                                          # roster and its verify page (with leanui-roster's notary)\nnode tests/browser/bench.mjs              # the benchmark (Chromium traces); selfbench.mjs for release browsers,\n                                          # apply-bench.mjs for the JS runtime alone\n```\n\n`lake build` also checks every proof.\n\n## Continuous integration and deployment\n\nGitHub Actions (`.github/workflows/ci.yml`) runs the native build and every test above on each push\nand pull request, then the browser suites against the wasm64 builds (and, for the race, the\nlean-vir builds), and checks the Roster demo's completeness proof against Mathlib in a job of its\nown. On `main` it deploys the LeanUI site to Cloudflare: a Worker in front of a\ncontainer that runs the native `leanui-site`, so pages are server-rendered and Safari gets\nserver-driven sessions. Setup and secrets:\n[docs/DEPLOY.md](docs/DEPLOY.md).\n\n## Documentation\n\n- [docs/DESIGN.md](docs/DESIGN.md): the architecture and the contracts between its parts\n- [docs/PROTOCOL.md](docs/PROTOCOL.md): the command-buffer protocol, normatively\n- [docs/SYNTAX.md](docs/SYNTAX.md): templates\n- [docs/PROOFS.md](docs/PROOFS.md): what is proved, tested and assumed\n- [docs/BENCHMARKS.md](docs/BENCHMARKS.md): performance, memory, startup, method\n- [docs/SERVER.md](docs/SERVER.md): the native server, server rendering, server-driven sessions\n- [docs/TOOLCHAIN.md](docs/TOOLCHAIN.md): the wasm64 toolchain\n- [vir/README.md](vir/README.md): the same apps on lean-vir, for the benchmarks\n- [roster/README.md](roster/README.md): the Roster demo, its proofs, notary and verify page\n- [docs/DEPLOY.md](docs/DEPLOY.md): CI, and the site on Cloudflare Containers\n\n## License\n\nMIT, see [LICENSE](LICENSE).\n",1792957695191]