Zena: A new Wasm-first programming language

In a pretty big departure from what I usually work on, I've been building a new programming language recently that I want to start talking about more publicly, called Zena.

Zena is a statically typed, object-oriented, and functional language designed from scratch to compile directly to WebAssembly GC. It has a familiar, TypeScript-inspired syntax, but with strict static semantics designed to produce tiny, fast Wasm binaries. On top of that foundation, Zena incorporates modern ideas from languages like Dart, Swift, Scala, and Rust, alongside relatively novel systems for resource ownership, asynchronous cancellation, iteration, and native WebAssembly Component Model integration.

Zena is not finished, not at a 1.0, or even a beta, and not even usable yet. Today I'm just giving the first detailed overview of the project and previewing the work-in-progress website at zena-lang.dev to show people what I've been up to and hopefully pique the interest of anyone who likes the idea and may want to get involved.

A quick taste of Zena

Zena should feel instantly legible to anyone who knows TypeScript:

let maxRetries: i32 = 3;   // immutable variable
var attempts = 0;          // mutable variable

class Counter {
  #step: i32;              // Fields are immutable by default
  var count: i32 = 0;

  new(this.#step);         // Dart-like constructors: non-nullable and
                           // immutable fields must be initialized
                           // before constructor bodies run.
  increment() {
    this.count += this.#step;
  }
}

sealed class Shape {       // Sealed classes for algebraic data types
  case Circle(radius: f64) // Concise case classes
  case Rect(width: f64, height: f64)
}

// Pattern matching expressions with exhaustiveness checking
let area = (shape: Shape): f64 => match (shape) {
  case Circle {radius}: 3.14159 * radius ** 2
  case Rect {width, height}: width * height
};

// Pipelines for readable, left-to-right data transformations
let formatted = "  hello world  "
  |> trim($)
  |> toUpperCase($);

While the syntax looks mostly familiar, some things are already different from JavaScript and TypeScript:

  • Sound type system: Declared and inferred types are strictly accurate everywhere. There is no any type, no unchecked casts, and no implicit type coercion. "1" + 1 and if (1) are compile errors.
  • Real primitives: i32, i64, u32, u64, f32, f64, boolean, and v128 (128-bit SIMD). Primitives are unboxed and always use direct Wasm arithmetic instructions.
  • Immutability by default: let is immutable; var is mutable. Class fields are immutable by default. Records ({x: 1, y: 2}) and tuples ((10, true)) are shallowly immutable.
  • Nominal classes & structural records: Classes, interfaces, and mixins are nominally typed. Records and tuples are structurally typed.
  • Constructor soundness: All member initializers run before constructor bodies, ensuring a partially initialized this reference can never escape.
  • Expression-oriented control flow: if, match, try, throw, return, continue, and break are expressions.
  • Pattern matching: switch is replaced by match() with exhaustive pattern matching over records, tuples, classes, arrays, and more. Pattern matching also works in if and while.
  • Sealed classes and ADTs: Sealed classes have a closed hierarchy, so they can participate in exhaustive pattern matching.

Why a language for WebAssembly GC?

I've been using Wasm a lot more recently, and as I've dug into the ecosystem I've been frustrated with the current language and toolchain options for it.

Some of the issues I've seen include:

