On this page

Quick Reference

Zena is a statically typed, object-oriented and functional programming language targeting WebAssembly GC. It combines TypeScript-like syntax with a sound type system and low- or zero-cost abstractions.

Quick Start

zena
// hello.zena
export function main() {
  return 42;
}

Note: Zena is not yet released. The CLI examples below assume you've built the Zena tools locally from the GitHub repository.

bash
# Compile for host environment (JS)
zena build hello.zena -o hello.wasm --target host

# Compile for WASI
zena build hello.zena -o hello.wasm --target wasi

# Run with wasmtime
wasmtime run -W gc,function-references,exceptions --invoke main hello.wasm

Basic Syntax

Comments

Zena uses C-style comments: // for single-line and /* */ for multi-line. JSDoc-style comments (/** */) are recommended for documenting public APIs.

zena
// Single-line comment

/* Multi-line
   comment */

/** JSDoc-style comment (recommended for public APIs) */

Identifiers

Identifiers name variables, functions, classes, and other entities.

  • Must start with a letter, _, or $
  • Can contain letters, digits, _, or $
  • Case-sensitive

Semicolons

Semicolons are required after most statements. They're optional after block-ended constructs (if, match, try) when used as standalone statements.

zena
let x = 1;                    // Required
if (x > 0) {
  /* ... */
}                             // Optional
let y = if (x > 0) 1 else 2;  // Required (expression context)

Modules

Every Zena source file is a module. Modules provide namespacing and control visibility—top-level declarations are private by default.

Imports

Use import to bring declarations from other modules into scope:

zena
// Named imports
import {Map, Set} from 'zena:collections';

// Renamed import
import {StringBuilder as SB} from 'zena:string-builder';

// Namespace import
import * as regex from 'zena:regex';
regex.match(pattern, text);

// Alternative syntax (from ... import)
from 'zena:string' import {String};

Namespace imports (import * as x) define a read-only variable x whose type is a structural RecordType containing all of the value exports of the imported module as properties. They behave as standard record values and can be passed to functions, returned, stored, or destructured.

Exports

Mark declarations with export to make them available to other modules and the host environment:

zena
export let add = (a: i32, b: i32) => a + b;

export class Point {
  x: i32;
  y: i32;
  new(x: i32, y: i32) {
    this.x = x;
    this.y = y;
  }
}

// Private (not exported)
let helper = (x: i32) => x * 2;

Variables

Zena uses let and var to declare variables. Both are block-scoped and can appear at module level or in nested scopes. Variables can be shadowed in inner scopes but cannot be redeclared in the same scope. Unlike JavaScript, variables cannot be referenced before their declaration.

zena
let x = 1; // Immutable binding (like const in JS)
var y = 1; // Mutable binding
y = 2; // OK
x = 2; // ❌ Error: cannot reassign immutable binding

Type Annotations

Type annotations are optional—the compiler infers types from initializers. Literals are widened to their base type: integer literals become i32, float literals become f32, and string literals become String (not literal types like 1 or "hello").

zena
let x: i32 = 1; // Explicit type
let y = 1; // Inferred as i32
let s: String = 'hello'; // Explicit String type

Primitive Types

Zena's primitive types map directly to WebAssembly value types, with no boxing overhead. Integer and float literals default to i32 and f32 respectively; use as to convert to other numeric types.

Type WASM Type Description
i32 i32 32-bit signed integer (default for integer literals)
i64 i64 64-bit signed integer
u32 i32 32-bit unsigned (uses unsigned WASM operators)
u64 i64 64-bit unsigned (uses unsigned WASM operators)
u8 u16 i32 Narrow unsigned; promote to u32 in any operation
i8 i16 i32 Narrow signed; promote to i32 in any operation
f32 f32 32-bit float (default for float literals)
f64 f64 64-bit float
boolean i32 true or false
v128 v128 SIMD vector; no literals, operators or casts
String (ref $String) Immutable Unicode string
anyref anyref Top type for reference types only
never Bottom type (e.g., result of throw)

Note: There is no any type, and no implicit boxing. anyref accepts references — objects, arrays, strings, functions, null — and rejects primitives, so allocation is always visible in the source: put a primitive behind a reference with new Box<T>(x).

zena
let i: i32 = 42;
let n: i64 = 100 as i64;
let u: u32 = 255;
let f: f32 = 3.14;
let d: f64 = 3.14 as f64;
let b: boolean = true;
let s: String = 'hello';

// Narrow integers describe exact storage widths. A literal must fit,
// and arithmetic promotes to the 32-bit counterpart.
let byte: u8 = 255;
let sum: u32 = byte + 1;      // 256 — u8 does not survive the addition
let back: u8 = sum as u8;     // 0 — narrowing is explicit and truncates

Built-in Types (Prelude)

The following types are automatically available in every Zena module—no import needed. They come from the prelude, which is implicitly imported.

Type Description
String Immutable Unicode string
Error Base class for all errors (thrown with throw)
IndexOutOfBoundsError Thrown on invalid array/string index access
Option<T> Represents an optional value (Some<T> or None)
Some<T> Variant of Option containing a value
None Variant of Option representing no value
Array<T> Universal read-only array interface (length, [i], map)
MutableArray<T> Mutable array interface ([i] = v)
GrowableArray<T> Resizable array (growable([1, 2, 3]), push)
FixedArray<T> Fixed-size mutable array (fixed([1, 2, 3]))
ImmutableArray<T> Immutable array — what a [1, 2, 3] literal is
Map<K, V> Hash map (literal syntax: {"a" => 1})
Box<T> Wraps a primitive for use in a union or anyref
BoundedRange Range with start and end (1..10)
FromRange Range with start only (5..)
ToRange Range with end only (..10)
FullRange Unbounded range (..)
console Console output (console.log(...))

Helper functions some(value) and none() are also available for creating Option values.

Functions

Functions are first-class values that can be assigned to variables, passed as arguments, and returned from other functions. They come in two forms: arrow functions, which are expressions, and function declarations, which are top-level statements.

zena
// Expression body
let add = (a: i32, b: i32) => a + b;

// Block body
let greet = (name: String): String => {
  return 'Hello, ' + name;
};

// No parameters
let getAnswer = () => 42;

Function Declarations

A function declaration names a function at the top level of a module. It is hoisted, so forward references and mutual recursion just work.

zena
export function main(): i32 {
  return isEven(10);   // declared below
}

function isEven(n: i32): i32 {
  if (n == 0) { return 1; }
  return isOdd(n - 1);
}

function isOdd(n: i32): i32 {
  if (n == 0) { return 0; }
  return isEven(n - 1);
}

The difference from an arrow function is closures: a function can never be one. It may only appear at the top level, so its body sees only its own parameters and locals plus module-level bindings — never a variable from an enclosing scope. When you need to capture, use an arrow function.

