Kite
A small, explicit language for the web.
Why Kite?
Explicit
Nothing hides control flow. There is no exception unwinding, no implicit conversion and no overloading — every branch a program can take is written down where it happens. Verbosity is an acceptable price for that; hidden control flow is not.
Checked
Failures are ordinary values, and the compiler enforces it: an error you have not checked makes the value beside it unreadable. Matches are exhaustive, bindings are immutable by default, and the diagnostics name one cause and one fix.
Web-first
WebAssembly is the primary target, not a backend bolted on. Kite emits WasmGC directly, so it ships no garbage collector of its own — the difference between a 300 KB hello world and a 5 KB one.
See it run
A Mastodon client
Kite Feeds is a real
client for a real network, and there is no JavaScript in the
project — the page points a <script
type="module"> at a .kite file and
vite-plugin-kite
wires up what kitec produced. Sign in with any Mastodon
server: it registers itself, sends you there to approve it, and
exchanges the code you come back with. There is no backend.
Three things in it are worth reading the source for. The timeline is virtualized against the window, and the arithmetic that decides which rows exist is a file with no document in it at all. Timelines are lists of ids, so returning to a route you have already visited costs no request and puts your scroll position back — right down to the measured row heights, which is what makes the offset land rather than approximate. And pressing the heart writes to one map entry before any request exists, so the home feed, the post's own page and its author's profile all change at once: not because they were told to, but because none of them ever held a copy.
129 KB of WebAssembly for the whole application. The source is channyeintun/kite-feeds, and its README is candid about the two files that exist because of the target rather than the design.
A point of sale
Kite POS
is two installable apps for one corner shop — a till for the counter,
a back office for whoever owns the place. A role decides which one a
sign-in opens and there is no navigation between them, so they are
separate pages, each pointing a <script type="module">
straight at a .kite file. They still share every module
in app/src/: because a Kite module is a
directory, api.kite, money.kite and
i18n.kite are compiled into both without a line of either
duplicated.
It is the bigger of the two programs. Fourteen back-office screens off
one rail — stock, purchasing, double-entry books where no balance is
stored and every figure is summed from the journal — and a till that
merges a rescan into ×3 rather than three lines, splits a
payment four ways, and keeps a held basket in the database so a
refresh cannot lose it. Every amount is an integer count of the
currency's minor unit, and money.kite reads a typed price
as digits rather than as a float, because 1.005 is really
1.00499999999999989 as a double and would round the wrong
way. Both apps run in English and Burmese (မြန်မာ).
220 KB of WebAssembly for the till and 1.35 MB for the back office — 72 KB and 292 KB over the wire. There is nothing to open: a shop's books are its own, so this one is here to be read rather than clicked. The Worker behind both apps is TypeScript, not Kite.
What is different
An error you have not checked makes the value unreadable
Go's (T, error) shape is right: failures are ordinary values
and every one is visible in the source. Its flaw is that nothing enforces
it — and the value on a failure path is a zero value that flows onward
looking valid.
use std/json
fn title_of(document: str) -> (str, error) {
let (parsed, err) = json.parse(document)
// `parsed` cannot be read yet: on the failure path there is no value at all.
check err
return json.text_or(parsed, "title", "untitled"), nil
}
Reading parsed before the check is E0301.
Letting err go out of scope unexamined is
E0302.
A type that checks the answer, not one that assumes it
axios.get<Basket>(url) is a claim, not a check.
TypeScript writes the type in and never looks at a field, so a server
that stops sending total produces an
undefined somewhere later — far from the request that
caused it.
use std/json
@derive(Decode)
struct Basket {
sale_id: str
total: int
}
fn basket_of(body: str) -> (Basket, error) {
let (answer, err) = json.parse(body)
check err
return Basket.decode(answer)
}
A body without total comes back as
Basket.total: expected a whole number — at the request,
naming the field. The decoder is written from the struct, so unlike a
hand-kept validation schema there is no second copy to drift.
It is strict on purpose: a missing field fails the whole value. That is right for a server whose columns cannot be null, and wrong for one that may legitimately leave a field out — which wants a reader written by hand. Worth deciding rather than discovering.
Concurrency is one concept
Some operations take time: mark them async and
await them. Calling one yields a Task<T>
and does not run its body — await is what drives
it, and two tasks awaited together overlap. There are no channels, no
goroutines and no select.
let a = fetch("alpha", 100)
let b = fetch("beta", 50)
let (first, second) = await task.both(a, b) // 100ms, not 150
The source never says how many threads exist. Share — a
marker the compiler infers structurally — is what will make the same
program parallel on the web the day shared-everything-threads ships.
Exhaustive matching, and a compiler that names what is missing
enum Shape {
Circle(radius: float)
Rect(width: float, height: float)
Point
}
fn area(s: Shape) -> float {
return match s {
Circle(r) => 3.14159 * r * r,
Rect(w, h) => w * h,
Point => 0.0,
}
}
Leave out Point and the error names it. That is what makes
adding a variant safe: the compiler shows every place that must change.
Immutable by default
let bindings and struct fields are immutable unless marked
var. That maps directly onto WasmGC's per-field mutability
flag, removes the value-versus-pointer distinction, and makes most types
shareable across tasks without their author doing anything.
Diagnostics are the product
error[E0114]: cannot assign to immutable binding `total`
┌─ cart.kite:14:5
│
9 │ let total = 0
│ ----- declared immutable here
⋮
14 │ total = total + item.price
│ ^^^^^ cannot assign
│
help: make the binding mutable
│
9 │ var total = 0
│ ~~~
Several decisions in the language — nominal traits, explicit
dyn, no implicit conversions, no overloading — were made
because they let the compiler name one cause and one fix.
Run Kite with no server
The playground is the compiler itself, built for WebAssembly. It checks, formats, runs, and will show you the AST, the HIR, the MIR and the bytecode. Nothing on that page talks to a server.
Why it exists
JavaScript and TypeScript grew into application development by accident. Every serious web application today ships a compiler, a bundler, a type checker bolted on from outside, a virtual DOM, and a runtime that re-derives structure the compiler already knew and threw away.
WebAssembly 3.0 — ratified 13 June 2026 — removed the last technical reason to accept that. It standardises garbage collection, native exception handling, tail calls and typed function references, and all of it is baseline across Chrome, Firefox and Safari. A language targeting Wasm today does not need to ship a garbage collector inside its own binary. That single fact is the difference between a 300 KB "hello world" and a 5 KB one.
Where it runs
| Target | How | State |
|---|---|---|
wasm32-gc | WasmGC, emitted directly | Every construct the language has |
kbc | Register bytecode and a VM | The dev loop, the embedding target, and the differential oracle |
native-* | Cranelift, ahead of time | Not yet — see the roadmap |
Every program in the test corpus is compiled to both backends, run on both, and the outputs compared. Two independent implementations that must agree is what makes codegen bugs findable.
The tools
kitec run file.kite compile and run
kitec check file.kite check only
kitec test file.kite run every `test_` function
kitec fmt file.kite lay it out the one way
kitec doc file.kite the reference, from the doc comments
kitec fix file.kite apply every machine-applicable suggestion
kitec build file.kite --emit wasm --out dist
kitec --explain E0301 why a rule exists
Reading order
The crash course
The whole language in one narrated sitting, typed into an editor as it goes. Every program in it is compiled by this compiler, so nothing on screen is a mock-up.
Watch the courseThe specification
The language itself — grammar, type system, semantics and the diagnostic catalogue.
Read the specThe standard library
Generated from its own source by kitec doc, because
the library is written in Kite too.
Platform research
What Wasm can and cannot do in 2026, with sources.
Read the researchConcurrency
The async model and the Share marker.
Compiler architecture
Crates, IRs, and the WasmGC lowering.
Read the architecture