  • Dynamic languages produce massive and slow Wasm binaries. We're talking 10s of megabytes for "Hello World" in JavaScript or Python.
  • Rust is well supported, and produces more reasonable binary sizes, but compile times are very slow, and I don't particularly love Rust. I like GC languages and they're better for integrating with JS and the DOM.
  • Toolchains are complicated and usually involve stringing together several separate tools to produce a usable Wasm binary - either to emit JS glue, create WASI components, or consume WASI component definitions.
  • AssemblyScript seems promising, but is missing critical features, isn't moving fast as a project, and is tied to partial TypeScript compatibility. The lead maintainer has a major disagreement with the WASI project and pulled support, and WASI component support is important to me. The AssemblyScript team also doesn't seem eager to move to Wasm GC, so they're still shipping their own bundled garbage collector.
  • There aren't great options for Wasm GC (though Scala and Kotlin seem to be making good progress). Dart uses Wasm GC and the team helped with the design and testing, but...
  • Dart currently only runs in JS hosts and is not quite the language I want, though it is fairly close. (Dart would likely be different if it never compiled to JS, and I want to explore that space.)
  • Even static languages produce larger binaries than I want. I want binaries to rival hand-written WAT (Wasm's text format).

Among the existing mainstream languages like TypeScript, Rust, Python, Java, C#, Dart, Swift, Scala, Kotlin, Ruby, Zig, C/C++, even Haskell or OCaml, none have the combination of ahead-of-time compilation, static and sound types, garbage collection, TypeScript-like ergonomics, modern language features, and a high level of WebAssembly orientation and optimization that I personally want.

So I figured that a new language that's both familiar to TypeScript developers, but not beholden to backward compatibility with it, with a design tailored to help emit ideal Wasm binaries, and specifically using Wasm GC for ease of use and great JS and DOM interop, could serve a real need out there.

Thus Zena, which targets WebAssembly first, and possibly only. Every design decision in the language is evaluated by how cleanly and efficiently it maps to Wasm.

Zena's goals

Small WebAssembly binaries

Small module size is important for opening up Wasm to a lot of interesting widespread use cases. Zena is designed from the start to produce very small Wasm output. This includes changing the language semantics and standard library when we have to, to more closely align with WebAssembly.

Zena steers clear of all the dynamism of JavaScript: objects have fixed keys, use classical inheritance, and don't have prototypes that can change. The Zena compiler can generate simple Wasm GC code, with direct struct.get instructions.

The simplest complete Zena program is probably one which just exports a main() that returns a number:

export function main() {
  return 1;
}

which compiles to this Wasm text format, for a binary of only 37 bytes:

(module
  (type (func (result i32)))
  (export "main" (func 0))
  (func (type 0) (result i32)
    i32.const 1
  )
)

A "Hello World" program like:

export function main() {
  return "hello";
}

currently compiles to 351 bytes, most of which are exported functions for reading individual String bytes from the host.

Performance

Besides small Wasm binaries, it's Zena's goal to produce fast Wasm, which it does by closely aligning with Wasm where possible.

Zena's primitives line up with Wasm's. Zena has real numeric types like i32 and f64 directly from Wasm. Like in Wasm, primitives are separate from references and there is no top type above them. Primitives are not objects, there's no auto-boxing, and no need for optimizations that infer more narrow machine types from broad number types, so Zena emits plain Wasm operators for math with no runtime checks.

Zena has two core, low-level array types: FixedArray<T> and ImmutableArray<T>, which are just Wasm GC's (array (mut T)) and (array T) — no wrapper objects. Indexed access is just Wasm's array.get, with no indirection or extra bounds checks.

Non-nullable types, sound class initialization, and immutability by default combine to allow the Zena compiler to emit non-mutable, non-nullable Wasm references, helping engines more easily emit fast machine code with fewer runtime checks.

Some object-oriented language features, especially polymorphism, do have runtime overhead, but they're so convenient and expected that I had to include them. The Zena compiler, even in its early state, is built to optimize those away when possible. And when OO overhead can't be compiled away, many modern Wasm VMs (like V8) will further bring their JIT to bear to optimize the code at runtime.

First-class WebAssembly, WASI, and WASI Component support

Small and fast are great, but I also want WebAssembly to be a first-class target of the Zena toolchain. Zena should be the easiest language to use for Wasm and from Wasm. This means the compiler directly emits Wasm (both binary and text formats), and is itself a small, fast Wasm binary — including its optimizations, LSP, and formatter — and will soon support emitting and importing WASI components without any bindings generator or external tools.

Correctness and AI-friendliness

It's debatable whether humans will be writing most of the code in the future. We might already not be! So any new language has to contend with how well coding agents will read and write it.

And even before the agentic coding era, languages have been heading in the direction of less dynamism and more static checks so that humans can understand and manage source code better.

Zena leans into these areas by biasing features towards correct-by-default, and familiarity.

As much as possible, I want it to be impossible to represent incorrect states, for types to be wrong, for resources to be unreleased, for tasks to be orphaned, etc., even when that means a little bit more strictness and pedantry. This helps humans and AI write correct code.

For familiarity, I try to construct Zena the way I talk about it: start with TypeScript syntax, fix well-known issues, add Dart-style constructors, Swift-style strings, Scala-style sealed classes, etc. Again, humans and AI both benefit: they can get a quick idea of how to write Zena code from a compact description.

More than a static TypeScript

Compiling to Wasm GC and addressing JavaScript's historical quirks was just the starting point for Zena. As Zena took shape, and as I wrote (or prompted) more and more Zena code, I kept adding useful features that brought it beyond a minor TypeScript fork.

1. Sealed classes & exhaustive pattern matching

Zena embraces sum types (algebraic data types, aka tagged enums), but integrates them into the existing object-oriented class system via sealed class hierarchies:

sealed class JsonValue {
  case JsonNull
  case JsonBool(value: boolean)
  case JsonNumber(value: f64)
  case JsonString(value: String)
  case JsonArray(items: Array<JsonValue>)
  case JsonObject(entries: Map<String, JsonValue>)
}

A sealed class is implicitly abstract and defines a closed set of subclasses. Subclasses can carry data (like JsonNumber) or be parameterless unit variants (like JsonNull). Case classes automatically receive structural equality (operator ==) and hash codes.

Because the hierarchy is closed, the compiler enforces exhaustiveness in match expressions:

let stringify = (json: JsonValue): String => match (json) {
  case JsonNull: "null"
  case JsonBool {value}: if (value) "true" else "false"
  case JsonNumber {value}: `${value}`
  case JsonString {value}: `"${value}"`
  case JsonArray {items}: `[${items.map(stringify).join(', ')}]`
  case JsonObject {entries}: `{...}`
};

If you miss a case, the compiler emits an error.

2. Affine resources & ownership

Zena is a garbage-collected language, but not everything a program manages is memory that a GC can automatically collect.

WASI file descriptors, WASI Component Model resource handles, linear-memory allocations, and other foreign objects must often be released deterministically when no longer used.

Wasm GC has no finalizers to run cleanup code when an object is collected. The usual solution is a manual dispose protocol that programmers have to remember to call. More automated solutions, like JavaScript's resource management with using, help a bit.

Zena includes using, but it goes further with an affine ownership system and resource classes for non-GC resources:

resource class FileDescriptor {
  #fd: i32;
  new(this.#fd);

  [Disposable.dispose](this: Own<this>): void {
    wasi_fd_close(this.#fd);
  }
}

Resource classes declare a type that holds an unmanaged resource. All references to resources must be wrapped with Own<T> (transferring ownership) or Borrow<T> (temporarily borrowing access) utility types. The compiler enforces linear move semantics on Own<T>, and Borrow<T> references are second-class and stack-bound—they cannot be stored in long-lived heap objects or unmanaged references. When an owned resource goes out of scope, its symbol-keyed [Disposable.dispose]() method is called automatically—even across early returns, thrown exceptions, or cancellation unwinds.

Zena's borrow checker is much more limited than Rust's, but its purpose is more limited because GC takes care of memory safety and management. And since borrows cannot outlive the stack frame that issued them, Zena completely avoids complex lifetime annotations ('a).

For ordinary classes that hold resources, Zena provides using let declarations for block-scoped deterministic disposal, matching modern standards while preserving static safety.

3. First-class async cancellation

Async cancellation is something that Zena tries to improve on by making it part of the language.

If you've written asynchronous code in JavaScript, Go, or C#, you know how tedious cancellation can be. Passing an AbortSignal, context.Context, or CancellationToken as a manual parameter through every function in your call stack, and calling AbortSignal.throwIfAborted() is a lot of boilerplate to remember:

// The tedious way: drilling cancellation tokens through every frame
async function fetchData(url: string, signal: AbortSignal) {
  const response = await fetch(url, { signal });
  const result1 = process1(response, signal);
  signal.throwIfAborted();
  return process2(result1, signal);
}

Not to mention that properly handling an abort is incredibly hard to wire up correctly.

Then there's the swallowing problem: when cancellation is an ordinary thrown exception (like JS's AbortError or Kotlin's CancellationException), a well-meaning catch (e: Exception) block anywhere in the call stack will accidentally swallow it, defying the caller's intent and keeping abandoned background work running.

Zena solves this by making cancellation a third language channel, distinct from both normal return values and thrown exceptions, and adding ambient cancellation scopes so that all async functions are automatically cancellable.

You never pass cancellation tokens through function signatures, and rarely need to manually check for cancellation. Cancellation is automatically delivered at suspension checkpoints (await) or manual checks (checkCancellation()) and from there unwinds the stack like an exception.

try expressions have a cancel clause specifically to handle cancellations:

try {
  await performLongOperation();
} catch (e) {
  // Runs ONLY on real application errors
  logError(e);
} cancel {
  // Runs ONLY when cancelled by an ancestor scope
  shielded {
    // Shielded blocks are not cancellable, for async cleanup:
    await notifyServerCancelled();
  }
} finally {
  // Runs on success, error, or cancellation
  cleanup();
}

Ordinary catch (e) blocks never observe cancellation. Cancellation unwinds the call stack cleanly, running cancel and finally handlers along the way. If cleanup logic itself needs to perform asynchronous operations during cancellation, the shielded block temporarily suppresses cancellation so cleanup is guaranteed to finish without being cut short.

This design is heavily inspired by Python's Trio library and Nathaniel J. Smith's fantastic essay, Timeouts and cancellation for humans. Bringing those structured concurrency concepts directly into language-level syntax makes async code dramatically more robust.

4. Native WIT & Component Model Integration

The WebAssembly Component Model and WebAssembly Interface Types (WIT) represent the future of composable, language-agnostic software modules. But today, using WIT in almost any language requires multi-step build pipelines involving external tools like wit-bindgen to generate sprawling glue code.

Zena treats WIT as a first-class language citizen. The Zena compiler includes a built-in WIT parser (written in pure Zena, passing the complete canonical wasm-tools test suite).

In Zena, you can import a WIT file directly in your source code:

from './kv-store.wit' import { KeyValueStore };

export function handleRequest(store: KeyValueStore) {
  store.set("key", "value");
}

The compiler parses the WIT definitions during compilation, type-checks against them natively, and synthesizes the required Canonical ABI lifting and lowering adapters directly into the emitted Wasm module. The WIT interfaces use Zena's affine resource class support so that WIT resources are automatically released.

Built with AI

I've wanted to build a language like Zena for a long time. But building a production-grade programming language—including parser, type checker, IR, optimizer, code generator, test runner, formatter, language server, and standard library—has traditionally required either a massive corporate team or years of single-minded sacrifice.

As an engineer with a busy life (and ADHD!), I didn't have the time or endurance to write every line of compiler infrastructure from scratch. That changed with the arrival of modern coding agents (specifically Gemini 3 Pro and Claude Opus).

There is a fantastic Brian Eno quote about computer sequencers that has guided my thinking throughout this project:

"The great benefit of computer sequencers is that they remove the issue of skill, and replace it with the issue of judgement.

With Cubase or Photoshop, anybody can actually do anything...

So the question becomes not whether you can do it or not, because any drudge can do it if they’re prepared to sit in front of the computer for a few days, the question then is, 'Of all the things you can now do, which do you choose to do?'"

Like sequencers were to music production, AI drastically lowers the barriers of pure implementation skill, time, and budgets, leaving you with judgment.

Even on judgment, frontier AI is an incredible partner in research and design. LLMs have been trained on (or can search) all of the programming languages on the web including their implementations, and all of the academic books and papers on language and compiler design. Even while it's quite fallible, AI can teach you a lot, make you aware of even more, and lead you to sources from which you can dig even deeper.

So Zena is built almost entirely via coding agent prompts, and design iteration has happened in "conversations" and over design docs written by AI. But every language design decision was made by me, and by this point in the project most of the architecture was suggested or guided by me, and much of the code (definitely not every line though) has been reviewed by me.

There is still typical AI-produced tech debt in the codebase. It was even borderline slop at the beginning of the project. Early on, when trying to implement the iterator protocol, the agents began failing hard. The architecture was a mess, riddled with redundant type-checking passes, messy AST mutations, and subtle name-based collision bugs. AI couldn't fix it; in fact, it kept generating more debt and horrific workarounds. I had to pause, learn a lot more about compiler architecture myself, and carefully guide the agents step-by-step through a complete architectural refactor.

Even after that, I'm sure that many problems have crept in with recent sprints. I'm sometimes consciously trading quality for velocity at this time, trying to get a more complete vision out there rather than production-ready code. The most important part is a language design that makes sense and can be optimized well and compiled quickly. The implementation can be fixed over time.

Working on Zena has shown me that, at least for now, AI does not replace human judgment. It augments and amplifies it. It allows a solo developer to learn, explore ideas, and make decisions at a speed that would have been unimaginable just a few years ago. And it makes tackling large and ambitious projects much more feasible.

Current status & what's next

Zena is under intense active development, but it has already reached some exciting milestones:

  • Self-Hosted Compiler: The Zena compiler is written in Zena and compiles to WebAssembly using its custom ZIR (CFG/SSA) optimization pipeline.
  • Async functions and generators: The control-flow graph IR unlocked program transforms like the CSP transform used to implement stackless async functions and generators.
  • Unified Toolchain: The zena CLI includes a native compiler, test runner, code formatter, and Language Server (LSP).
  • Online Playground: You can try Zena directly in your browser with our in-browser Wasm compiler and interactive playground at zena-lang.dev/playground.
  • Component Model & WIT: The WIT parser is complete, and direct WIT-typed module compilation is landing.

I'm actively working on finishing async cancellation and iteration, the affine borrow checker, and core optimization passes; expanding value types to records and classes; and reorganizing the standard library.

After that is even more interesting work like workers, macros and decorators, numeric units and dimensions, JSX and/or builder syntax, contracts, and possibly even formal verification or a GPU backend.

Zena is still experimental. APIs change frequently, and you shouldn't run your bank or even your hobby website on it yet. But if you're interested in programming language design, WebAssembly, or things like component-driven systems or sandboxed AI code generation, I'd love for you to check out the GitHub repository, and join the discussions on both GitHub Discussions and the #zena channel on the WebAssembly Discord.

likes on Bluesky