zena
let makeAdder = (x: i32) => (y: i32) => x + y;   // arrows capture

function bad(y: i32): i32 {
  return y + x;   // there is no enclosing `x` to capture
}

gen and async work the same as on arrows: gen function items(): Iterator<i32>, async function load(): Future<i32>. Type parameters go after the name: function identity<T>(x: T): T.

Generic Functions

Generic functions work with multiple types while preserving type safety. Type parameters are specified in angle brackets before the parameter list.

zena
let identity = <T>(x: T): T => x;

let num = identity<i32>(42); // Explicit type argument
let str = identity('hello'); // Inferred

Constrained Generics

Type parameters can be constrained using extends to require certain capabilities.

zena
let print = <T extends Printable>(x: T): void => {
  // Can call methods from Printable
};

Optional Parameters

Parameters marked with ? are optional. For reference types, the parameter becomes T? (T | null). Primitive types cannot be optional without a default value (since they can't be null).

zena
// Reference types can be optional (become T?)
let greet = (name: String, greeting?: String) => {
  if (greeting == null) {
    return 'Hello, ' + name;
  }
  return greeting + ', ' + name;
};

greet('Alice'); // "Hello, Alice"
greet('Bob', 'Hi'); // "Hi, Bob"

Default Parameters

Parameters can have default values. Unlike Python, default expressions are evaluated fresh at each call site—not shared between calls. Default values are used when the argument is omitted entirely—there's no sentinel value like null or undefined that triggers defaults.

zena
let increment = (x: i32, amount: i32 = 1) => x + amount;

increment(10); // 11 (amount defaults to 1)
increment(10, 5); // 15 (amount is 5)

Argument Adaptation

Functions with fewer parameters can be passed where more are expected:

zena
let map = (fn: (item: i32, index: i32) => i32) => { ... };

// Pass a function that ignores `index`
map((item) => item * 2);

Closures

Functions are closures—they capture variables from their enclosing scope. Captured variables are stored in a heap-allocated environment.

zena
let makeAdder = (x: i32) => {
  return (y: i32) => x + y;
};

let add5 = makeAdder(5);
add5(10); // 15

Tail Calls

tail return f(x); compiles the call to WebAssembly's return_call: the frame is discarded before the callee runs, so a chain of tail calls executes in constant stack space.

zena
function sum(n: i32, acc: i32 = 0): i32 {
  if (n == 0) { return acc; }
  tail return sum(n - 1, acc + n);
}

sum(1000000);  // no stack overflow

The annotation is required. A call in tail position without it compiles to an ordinary call, keeping its frame and its line in every backtrace. tail is a contextual keyword and stays available as an identifier.

The call must be the whole operand and must return exactly the enclosing function's declared return type. tail return is rejected in an async function, a generator or a constructor, and inside a try or while a using binding is live — in each case the frame still has work to do.

Multi-Value Returns

Functions can return multiple values using unboxed tuples. Unlike regular tuples (which are heap-allocated), multi-value returns compile directly to WASM's multi-value return feature—values go on the stack, not the heap. This guarantees good performance since stack values are prime candidates for register allocation.

zena
let divide = (a: i32, b: i32): (i32, i32) => {
  return (a / b, a % b);
};

let (quot, rem) = divide(17, 5);  // quot=3, rem=2

Multi-value returns must be immediately destructured at the call site—you cannot store them in a variable.

Use Case: Iterators

The Iterator interface uses multi-value returns for efficient iteration:

zena
interface Iterator<T> {
  next(): (T, true) | (never, false);  // (value, hasValue)
}

This avoids the two-call pattern common in Java (hasNext() then next()) and the wrapper-object pattern in JavaScript (returning {done, value}). A single call returns both the status and the value with zero allocation overhead.

Use Case: Map Lookups

Map.get() uses multi-value returns to safely handle missing keys without relying on null:

zena
let map = {"a" => 42, "b" => 100};

let (value, found) = map.get('a');  // (42, true)
let (_, found2) = map.get('c');     // (_, false)

This is especially important for primitive value types like i32 where null isn't an option. Instead of returning i32 | null (which would require boxing), the boolean found flag indicates whether the lookup succeeded, and when found is false, the _ identifier is used to return a never value.

Operators & Expressions

Zena provides familiar operators from C-family languages. Operators are strictly typed—you cannot mix i32 and u32 without explicit casting.

Arithmetic

zena
let a = 1 + 2; // Addition
let b = 5 - 3; // Subtraction
let c = 2 * 3; // Multiplication
let d = 10 / 3; // Division (float: 3.333...)
let e = 10 % 3; // Modulo (1)
let f = 2 ** 10; // Exponentiation (1024)

Compound Assignment

zena
var x = 10;
x += 5; // x is now 15
x -= 3; // x is now 12
x *= 2; // x is now 24
x /= 4; // x is now 6
x %= 4; // x is now 2
x **= 3; // x is now 8

Nullish assignment assigns only when the left side is null:

zena
var name: String? = null;
name ??= 'Anonymous'; // name is now 'Anonymous'

Compound assignment works with variables, class fields, and array indices. There are no ++ or -- operators — use += 1 and -= 1 instead.

Comparison

Equality (==) uses value comparison for primitives and strings, and reference comparison for objects (unless operator == is defined). Use === to always compare by reference.

zena
a == b; // Equal (value equality for strings)
a != b; // Not equal
a === b; // Strict equal (reference equality)
a !== b; // Strict not equal
a < b; // Less than
a <= b; // Less than or equal
a > b; // Greater than
a >= b; // Greater than or equal

Logical

zena
a && b; // Logical AND (short-circuit)
a || b; // Logical OR (short-circuit)
!a; // Logical NOT

Nullish Coalescing

The ?? operator returns the right operand when the left is null:

zena
let name: String? = null;
let display = name ?? 'Anonymous'; // 'Anonymous'

?? has the same precedence as ||.

Optional Chaining

Safe access on potentially null values. Short-circuits to null if the receiver is null.

zena
let name = user?.name;       // Property access
let first = items?[0];       // Index access
let result = callback?(42);  // Call
let display = user?.name ?? 'Anonymous'; // With fallback

Bitwise

zena
a & b; // AND
a | b; // OR
a ^ b; // XOR
~a; // NOT
a << b; // Left shift
a >> b; // Right shift (signed)
a >>> b; // Right shift (unsigned)

Type Operators

The as operator casts between types (checked at runtime for reference types). The is operator tests types and enables type narrowing.

zena
x as i64      // Type cast (checked at runtime)
x is MyClass  // Type check (returns boolean)

Range Operators

Ranges represent sequences of indices, primarily for array slicing and iteration. All ranges are half-open (exclusive end).

zena
1..10     // BoundedRange [1, 10)
5..       // FromRange (5 to end)
..10      // ToRange (start to 10)
..        // FullRange (all elements)

Pipeline Operator

The pipeline operator |> enables fluent data transformation by passing the result of one expression as input to the next. The placeholder $ refers to the piped value.

zena
// Without pipeline (inside-out)
let result = validate(transform(parse(data)));

// With pipeline (left-to-right)
let result = data |> parse($) |> transform($) |> validate($);

The $ placeholder can be used multiple times and in any position:

zena
10 |> $ + $               // 20 (use $ twice)
5 |> add($, 10)           // 15 ($ as first arg)
3 |> subtract(10, $)      // 7  ($ as second arg)

Pipelines can be chained and work with method calls:

zena
text |> $.trim() |> $.toUpperCase()
1 |> $ + 1 |> $ * 2 |> $ + 3  // ((1 + 1) * 2) + 3 = 7

$ is only valid inside pipeline expressions:

zena
let x = $; // ❌ Error: '$' can only be used inside a pipeline expression

Operator Precedence (highest to lowest)

  1. Unary: !, -, ~
  2. Multiplicative: *, /, %
  3. Additive: +, -
  4. Type cast/check: as, is
  5. Range: ..
  6. Shift: <<, >>, >>>
  7. Relational: <, <=, >, >=
  8. Equality: ==, !=, ===, !==
  9. Bitwise AND: &
  10. Bitwise XOR: ^
  11. Bitwise OR: |
  12. Logical AND: &&
  13. Logical OR / Nullish Coalescing: ||, ??
  14. Pipeline: |>
  15. Assignment: =, +=, -=, *=, /=, %=

Note: as currently binds looser than arithmetic, so a + b as i64 means (a + b) as i64. We may change this in the future to match other languages where as binds tightly.

Control Flow

Zena's control flow is expression-oriented—if and match can return values. This enables concise conditional expressions without ternary operators.

If Statement / Expression

Like Rust, if can be used as an expression. When used as an expression, the else branch is required and both branches must have compatible types.

zena
// Statement
if (x > 0) {
  // ...
} else if (x < 0) {
  // ...
} else {
  // ...
}

// Expression (else required)
let abs = if (x >= 0) x else -x;

While Loop

Standard while loop with a boolean condition.

zena
var i = 0;
while (i < 10) {
  i = i + 1;
}

For Loop (C-style)

Traditional C-style for loop with init, condition, and update expressions. Note: use var for the loop variable since it needs to be mutable.

zena
for (var i = 0; i < 10; i = i + 1) {
  // ...
}

For-In Loop

Iterate over any collection that implements the iteration protocol.

zena
let arr = [1, 2, 3];
for (let item in arr) {
  // item is 1, 2, 3
}

Break and Continue

zena
while (true) {
  if (done) break;
  if (skip) continue;
}

Let-Pattern Conditions

Combine pattern matching with conditionals using if let and while let. The condition succeeds if the pattern matches.

zena
// if-let
if (let Some(value) = maybeValue) {
  // value is bound here
}

// while-let
while (let (true, item) = iterator.next()) {
  // item is bound here
}

Pattern Matching

Zena's match expression provides exhaustive pattern matching. Unlike switch, match is an expression that returns a value, and the compiler ensures all cases are covered.

zena
let result = match (x) {
  case 0: "zero"
  case 1: "one"
  case let n if n < 0: "negative"
  case _: "other"
};

Pattern Types

Patterns can match literals, bind variables, destructure data structures, and combine with logical operators.

zena
// Literals
case 42: ...
case "hello": ...
case true: ...
case null: ...

// Binding (`let`/`var` binds the matched value)
case let x: x + 1

// Type pattern (a bare name must name a class; it matches instances of it)
case Circle: ...

// Wildcard (matches anything)
case _: ...

// Tuple destructuring
case let (a, b): a + b

// Record destructuring
case let { x, y }: x + y

// Shorthand fields bind, like `let { x, y } = rec` destructuring.
// `as` binds a field under a different name.
case { x, y }: x + y
case { x as first }: first

// Class destructuring
case let Point { x: 0, y }: "on y-axis"

// Or patterns
case 1 | 2 | 3: "small"

// Guard patterns
case let n if n > 100: "large"

Block Cases

zena
match (x) {
  case 1: {
    let doubled = x * 2;
    doubled + 1
  }
  case _: 0
}

Exhaustiveness

Match expressions must cover all possible values:

zena
type Status = "ok" | "error";
let s: Status = "ok";

match (s) {
  case "ok": ...
  case "error": ...  // Required for exhaustiveness
}

Strings

Strings in Zena are immutable sequences of Unicode text. The internal encoding (WTF-8 or WTF-16) is abstracted away—you work with characters and code points, not bytes.

String literals can be written with single quotes ('...'), double quotes ("..."), or backticks (`...`). Single and double quotes are equivalent. Backticks create template literals that support multi-line content and ${...} interpolation.

zena
let s1 = 'hello'; // Single quotes
let s2 = 'world'; // Double quotes (equivalent)
let s3 = 'line1\nline2'; // Escape sequences

// Template literals (backticks)
let name = 'Alice';
let greeting = `Hello, ${name}!`; // Interpolation

// Multi-line (only with backticks)
let text = `
  This is
  multi-line
`;

Escape Sequences

Sequence Meaning
\n Newline
\r Carriage return
\t Tab
\\ Backslash
\" Double quote
\' Single quote
\$ Dollar sign (in templates)
\xNN Hex byte
\uNNNN Unicode code point

String Operations

String slicing is O(1) and shares backing storage with the original string—no copying required. Use copy() when you need to release the parent string's memory.

zena
let s = 'hello';
s.length; // Length in code units
s.slice(1, 3); // "el" (O(1), shares backing storage)
s.copy(); // Force a copy (release parent memory)
s + ' world'; // Concatenation

StringBuilder

Use StringBuilder for efficient concatenation when building strings in a loop. For simple a + b + c expressions, regular concatenation is fine.

zena
import {StringBuilder} from 'zena:string-builder';

let sb = new StringBuilder();
sb.append('Hello');
sb.append(', ');
sb.append('World!');
let result = sb.toString(); // "Hello, World!"

StringReader

Use StringReader for parsing strings. It provides a cursor-based API that safely handles UTF-8 boundaries, making it ideal for tokenizers and parsers.

zena
import {StringReader} from 'zena:string-reader';

let r = new StringReader('hello world');
r.skipWhitespace();
let start = r.mark();
while (!r.isAtEnd && r.peekByte() != 32) {
  // 32 = space
  r.advance();
}
let word = r.sliceFrom(start); // "hello"

Tagged Template Literals

Template literals can be tagged with a function that processes the template. The tag function receives the static string parts and interpolated values separately.

zena
let highlight = (
  strings: TemplateStringsArray,
  ...values: Array<any>
): String => {
  let result = new StringBuilder();
  for (var i = 0; i < values.length; i = i + 1) {
    result.append(strings[i]);
    result.append('**');
    result.append(values[i] as String);
    result.append('**');
  }
  result.append(strings[strings.length - 1]);
  return result.toString();
};

let name = 'Zena';
highlight`Hello, ${name}!`; // "Hello, **Zena**!"

The TemplateStringsArray is guaranteed to be referentially stable—the same template literal in source code always produces the same array instance. This enables caching based on array identity.

Raw Strings

Tag functions can access unprocessed string content (with escape sequences intact) via the raw property:

zena
let showEscapes = (strings: TemplateStringsArray): String => {
  return strings.raw[0]; // Escape sequences not processed
};

showEscapes`line1\nline2`; // "line1\\nline2" (literal backslash-n)

The regex Tag

The regex tag from zena:regex compiles a regular expression at runtime. It uses raw strings, so you don't need to double-escape backslashes:

zena
import {regex} from 'zena:regex';

// Without tagged template: must escape backslashes
let r1 = new Regex('\\d+\\.\\d+');

// With regex tag: write patterns naturally
let r2 = regex`\d+\.\d+`; // Same pattern, easier to read

Type System

Zena has a sound type system—if your code compiles, it won't have type errors at runtime. The type system combines nominal typing (for classes) with structural typing (for records and functions).

Type Aliases

Type aliases create a new name for a type without creating a new type. Aliases are interchangeable with their underlying type.

zena
type ID = String;
type Point = {x: i32; y: i32};
type Callback = (result: String) => void;
type Container<T> = {value: T};

Distinct Types

Create nominally distinct types from existing types:

zena
distinct type Meters = i32;
distinct type Seconds = i32;

let m: Meters = 10 as Meters;
let s: Seconds = 5 as Seconds;

let x = m + s;  // ❌ Error: type mismatch

Union Types

Union types represent values that can be one of several types. All types in a union must be distinguishable at runtime—the compiler rejects unions where it can't tell the types apart.

zena
let x: String? = null;  // T? is shorthand for T | null
x = "hello";

// Union narrowing
if (x != null) {
  // x is String here
}

if (x is MyClass) {
  // x is MyClass here
}

Union restrictions:

  • Primitives cannot mix with other types: i32? is not allowed - Literal unions of the same primitive are fine: 1 | 2 | 3 works
  • Extension classes on the same base type cannot be unioned (indistinguishable)
  • Use Box<T> to put primitives in unions with references: Box<i32>?

Literal Types

Literal types represent exact values. Combined with unions, they create enumeration-like types with precise type checking.

zena
type Mode = 'read' | 'write';
type Level = 1 | 2 | 3;

let mode: Mode = 'read';
let level: Level = 2;

Function Types

zena
type BinaryOp = (a: i32, b: i32) => i32;
type Predicate<T> = (item: T) => boolean;

Type Narrowing

The type system narrows types based on control flow:

zena
let process = (x: String?) => {
  if (x == null) {
    return 'empty';
  }
  // x is String here
  return x.length;
};

Narrowing also works for immutable paths (class let fields, record fields, tuple elements):

zena
class Wrapper {
  let inner: Container?;  // Immutable field
  new() : inner = null { }
}

let process = (w: Wrapper): i32 => {
  if (w.inner !== null) {
    return w.inner.value;  // w.inner narrowed to Container
  }
  return 0;
};

Mutable fields (var) cannot be narrowed—another reference could modify the field between the check and use.

Nominal vs Structural

  • Nominal: Classes, interfaces, distinct types (identity matters)
  • Structural: Records, tuples, functions (shape matters)
zena
class A {
  x: i32;
}
class B {
  x: i32;
}
// A and B are NOT compatible (nominal)

type R1 = {x: i32};
type R2 = {x: i32};
// R1 and R2 ARE compatible (structural)

Enums

Enums define a set of named constants. They're distinct types—you can't accidentally use a raw integer where an enum is expected.

zena
// Integer-backed (default)
enum Color {
  Red, // 0
  Green, // 1
  Blue, // 2
}

// Explicit values
enum Status {
  Ok = 200,
  NotFound = 404,
}

// String-backed
enum Direction {
  Up = 'UP',
  Down = 'DOWN',
}

let c: Color = Color.Red;
let n: i32 = c as i32; // Explicit cast required

Records & Tuples

Records and tuples are immutable, structural data types. Two records with the same shape are compatible, regardless of where they're defined.

Records

Records are immutable objects with named fields. They support shorthand syntax and spreading.

zena
let p = {x: 1, y: 2};
let x = p.x; // 1

// Shorthand
let x = 1;
let y = 2;
let p = {x, y}; // { x: 1, y: 2 }

// Spread
let p2 = {...p, z: 3}; // { x: 1, y: 2, z: 3 }

Tuples

Tuples are fixed-length sequences where each position can have a different type.

zena
let t = (1, 'hello');
let n = t[0]; // 1
let s = t[1]; // "hello"

Tuple indices must be compile-time known values:

zena
let t = (1, 'hello', true);

let first = t[0]; // ✅ Literal index
let idx = 1; // let variable with literal initializer
let second = t[idx]; // ✅ Compile-time known

var i = 0;
let x = t[i]; // ❌ var is not compile-time known

Tuple elements support type narrowing since tuples are immutable:

zena
let process = (t: (Container?, i32)): i32 => {
  if (t[0] !== null) {
    return t[0].value; // t[0] narrowed to Container
  }
  return 0;
};

Destructuring

Destructuring extracts values from records, tuples, and class instances into separate variables.

zena
// Record
let { x, y } = point;
let { x as a, y as b } = point;  // Rename

// Tuple
let (first, second) = tuple;
let (a, , c) = (1, 2, 3);  // Skip elements

Classes

Classes are nominal types with fields, methods, and constructors. They compile to WASM-GC structs, with vtables (WASM tables) generated as needed for virtual dispatch. Classes support single inheritance, can implement multiple interfaces, and can include mixins.

zena
class Point {
  x: i32;
  y: i32;

  new(x: i32, y: i32) {
    this.x = x;
    this.y = y;
  }

  move(dx: i32, dy: i32): void {
    this.x = this.x + dx;
    this.y = this.y + dy;
  }

  distanceFromOrigin(): f32 {
    return sqrt((this.x * this.x + this.y * this.y) as f32);
  }
}

let p = new Point(3, 4);
p.move(1, 1);

Fields

Fields are public and immutable by default. Public fields are virtual—they're inherited by subclasses and can be overridden with accessors. This means field access may involve a virtual call.

Tip: Immutable fields work with type narrowing and make code easier to reason about. Use var when mutability is needed. Use private fields (#) or final classes to avoid virtual dispatch overhead. See Field Mutability and Private Fields.

zena
class Rectangle {
  var width: i32;   // Public, virtual, mutable
  var height: i32;  // Public, virtual, mutable
}

class Square extends Rectangle {
  // Override width with an accessor that keeps width == height
  width: i32 {
    get { return this.height; }
    set(v) { this.height = v; }
  }
}

Private Fields

Private fields use the # prefix. They're only accessible within the class, are not inherited, and have no virtual dispatch overhead.

zena
class Counter {
  #count: i32; // Private, direct access

  new() {
    this.#count = 0;
  }

  increment(): void {
    this.#count = this.#count + 1;
  }

  get(): i32 {
    return this.#count;
  }
}

Field Mutability

Fields are immutable by default. Use var to make a field mutable. The let modifier is accepted but redundant — bare fields are already immutable. Field types can be inferred from their initializer expression.

zena
class User {
  id: i32;                    // Immutable (default)
  let created: i64;           // Immutable (explicit, same as bare)
  var email: String;          // Mutable
  var(#phone) phone: String;  // Mutable with private setter
}
Syntax Getter Setter Mutability
name: Type Public None (constructor only) Immutable
let name: Type Public None (constructor only) Immutable
var name: Type Public Public Mutable
var(#name) name: Type Public Private (#name) Mutable

The var(#name) syntax creates a publicly readable field with a private setter:

zena
class Counter {
  var(#count) count: i32 = 0;

  increment(): void {
    this.#count = this.count + 1;  // Write via #count
  }
}

let c = new Counter();
let n = c.count;  // OK - reading is public
c.count = 5;      // Error - no public setter

Nullable Fields

A field of nullable type (Type?, shorthand for Type | null) needs no initializer: it defaults to null when not set in the constructor. Like any nullable type, primitives are excluded—use Box<T> if needed. Fields cannot be marked optional themselves (bio?: String is an error): ? after a name means "may be absent", which is a record property and parameter concept—a constructed object's fields are never absent.

zena
class User {
  name: String;
  bio: String?;  // String or null

  new(name: String) {
    this.name = name;
    // bio defaults to null
  }
}

interface Configurable {
  label: String?;  // Nullable interface field
}

mixin Timestamped {
  updatedAt: String?;  // Nullable mixin field
}

Nullable fields work with abstract and private (#) fields:

zena
abstract class Base {
  abstract metadata: String?; // Subclasses must provide
}

class Cache {
  #lastResult: String?; // Private nullable field
}

Initializer Lists

For immutable fields that need constructor parameters, use Dart-style initializer lists. The initializer list appears after : and before the constructor body.

zena
class Point {
  let x: i32;
  let y: i32;

  // Initializer list before the body
  new(x: i32, y: i32) : x = x, y = y { }
}

class Rectangle {
  let width: i32;
  let height: i32;
  let area: i32;

  // Can compute values from parameters
  new(w: i32, h: i32) : width = w, height = h, area = w * h { }
}

Initializer list expressions can reference constructor parameters and earlier fields in the list. They cannot reference this because the object doesn't exist yet.

For derived classes, super() goes at the end of the initializer list:

zena
class Point3D extends Point {
  let z: i32;

  // Initialize z, then call super
  new(x: i32, y: i32, z: i32) : z = z, super(x, y) { }
}

Only actual fields (not setters) can appear in initializer lists.

Getters and Setters

Accessors define computed properties. They can override inherited fields or stand alone. Use get for read-only, or both get and set for read-write.

zena
class Rectangle {
  width: i32;
  height: i32;

  area: i32 {
    get {
      return this.width * this.height;
    }
  }
}

Inheritance

Classes can extend one parent class using extends. The child inherits all fields and methods, and can override methods.

zena
class Animal {
  name: String;

  new(name: String) {
    this.name = name;
  }

  speak(): String {
    return '...';
  }
}

class Dog extends Animal {
  new(name: String) {
    super(name);
  }

  speak(): String {
    return 'Woof!';
  }
}

Generic Classes

Classes can have type parameters. Zena uses monomorphization—Box<i32> and Box<String> are completely separate types at runtime.

zena
class Box<T> {
  value: T;

  new(value: T) {
    this.value = value;
  }

  map<U>(fn: (val: T) => U): Box<U> {
    return new Box<U>(fn(this.value));
  }
}

let b = new Box<i32>(42);

Static Members

Static fields and methods belong to the class itself, not instances. Access them using the class name.

zena
class Math {
  static PI: f32 = 3.14159;

  static max(a: i32, b: i32): i32 {
    return if (a > b) a else b;
  }
}

let pi = Math.PI;
let m = Math.max(3, 5);

A static belongs to the class itself, so a generic class's type parameters are out of scope inside one. A static that needs a type parameter declares its own, solved from the arguments or written at the call like any generic function's.

zena
class Boxed<T> {
  item: T;
  new(this.item);
  static of<A>(v: A): Boxed<A> { return new Boxed<A>(v); }
  static none<A>(): Boxed<A>? { return null; }
}

let b = Boxed.of(11);          // Boxed<i32>, solved from the argument
let e = Boxed.none<String>();  // written: nothing else determines A

Static storage is one cell shared by every use of the class, and there is no way to write a class's type arguments in expression position — a static never varies with them.

Modifiers

abstract classes cannot be instantiated and may have abstract methods. final classes cannot be extended, and final methods cannot be overridden.

zena
abstract class Shape {
  abstract area(): f32;
}

final class Circle extends Shape {
  radius: f32;

  new(radius: f32) {
    this.radius = radius;
  }

  area(): f32 {
    return 3.14159 * this.radius * this.radius;
  }
}

Extension Classes

Add methods to existing types:

zena
extension class StringExt on String {
  isEmpty(): boolean {
    return this.length == 0;
  }
}

"hello".isEmpty();  // false
"".isEmpty();       // true

Case Classes

A class with a parameter list after its name is a case class—a concise declaration that auto-generates fields, a constructor, operator ==, and hashCode(). Case classes are implicitly final and cannot be extended.

zena
class Point(x: f64, y: f64)

let a = new Point(1.0, 2.0);
let b = new Point(1.0, 2.0);
a == b;    // true (structural equality)
a.x;       // 1.0

Case classes can have a body for additional members, and support extends, with, and implements:

zena
class Point(x: f64, y: f64) {
  distance(): f64 { return sqrt(x * x + y * y); }
}

class Counter(name: String, var count: i32)  // var for mutable fields
class Pair<A, B>(first: A, second: B)        // Generic case classes
class Event(name: String) with Timestamped implements Hashable

Sealed Classes

Sealed classes restrict which classes can extend them. Only the variants listed in the sealed class body (or explicitly allowed) may be direct subclasses. This enables exhaustive pattern matching—the compiler knows every possible case.

Sealed classes are implicitly abstract and cannot be instantiated directly.

zena
sealed class Expr {
  case Binary(left: Expr, op: String, right: Expr)
  case Literal(value: i32)
  case Unary(op: String, expr: Expr)
}

// Inline variants are case classes—with auto-generated fields, ==, hashCode

let expr = new Binary(new Literal(1), '+', new Literal(2));

// Exhaustive match—compiler ensures all cases are covered
let result = match (expr) {
  case let Binary { left, op, right }: eval(left) + eval(right)
  case let Literal { value }: value
  case let Unary { op, expr }: -eval(expr)
};

Variants can also be declared separately using extends:

zena
sealed class Shape { }

class Circle(radius: f64) extends Shape
class Rectangle(width: f64, height: f64) extends Shape

Operator Overloading

Classes can overload operators to provide custom behavior for built-in syntax.

operator ==

Define custom equality. Called by == and the eq intrinsic.

zena
class Point {
  x: i32;
  y: i32;

  operator ==(other: Point): boolean {
    return this.x == other.x && this.y == other.y;
  }
}

let a = new Point(1, 2);
let b = new Point(1, 2);
a == b;  // true (calls operator ==)
a === b; // false (reference equality)

operator []

Define index access for custom collections. Implement get for reading and set for writing.

zena
class Grid {
  #data: FixedArray<i32>;
  #width: i32;

  operator [](x: i32, y: i32): i32 {
    get {
      return this.#data[y * this.#width + x];
    }
    set(value) {
      this.#data[y * this.#width + x] = value;
    }
  }
}

let grid = new Grid(10, 10);
grid[3, 4] = 42;      // calls operator [] set
let v = grid[3, 4];   // calls operator [] get

The index operator can take any number of parameters. For single-index access (like arrays), use one parameter. For multi-dimensional access (like matrices or grids), use multiple parameters.

Method Overloading

Classes can have multiple methods with the same name but different parameter types or counts. The compiler resolves the correct overload at compile time based on argument types.

zena
class Printer {
  print(val: i32): void {
    console.log('Integer: ' + val);
  }

  print(val: String): void {
    console.log('String: ' + val);
  }

  print(val: i32, count: i32): void {
    for (var i = 0; i < count; i = i + 1) {
      console.log(val);
    }
  }
}

let p = new Printer();
p.print(42); // Calls print(i32)
p.print('hello'); // Calls print(String)
p.print(7, 3); // Calls print(i32, i32)

Overload resolution is always static—the compiler picks the method based on the declared types at the call site, not the runtime type of arguments. This is different from virtual dispatch, which selects the method implementation at runtime based on the object's actual type.

zena
class Base {
  process(val: i32): i32 {
    return val;
  }
  process(val: f32): i32 {
    return 100;
  }
}

class Child extends Base {
  // Override only the i32 version
  process(val: i32): i32 {
    return val * 2;
  }
  // Inherits the f32 version from Base
}

let c: Base = new Child();
c.process(10); // Overload i32 selected at compile time
// Virtual dispatch calls Child.process(i32) → 20
c.process(3.14); // Overload f32 selected at compile time
// Calls Base.process(f32) → 100

Interfaces

Interfaces define contracts that classes must fulfill. A class can implement multiple interfaces. Interface values use "fat pointers" (object + vtable) at runtime.

zena
interface Drawable {
  draw(): void;
}

interface Resizable {
  resize(factor: f32): void;
}

class Circle implements Drawable, Resizable {
  radius: f32;

  new(radius: f32) {
    this.radius = radius;
  }

  draw(): void {
    // ...
  }

  resize(factor: f32): void {
    this.radius = this.radius * factor;
  }
}

Generic Interfaces

zena
interface Container<T> {
  get(): T;
  set(value: T): void;
}

Interface Inheritance

zena
interface Named {
  name: String { get; }
}

interface Person extends Named {
  age: i32 { get; }
}

Mixins

Mixins provide reusable chunks of functionality that can be composed into classes. Unlike interfaces, mixins include implementation. A class can include multiple mixins using the with clause.

zena
mixin Timestamped {
  createdAt: i64;

  touch(): void {
    this.createdAt = getCurrentTime();
  }
}

mixin Named {
  name: String;
}

class Document with Timestamped, Named {
  content: String;

  new(name: String, content: String) {
    this.name = name;
    this.content = content;
    this.createdAt = getCurrentTime();
  }
}

Constraints (the on clause)

A mixin can restrict which classes it can be applied to by using the on clause. This allows the mixin to access fields and methods on this that are guaranteed by the constrained type:

zena
mixin Syncable on Entity {
  sync(): void {
    this.save();  // OK: save() is defined on Entity
  }
}

Satisfying Constraints and Interface Implementation

A target class satisfies a mixin's on constraint if the constraint type is assignable to:

  1. The target class's superclass.
  2. The target class's extension on type (if it is an extension class).
  3. Any interface in the target class's implements list.

This allows classes to implement an interface (e.g. Iterable<T>) via a mixin that is constrained to that same interface (e.g. on Iterable<T>). During type checking, this is resolved cleanly without circular dependency:

  1. The class declares implements Iterable<T>, which immediately satisfies the mixin's on Iterable<T> constraint.
  2. The mixin is applied, injecting the required utility methods into the class.
  3. The compiler validates that the class fully implements all methods in Iterable<T>, which succeeds because the mixin provided the required methods.

For example, HashMap implements Map<K, V> (which extends Iterable) by mixing in IterableUtils (which is on Iterable):

zena
// HashMap satisfies 'on Iterable' via 'implements Map'
export class HashMap<K, V> with IterableUtils<MapEntry<K, V>> implements Map<K, V> {
  // Directly implements the core iterator method
  :Iterable.iterator(): Iterator<MapEntry<K, V>> {
    return new HashMapEntryIterator<K, V>(this.#buckets);
  }

  // The utility methods (contains, all, fold, etc.) required by Iterable
  // are automatically supplied by IterableUtils!
}

Arrays & Collections

Zena provides universal interfaces for indexed collections (Array<T> and MutableArray<T>), along with concrete implementations for fixed-size and resizable arrays, plus a hash map. All collections are generic and type-safe.

Array Hierarchy

  • Array<T>: The universal indexed read-only interface (length, operator [](i32): T, map, :Iterable.iterator()). Implemented by FixedArray<T>, GrowableArray<T>, and ImmutableArray<T>.
  • MutableArray<T>: The mutable indexed interface extending Array<T> (operator []=(i32, T): void). Implemented by FixedArray<T> and GrowableArray<T>.

Literals

An array literal is an ImmutableArray<T> — Wasm's immutable (array T) — unless the context wants mutable elements (a FixedArray, MutableArray, or array<var T> expected type), in which case it builds directly as a FixedArray<T>. The fixed(...) and growable(...) helpers supply that context in expression position:

zena
let nums = [1, 2, 3];                     // ImmutableArray<i32>
let buf: FixedArray<i32> = [1, 2, 3];     // FixedArray via the annotation
let inline = fixed([1, 2, 3]);            // FixedArray, no annotation, no copy
let grow = growable([1, 2, 3]);           // GrowableArray adopting the literal

FixedArray

FixedArray<T> has a fixed size set at creation and maps directly to a native WebAssembly GC array of mutable elements (array<var T>).

zena
let nums = fixed([1, 2, 3]); // FixedArray<i32>
let arr = new FixedArray<i32>(10, 0); // Size 10, initialized to 0
arr[0] = 42;
let len = arr.length; // 10

GrowableArray

GrowableArray<T> is a dynamic, resizable array. growable([...]) adopts a literal as its storage without copying; GrowableArray.from(seq) copies from any array; new GrowableArray<T>() makes an empty one.

zena
let arr = growable([1, 2, 3]);
arr.push(4); // [1, 2, 3, 4]
let len = arr.length; // 4
let first = arr[0]; // 1
let last = arr.pop(); // 4

let empty = new GrowableArray<i32>(); // Empty growable array

ImmutableArray

ImmutableArray<T> is the immutable array — a distinct WebAssembly type from FixedArray<T>, with no subtyping or casting between them. Writes are compile-time errors, and the immutability is real at runtime, not a view over mutable storage.

zena
let immutable = [1, 2, 3];  // ImmutableArray<i32>
let first = immutable[0];
immutable[0] = 9;           // error: elements are immutable

Slicing

Use range syntax to slice a FixedArray. Slices are independent copies.

zena
let arr = fixed([1, 2, 3, 4, 5]);
let slice = arr[1..4];    // [2, 3, 4] (a fresh FixedArray)

Map

Map<K, V> is a hash map. Keys must satisfy the Hashable interface (hashCode(): i32): numeric primitives, boolean, String, enums, distinct types, and case classes satisfy it automatically; other classes must declare implements Hashable and define hashCode().

Maps can be created using literal syntax with =>:

zena
let scores = {"Alice" => 95, "Bob" => 87};  // Map<String, i32>
let lookup = {1 => "one", 2 => "two"};       // Map<i32, String>

Or constructed explicitly:

zena
let map = new Map<String, i32>();
map.set('one', 1);
map['two'] = 2;

Use get() for safe lookups—it returns (value, found):

zena
let(val, found) = map.get('one');
if (found) {
  // use val
}

The index operator map[key] throws KeyNotFoundError if the key doesn't exist.

Iteration

Use for-in to iterate over any collection that implements the iteration protocol.

zena
let arr = [1, 2, 3];
for (let item in arr) {
  // item is 1, 2, 3
}

let map = new Map<String, i32>();
for (let (key, value) in map) {
  // iterate over entries
}

Boxing

Primitive types (i32, f32, boolean) cannot be used in union types because they have a different memory representation than references. Use Box<T> to wrap primitives when needed.

zena
let maybeNumber: Box<i32>? = new Box(42);

if (maybeNumber != null) {
  let n = maybeNumber.value;
}

Boxing is always explicit

There is no any type and no implicit boxing. anyref is the top type for references, and a primitive is not assignable to it — wrap it first.

zena
let boxed: anyref = new Box<i32>(42); // allocation is visible
// let bad: anyref = 42;              // Error: no implicit boxing

if (boxed is Box<i32>) {
  let n = (boxed as Box<i32>).value; // 42
}

Exception Handling

Zena uses exceptions for error handling, compiled to WASM exception handling instructions.

Throw

The throw expression has type never and can be used anywhere an expression is expected.

zena
throw new Error("Something went wrong");

// throw has type 'never', can be used anywhere
let x: i32 = throw new Error("Boom");

Error Class

All thrown values must be Error or a subclass. Create custom error types by extending Error.

zena
class Error {
  message: String;
  new(message: String) {
    this.message = message;
  }
}

// Custom errors
class ValidationError extends Error {
  field: String;
  new(field: String, message: String) {
    super(message);
    this.field = field;
  }
}

The cancel Clause (async only)

A try in an async function may add cancel { ... } between the try block and any catch: the block runs when a cancellation unwinds the try, and the unwind then continues — catch (e) never sees a cancellation, and leaving the block with return/break/continue is rejected.

zena
try {
  let rows = await db.query(q);
} cancel {
  metrics.increment('query-abandoned');
} finally {
  releaseBuffers();
}

shielded Blocks (async only)

shielded { ... } masks cancellation checkpoints for its extent — cleanup that must await after cancellation has arrived runs to completion, and async work created inside binds to a scope no cancellation reaches. At the closing brace, delivery resumes.

Resource Management

using

using releases a Disposable when it leaves the enclosing block — on every path out, including return, break/continue, and exception unwind.

zena
import { Disposable } from 'zena:ownership';

class Lock implements Disposable {
  [Disposable.dispose](): void { release(this.handle); }
}

let update = (): void => {
  using guard = acquire(lock);
  // … use guard …
};   // released here

// Several in one block release in reverse declaration order
using a = open('a');
using b = open('b');   // b first

// A binding is optional
using acquire(lock);

dispose implementations must be idempotent and must not throw — they run on unwind paths. using declares the binding itself — no let or var, since a rebound name would release a stale value and leak the new one.

Host Imports

Use declare with @external to import functions from the host environment. These become WASM imports that must be provided by the host (JavaScript, WASI runner, etc.).

zena
@external("env", "log")
declare function log(val: i32): void;

@external("env", "now")
declare function now(): i64;

Intrinsics & Decorators

Intrinsics and decorators provide low-level control over code generation and enable standard library implementation.

@intrinsic

Intrinsics map to compiler-generated code or direct WASM instructions. Used primarily in the standard library.

zena
@intrinsic('eq')
declare function equals<T>(a: T, b: T): boolean;

@intrinsic('hash')
declare function hash<T>(val: T): i32;

@pure

Mark accessors as side-effect free. This enables the compiler to eliminate unused writes during dead code elimination.

zena
class Data {
  @pure
  value: i32 {
    get { return this.#backing; }
    set(v) { this.#backing = v; }
  }
  #backing: i32;
}

Type Casting

The as operator performs type casts. Numeric conversions compile to WASM conversion instructions. Reference type casts are checked at runtime and throw if invalid. Unnecessary casts (casting a value to its exact type) can be warned against by enabling the CLI flag --warn-unnecessary-casts.

zena
// Numeric conversions
let n: i64 = 100 as i64;      // i32 to i64
let f: f32 = 10 as f32;       // i32 to f32
let i: i32 = 3.14 as i32;     // f32 to i32 (truncates)

// Distinct types (zero-cost)
distinct type ID = i32;
let id = 42 as ID;

// Reference types (checked at runtime)
let obj: any = getObject();
let p = obj as Point;         // Throws if not a Point

Standard Library

Zena's standard library is organized into modules. Types from the prelude are available without imports; other modules must be explicitly imported.

zena:math

Math functions that map directly to WASM instructions—no runtime overhead.

zena
import {sqrt, floor, ceil, abs, min, max} from 'zena:math';

sqrt(16.0); // 4.0
floor(3.7); // 3.0
ceil(3.2); // 4.0
abs(-5.0); // 5.0
min(3.0, 7.0); // 3.0
max(3.0, 7.0); // 7.0

Also includes bit manipulation: clz (count leading zeros), ctz (count trailing zeros), popcnt (population count).

zena:simd

Two layers over WebAssembly SIMD, in one import.

Shaped types give a v128 a lane width and a lane type: I8x16, I16x8, I32x4, I64x2, F32x4, F64x2. They are extension classes over v128 and are erased, so a shape IS a v128 at runtime — every method is one instruction and nothing allocates.

zena
import {F32x4} from 'zena:simd';

let a = F32x4.splat(1.5 as f32);
let b = new F32x4(2.5 as f32);
let c = (a + b).withX(10.0 as f32);
c.x + c.y; // 10.0 + 4.0

Operators are elementwise, as in numpy and Rust: a * b multiplies lane i by lane i, and is not a dot product. Lane-combining operations are named methods. Four- and two-lane shapes name their lanes x/y/z/w with matching withX/withY/… copies; wider shapes use the instruction layer.

Missing on purpose: / on integer shapes (wasm SIMD divides floats only), * on I8x16 (no i8x16.mul), and == (wasm's lane comparison yields a mask, not a boolean). Negation is v.neg(), since Zena dispatches only binary operators to operator methods.

All 236 instructions are also exposed, one function each, lowered to that single instruction. Names transliterate the wasm names: split on . and _, then capitalize every segment but the first, so i32x4.add is i32x4Add and v128.load8x8_s is v128Load8x8S.

zena
import {i32x4Splat, i32x4Add, i32x4ExtractLane} from 'zena:simd';

let sums = i32x4Add(i32x4Splat(5), i32x4Splat(3));
i32x4ExtractLane(sums, 0); // 8

Arguments that wasm encodes as immediates must be literals: lane indices, the four words of v128Const, and the sixteen selectors of i8x16Shuffle. Loads and stores take an address only — the compiler picks the natural alignment. See simd.md.

zena:console

Console output for logging and debugging. The console global is automatically available in every module (via the prelude), so you don't need to import it.

zena
console.log('Hello, world!');
console.error('Something went wrong');
console.warn('This is a warning');
console.info('FYI');
console.debug('Debug info');

The console implementation is selected based on the --target flag:

  • --target host: Uses imported JavaScript functions (env.console_log, etc.) that the host must provide
  • --target wasi: Writes directly to stdout/stderr using WASI file descriptors

This means the same Zena code works in both browser/Node.js environments and standalone WASI runtimes like wasmtime.

zena:string-builder

Efficient string concatenation for loops. See StringBuilder.

zena
import {StringBuilder} from 'zena:string-builder';

zena:string-reader

Cursor-based string parsing. See StringReader.

zena
import {StringReader} from 'zena:string-reader';

zena:regex

A regular expression engine based on Thompson NFA (similar to RE2 and Go's regexp). Guarantees O(n×m) time complexity—no backtracking, no ReDoS vulnerabilities.

zena
import {Regex, regex} from 'zena:regex';

// Constructor
let r = new Regex('\\d+');

// Template tag (no double-escaping needed)
let r2 = regex`\d+\.\d+`;

// Matching
let m = r.match('abc123def');
if (m != null) {
  m.group(0); // "123"
}

// Flags: (?i) case-insensitive, (?m) multiline, (?s) dot-matches-newline
let r3 = regex`(?i)hello`;

Supported: Literals, alternation (|), character classes ([a-z], \d, \w, \s), quantifiers (*, +, ?, {n,m}), non-greedy (*?, +?), groups (capturing and non-capturing), anchors (^, $, \b).

Not supported (by design): Backreferences and lookahead/lookbehind—these require backtracking which breaks the linear time guarantee.

zena:json

JSON parsing with typed accessors and optional comment support.

zena
import {parseJson, JsonObject, JsonArray} from 'zena:json';

let obj = parseJson('{"name": "Zena", "version": 1}') as JsonObject;
obj['name']; // "Zena" (as any)
obj['version']; // 1 (as any, boxed)

// With options
let config = parseJson(text, {
  allowComments: true, // Allow // and /* */ comments
  trackLocations: false, // Track source locations for errors
}) as JsonObject;

JSON values are represented as JsonObject, JsonArray, String, Box<f64>, Box<boolean>, or null.

zena:test

Test framework for writing unit tests. Tests are defined using suite() and test() functions, and you must export a variable named tests.

zena
import {suite, test} from 'zena:test';
import {equal, isTrue} from 'zena:assert';

export let tests = suite('math', () => {
  test('adds numbers', () => {
    equal(1 + 1, 2);
  });

  test('comparisons', () => {
    isTrue(3 > 2);
  });
});

zena:assert

Assertion functions for tests. All assertions throw AssertionError on failure.

zena
import {equal, notEqual, isTrue, isFalse, isNull, isNotNull, throws} from 'zena:assert';

equal(actual, expected);           // actual == expected
notEqual(actual, expected);        // actual != expected
same(actual, expected);            // actual === expected (reference equality)
notSame(actual, expected);         // actual !== expected
isTrue(value);                     // value === true
isFalse(value);                    // value === false
isNull(value);                     // value === null
isNotNull(value);                  // value !== null
greater(a, b);                     // a > b
greaterOrEqual(a, b);              // a >= b
less(a, b);                        // a < b
lessOrEqual(a, b);                 // a <= b
throws(() => { ... });             // function throws an exception
doesNotThrow(() => { ... });       // function does not throw
fail('message');                   // always fails

zena:fs

File system operations (WASI target only).

zena
import {readFile, writeFile} from 'zena:fs';

let content = readFile('input.txt');
writeFile('output.txt', content);