Kite draft

A small, explicit language for application software

WebAssembly is the primary target, not an afterthought. Errors are values and the compiler enforces it. There is one concurrency concept. Nothing hides control flow.

fn main() {
    let (cfg, err) = config.load("app.toml")
    check err

    ui.run(App{ title: cfg.title })
}

Run Kite in your browser — the playground is the compiler itself, built for WebAssembly. Nothing there 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.

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.

fn load_user(id: int) -> (User, error) {
    let (raw, err) = db.query("SELECT ...", id)
    // `raw` cannot be read yet: on the failure path there is no value at all.
    check err
    return parse_user(raw)
}

Reading raw before the check is E0301. Letting err go out of scope unexamined is E0302.

Concurrency is one concept

Some operations take time: mark them async and await them. Calling an async fn starts it and yields a Task<T>, which is how two things happen at once. 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,
    }
}

fn main() {
    io.print(area(Shape.Rect(width: 2.0, height: 3.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.

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