Kite

Kite

A small, explicit language for the web.

Try it in your browser Watch the crash course Install

v0.1.9 — release notes

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.

The crash course

Open it full screen

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.

Open Kite Feeds

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.

Read the source

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 bodyawait 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.

Open the playground

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

TargetHowState
wasm32-gcWasmGC, emitted directlyEvery construct the language has
kbcRegister bytecode and a VMThe dev loop, the embedding target, and the differential oracle
native-*Cranelift, ahead of timeNot 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 course

The specification

The language itself — grammar, type system, semantics and the diagnostic catalogue.

Read the spec

The standard library

Generated from its own source by kitec doc, because the library is written in Kite too.

Read the reference

Platform research

What Wasm can and cannot do in 2026, with sources.

Read the research

Concurrency

The async model and the Share marker.

Read the design

Compiler architecture

Crates, IRs, and the WasmGC lowering.

Read the architecture