In this tutorial we will write a few Lean definitions about rectangles, prove a fact about them, generate a TypeScript package (running as WebAssembly) from them with lungo, and use it. The Lean project is the same as in the tutorials for Go, Python, Swift and C.
Before we start
We need:
- Node.js 20 or later
- elan, Lean’s toolchain manager
- the
lungocommand
elan toolchain install leanprover/lean4:v4.34.1
brew install machinefabric/tap/lungo # apt, dnf, Windows, cargo: see the lungo command's reference
Write the Lean project
Make a directory with a Lake project in lean/:
mkdir shapes && cd shapes
mkdir lean
echo 'leanprover/lean4:v4.34.1' > lean/lean-toolchain
lean/lakefile.toml declares a library whose root module Lake builds by default:
name = "geometry"
version = "0.1.0"
defaultTargets = ["Geometry"]
[[lean_lib]]
name = "Geometry"
lean/Geometry.lean holds the definitions and the proof:
namespace Geometry
structure Rect where
width : Nat
height : Nat
deriving Repr
def area (r : Rect) : Nat := r.width * r.height
def scale (k : Nat) (r : Rect) : Rect :=
{ width := k * r.width, height := k * r.height }
/-- Scaling by `k` multiplies the area by `k * k`. -/
theorem area_scale (k : Nat) (r : Rect) : area (scale k r) = k * k * area r := by
simp only [area, scale]
exact Nat.mul_mul_mul_comm k r.width k r.height
/-- The rectangle with the largest area, if any. -/
def largest (rs : List Rect) : Option Rect :=
rs.foldl (fun best r => match best with
| some b => if area r > area b then some r else some b
| none => some r) none
end Geometry
Create Lake’s manifest once (it is part of the project):
(cd lean && lake update)
Generate the package
lungo.toml names the Lake project and where the npm package goes:
project = "lean"
[ts]
out = "geometry-js"
lungo generate
The first run builds the Lean project, which checks the proof: change k * k to k in the
theorem and lungo generate fails with Lean’s error. It also downloads the pinned wasi-sdk
(once; its digest is checked) and links the program to geometry-js/program.wasm. Add
.lungo/ (lungo’s scratch space) to .gitignore.
Call it from JavaScript
package.json of the application:
{ "type": "module", "dependencies": { "geometry": "file:./geometry-js" } }
shapes.js:
import { load } from "geometry";
const geometry = await load();
const r = { width: 3n, height: 4n };
console.log("area:", geometry.area(r));
const bigger = geometry.scale(10n, r);
console.log("scaled:", bigger);
console.log("largest:", geometry.largest([r, bigger]));
$ npm install && node shapes.js
area: 12n
scaled: { width: 30n, height: 40n }
largest: { width: 30n, height: 40n }
Nat became bigint, the structure a plain object, and Option a value or null;
index.d.ts gives TypeScript the types. Node.js may print an ExperimentalWarning about
WASI, which it runs the module on. In a browser, load() fetches program.wasm next to the
module and runs it on lungo-ts’s minimal WASI.
What we did
We generated an npm package from a Lake project whose proofs Lean checked; its methods run Lean’s compiled code, compiled to WebAssembly with the lungo runtime. Next:
- Generated packages: every Lean type in TypeScript, and what WebAssembly cannot do
- How to call host code from Lean
- How to distribute generated packages