Wado logo

The Wado Programming Language Wado

Type-safe, high-level WebAssembly.

Type-safe like Rust, familiar like TypeScript, compiled to clean Wasm. Wasm in plain sight.

Language Spec Cheatsheet


A First Look

#!/usr/bin/env wado run
// Hello World in Wado

use { println, Stdout } from "core:cli";

export fn run() with Stdout {
    println("Hello, world!");
}

A few lines, but a lot is on display: a shebang that makes the file directly executable as a one-file script, ES module–style imports from a namespaced standard library (core:cli), an export keyword that declares the entrypoint of the wasi:cli/command world, and a with Stdout clause that makes the side effect part of the function's type.

wado run hello.wado

The compiled .wasm for this program is 1,773 bytes — smaller than the same program written in C, Zig, Moonbit, or Rust.


Why Wado?

Existing Wasm-targeting languages bundle their own memory management runtime into every .wasm file. The result is bloated binaries even for trivial programs:

Program Wado Rust (wasm32-wasip1)
hello_world 1,773 B 40,115 B
pi_approx 8,982 B 59,496 B
zlib (gzip) 18,935 B 88,563 B
sqlite_highlight 622 KB 3,481 KB

Wado takes a different approach. By targeting Wasm GC, the garbage collector is provided by the host runtime itself — so it doesn't have to ship inside the module. The output is a thin shell of pure logic.

The timing matters too. With the Wasm Component Model and WASI P3 maturing, Wado is designed for this new era from the ground up — no legacy baggage, no retrofitting.


A Real Example: an HTTP Service

This section tours example/http-bin.wado — a single-file httpbin clone implementing /get, /post, /headers, /status/:code, /base64/:val, and more, served via the Component Model wasi:http/service world.

Run it:

wado serve example/http-bin.wado
# in another terminal:
curl http://localhost:8080/get

Eleven snippets follow, each highlighting something Wado does well.

1. One-file scripts, with the standard library and WASI in scope

A Wado file can be made directly executable with a shebang — one-file scripts are first-class:

#!/usr/bin/env wado serve

The stdlib is namespaced (core:* for the language runtime, wasi:* for WASI worlds) and imported the way ES modules are:

use { Request, Response, Method, Scheme, ErrorCode } from "wasi:http";
use { MonotonicClock } from "wasi:clocks";
use { TreeMap } from "core:collections";
use { decode } from "core:base64";
use { Serialize } from "core:serde";
use { Url } from "core:url";
use json from "core:json";

Notice that wasi:http and wasi:clocks are imported the same way as core:json — WASI worlds are simply modules.

2. Effects belong in the function type

