LeanUI counter
.set (s.toInt?.getD n)} />
module
public import LeanUI
public import LeanUI.Router
public import LeanUI.Json
public import Counter
public import TodoMVC
meta import LeanUI.Json.Derive
meta import LeanUI.Syntax
/-!
# The LeanUI site
A real multi-page app, and the page LeanUI presents itself with: a home page, the examples, and
the benchmark results. It is one `Program`, served by a native Lean server
(`examples/SiteServer.lean`) that renders every page to HTML first and then lets the browser's
wasm build hydrate it, or, where WebAssembly Memory64 is missing (Safari), drives it from the
server.
What it exercises:
* **routing** on real paths (`/`, `/examples`, `/benchmarks`) with `data-link` links,
back/forward, the page title, and a server that renders whichever page the URL names;
* **components**: the counter and TodoMVC are whole programs embedded with `Html.map`,
`Cmd.map` and `Sub.map`. TodoMVC keeps its own fragment routing (`/examples#/active`) next to
the site's path routing: each subscribes to URL changes, and one browser listener serves both;
* **data**: the benchmarks page fetches `/data/benchmarks.json`, decodes it with derived
`FromJson`, and draws an SVG chart (templates set SVG namespaces by themselves);
* **templates** throughout.
-/
@[expose] public section
open LeanUI Attr
namespace Site
/-! ## Data -/
structure Op where
name : String
desc : String
/-- CPU time per operation, LeanUI and the vanilla reference (Chromium, traced). -/
leanui : Float
vanilla : Float
deriving ToJson, FromJson, Inhabited
structure Ratios where
browser : String
/-- LeanUI ÷ vanilla per operation, in `ops` order. -/
ratios : Array Float
deriving ToJson, FromJson
structure Results where
machine : String
chromium : String
ops : Array Op
release : Array Ratios := #[]
/-- Native Lean cost of the same operations (update, view, diff, encode), ms. -/
native : Array Float := #[]
deriving ToJson, FromJson
/-! ## Pages -/
inductive Page | home | examples | benchmarks | notFound
deriving BEq, Repr, Inhabited
def Page.title : Page → String
| .home => "LeanUI" | .examples => "Examples" | .benchmarks => "Benchmarks" | .notFound => "Not found"
def pages : Route Page := Route.oneOf [
Route.top |>.to (fun _ => .home) (fun | .home => some () | _ => none),
Route.lit "examples" |>.to (fun _ => .examples) (fun | .examples => some () | _ => none),
Route.lit "benchmarks" |>.to (fun _ => .benchmarks) (fun | .benchmarks => some () | _ => none)]
def router : Router Page := { route := pages }
/-! ## Model -/
inductive Load (α : Type) where
| idle | loading | ok (a : α) | failed (why : String)
structure Model where
page : Page
counter : Int := 0
todo : TodoMVC.Model
results : Load Results := .idle
inductive Msg where
| urlChanged (url : String)
| counter (m : Counter.Msg)
| todo (m : TodoMVC.Msg)
| got (r : Response)
def dataUrl := "/data/benchmarks.json"
/-- Arrive on a page: set the title, and load the results the first time they are shown. -/
def arrive (m : Model) (p : Page) : Model × Cmd Msg :=
let title : Cmd Msg := .title (if p == .home then "LeanUI: web UIs in Lean 4" else s!"{p.title} · LeanUI")
match p, m.results with
| .benchmarks, .idle => ({ m with page := p, results := .loading }, .batch #[title, .fetch { url := dataUrl } .got])
| _, _ => ({ m with page := p }, if p == m.page then .none else title)
def update (msg : Msg) (m : Model) : Model × Cmd Msg :=
match msg with
| .urlChanged url => arrive m ((router.parse url).getD .notFound)
| .counter cm => let (n, c) := Counter.update cm m.counter; ({ m with counter := n }, c.map .counter)
| .todo tm => let (t, c) := TodoMVC.update tm m.todo; ({ m with todo := t }, c.map .todo)
| .got r =>
let results := if !r.ok then .failed s!"HTTP {r.status}" else
match Json.decode Results r.body with
| .ok d => .ok d
| .error e => .failed e
({ m with results }, .none)
/-- Flags: the page's URL (path and fragment). -/
def init (url : String) : Model × Cmd Msg :=
let (todo, tc) := TodoMVC.init url
let page := (router.parse url).getD .notFound
let m : Model := { page, todo }
-- the server renders the page as it is before any data arrives: "loading" for the results
let (m, c) := if page == .benchmarks then ({ m with results := .loading }, Cmd.fetch { url := dataUrl } Msg.got) else (m, .none)
-- no focus on arrival: the TodoMVC example should not steal the page's focus
let tc := match tc with
| .batch cs => Cmd.batch (cs.filter fun (c : Cmd TodoMVC.Msg) => match c with | .focus _ => false | _ => true)
| c => c
(m, .batch #[c, tc.map .todo])
/-- The site follows the URL's path, the embedded TodoMVC its fragment: two subscriptions to URL
changes, served by one browser listener. -/
def subscriptions (m : Model) : LeanUI.Sub Msg :=
.batch #[.urlChanges .urlChanged, (TodoMVC.subscriptions m.todo).map .todo]
/-! ## View -/
/-- The source repository (an ordinary link: it leaves the site). -/
def sourceUrl := "https://github.com/FawadHa1der/leanui"
def navLink (current p : Page) (label : String) : Html Msg :=
router.link p #[cls (if current == p then "active" else "")] #[text label]
def header (m : Model) : Html Msg :=
.set (s.toInt?.getD n)} />
LeanUI counter
{body}
A model, an update function and a view: plain Lean values, type-checked like the rest of your code. LeanUI compiles them to WebAssembly (wasm64), drives the DOM with a compact command buffer, and renders the same program on a Lean server.
{router.link .examples #[cls "button primary"] #[text "See the examples"]} {router.link .benchmarks #[cls "button"] #[text "How fast is it?"]}
{counterSource}That is the view of examples/Counter.lean, running here:
A program of four lines of logic, embedded here as a component with Html.map.
The whole TodoMVC specification: editing, filters in the URL fragment, the list kept in
{" "}localStorage as JSON. Its model logic is proved to keep the list's keys
unique under every message.
Can we staff this? An untrusted solver answers, and a checker proved correct in Lean decides what the app shows, down to why a day cannot be staffed. A roster comes with a proof, which an independent kernel checks again in your browser.{" "} Open the Roster demo
CPU time per operation (script and the rendering work it causes), median of fresh page loads, in {r.chromium} on {r.machine}. Both implementations build identical tables.
LeanUI vanilla JavaScript
{chart r.ops}| operation | Lean (native) | {...r.release.map fun b =>{b.browser} | }
|---|---|---|
| {o.desc} | {(r.native[i]?.map fun x => fmt x ++ " ms").getD "–"} | {...r.release.map fun b =>{(b.ratios[i]?.map fun x => fmt x ++ "×").getD "–"} | }
Release-browser columns: LeanUI ÷ vanilla (script and layout). "Lean (native)" is the Lean side alone, compiled natively: update, view, diff and command encoding, without the DOM.
The js-framework-benchmark keyed operations, against hand-written vanilla JavaScript with the same markup, stylesheet and data.
The race runs this benchmark and a Game of Life side by side in your browser, timed live: vanilla JavaScript, LeanUI compiled to wasm64, and LeanUI interpreted by lean-vir. It checks that all three end in the same state.
{match m.results with | .ok r => resultsView r | .failed why =>Could not load the results: {why}
| _ =>Loading the results…
}{router.link .home #[] #[text "Back to the home page"]}