export async fn handle(request: Request) -> Result<Response, ErrorCode>
    with MonotonicClock {

The with clause lists the capabilities this function uses. MonotonicClock here is a WASI capability, not a global; the runtime grants it on entry. Pure functions have no with clause, and you can tell at a glance.

async fn declares a Component Model–level async function, which the host can suspend and resume across the canonical ABI.

3. Auto-derived JSON, one line

struct RequestInfo {
    method: String,
    path: String,
    args: TreeMap<String, String>,
    headers: TreeMap<String, String>,
    origin: String,
    url: String,
    data: Option<String>,
}
impl Serialize for RequestInfo;

The single line impl Serialize for RequestInfo; derives the serializer. There is no separate macro and no schema file — the struct's field types are the schema.

4. Exhaustive pattern matching over variants

fn scheme_name(s: Option<Scheme>) -> String {
    return match s {
        Some(Http)        => "http",
        Some(Https)       => "https",
        Some(Other(name)) => name,
        None              => "http",
    };
}

Scheme is a variant (a sum type with payloads) imported straight from wasi:http. The compiler verifies that every case is covered, including the nested Other(name) payload that carries a custom scheme string.

5. Declarative naming for the wire format

#[serde(rename_all = "kebab-case")]
struct UserAgentResponse {
    user_agent: Option<String>,
}
impl Serialize for UserAgentResponse;

The Rust-style attribute renames user_agent to user-agent on the wire. Idiomatic Wado naming on the inside, idiomatic JSON on the outside.

6. match as an expression, even on strings

match path {
    "/"            => { content_type = "text/html; charset=utf-8"; body_str = HOMEPAGE; },
    "/get"         => { /* ... */ },
    "/post"        => { /* ... */ },
    "/headers"     => { /* ... */ },
    "/ip"          => { /* ... */ },
    "/user-agent"  => { /* ... */ },
    _              => { /* dynamic routes: /status/:code, /base64/:val, ... */ },
}

match is an expression and works on string literals as well as variants. The wildcard arm dispatches the dynamic routes through a separate function.

7. Template strings with format specifiers

trailers.append(
    "server-timing",
    `app;dur={elapsed_ms:0.3f}`.as_bytes() as FieldValue,
);

Backtick strings interpolate expressions like TypeScript, but the :0.3f format specifier is checked at compile time against the value's Display impl. The result is app;dur=12.345.

8. Tuples are first-class return values

fn echo_if_method(/* ... */) -> [StatusCode, String, Option<String>] {
    /* ... */
    return [200, build_request_json(/* ... */), null];
}

let [s, b, a] = echo_if_method(request, method, Method::Get, "GET", false, url, path, args);

Tuple literal syntax ([a, b, c]) is the same in types, expressions, and patterns. There is no third syntax for "multiple return values" — there's just tuples and destructuring.

9. task return: hand off the response, then keep working

task return Result::Ok(response);

body_tx.write(body_str.as_bytes());
body_tx.drop();

In a Component Model async fn, task return delivers the result to the caller without ending the function. The handler can then continue streaming the body, write trailers, and emit access logs — all after the response status is already on the wire.

10. Compile-time file inclusion

global HOMEPAGE: String = #include_str("./http-bin-index.html");

The HTML for / lives in a separate file for editing comfort, but ends up baked into the .wasm as a constant. There is no filesystem read at runtime, and no separate asset to ship.

11. if let with guards

if let Some(a) = request.get_authority() && !a.is_empty() {
    return a;
}

Pattern binding and a boolean guard combine in a single condition. The bound name a is in scope for both the guard and the body.

The full source is at example/http-bin.wado (~345 lines).


Design Principles

Wasm in plain sight

No macros. The code you read is the code that runs. This makes Wado predictable to read, predictable to debug, and predictable to optimize — the WAT output is something you can reason about by looking at the source.

Readable without context switching

Explicit over implicit. No implicit type conversions, no function overloading, no hidden dependencies. You shouldn't need to jump to other files to understand what a function does.

Type-safe by design

Strong static typing with no escape hatches like any. This prevents the defensive programming patterns (excessive try-catch, runtime type checks) that tend to creep into dynamically-typed codebases.

No exception unwinding

Errors are values: Result<T, E> and Option<T>. Control flow is local and visible. Stack unwinding doesn't exist as a language feature.

Minimal binary size

By leveraging Wasm GC instead of bundling a runtime, Wado produces compact .wasm files. This is the core motivation of the language.


Designed for Agentic Coding

The Wado compiler is 100% agentic-coded — every line of wado-compiler was written by an AI agent — and the same is true of most of the Wado code in the repository. The human handles language design and project management; the agents do the rest.

This isn't just a curiosity; it shaped the language itself:

The result is a language where common agentic-coding pitfalls are eliminated by design, not by convention.


Try It in 60 Seconds

Grab the latest prebuilt binary and verify it against the published checksums. The archive name is derived from uname, so this block works as-is on macOS (Apple Silicon) and Linux (x86_64, aarch64) — copy the whole thing:

BASE=https://github.com/wado-lang/wado/releases/latest/download
ASSET="wado-$(uname -s | tr A-Z a-z)-$(uname -m).tar.gz"

curl -fsSLO "$BASE/$ASSET"

if command -v shasum >/dev/null; then
  curl -fsSL "$BASE/SHA256SUMS.txt" | shasum -a 256 --ignore-missing -c -
else
  curl -fsSL "$BASE/SHA256SUMS.txt" | sha256sum --ignore-missing -c -
fi

tar xzf "$ASSET"
mkdir -p ~/bin
install -m 755 "${ASSET%.tar.gz}/wado" ~/bin/wado

This installs wado into ~/bin — make sure that directory is on your PATH.

Windows builds (x86_64, aarch64) are on the latest release page, alongside the same SHA256SUMS.txt for verification.

Then write a hello-world program and run it:

cat > hello.wado <<'EOF'
#!/usr/bin/env wado run
use { println, Stdout } from "core:cli";

export fn run() with Stdout {
    println("Hello, world!");
}
EOF

wado run hello.wado

Prefer to build from source? With a Rust toolchain installed, cargo install --git https://github.com/wado-lang/wado wado-cli builds the current main branch.

For an editor experience, the repo also includes a VS Code extension under wado-vscode/.


Status & Roadmap

Wado is experimental. The core language — syntax, static typing, generics, closures, modules, traits, pattern matching, the effect system — is implemented and functional. It is already usable for its original purpose: embedding small, type-safe Wasm modules into JavaScript projects where binary size matters.

That said, Wado's design points further into the future. Three things in the broader Wasm ecosystem need to land before Wado can be what it's meant to be:

When these land, Wado's era begins.


Resources