language

Types

WebAssembly’s value types, spelled like TypeScript.

Basic types

TypeWasmDescription
i32 / u32i3232-bit integer, signed / unsigned interpretation
i64 / u64i6464-bit integer
i8 u8 i16 u16i32small integers for storage; compute in i32 (below)
isize / usizei32pointer-sized — 32-bit (wasm32)
f32 / f64f32 / f64IEEE floats
booli32true / false
v128v128SIMD vector; see SIMD

Small integers

WebAssembly has no 8 or 16 bit values. Locals, parameters, returns and the operand stack hold only 32 and 64 bit integers and floats, so an i8, u8, i16 or u16 is a wasm i32 everywhere except memory. Memory is the one place with real 8 and 16 bit values: loads and stores of those widths read and write exactly one or two bytes.

The results still behave like true small integers, as in upstream AssemblyScript. Arithmetic runs at 32 bits and may leave extra high bits, and every use that could see them narrows first: comparisons, division, remainder, right shifts, **, conversions to other types, conditions, returns, switch, and the integer builtins abs, clz, ctz, popcnt, min and max. rotl and rotr rotate within the small width, and constant folding follows the same rule. The narrowing is one extra instruction, a small cost in a tight loop that compares or divides small integers. Addition, subtraction, multiplication, &, |, ^ and << never need it.

guidance

Store in small types, compute in i32. Declare fields, arrays and struct members as u8 or i16 to save bytes, where the store narrows for free. Declare locals, parameters, loop counters and return values as i32 or u32, which never narrow. Read a small field into an i32, work on it, and cast once when you store it back.

small_ints.as
declare function print(s: string): void;

class Pixel { r: u8 = 0; g: u8 = 0; }

export function demo(): void {
    let x: u8 = 255;
    x++;
    print(x.toString());                   // 0, as in C and asc

    let p = new Pixel();
    p.r = 200;
    p.g = 200;
    print(((p.r + p.g) >> 1).toString()); // 72, a u8 sum wraps before >>

    // Compute in i32: no overflow and nothing to narrow.
    let r: i32 = p.r;
    let g: i32 = p.g;
    p.r = <u8>((r + g) >> 1);
    print(p.r.toString());                 // 200
}

Nullability

Nullable types are reference-only: string?, C?, (() => i32)?, with T?[] an array of nullable elements and T[]? a nullable array. There are no other unions, so T | null is an error. A nullable basic type (i32?) is a compile error. Nullability folds correctly per generic instance, so nullable array elements behave. Value classes exclude nullability entirely: null refuses a value-class target at compile time.

After x as T a ? followed by an operand starts a ternary, as in Dart. At the end of a line it makes T nullable, so a statement can end there without a semicolon, unless the next line is a ternary branch followed by its :. In a class body a member on the next line ends the field instead, so a field's ternary broken after as T ? needs that ? on the next line or parentheses. A basic type such as bool can never be nullable, so after a cast to one the ternary wins unless the next line can only be a member: one with an initializer or a body, or a field of a basic type or string.

The checker enforces the flag with a definite non-null analysis. A member access, call, index, for..of or cascade on a nullable value warns (W3004) unless the value is proven non-null on that path, and so does a nullable value, or null itself, stored into a non-nullable local, parameter, field, element or return. A proof comes from if (x != null) and if (x) for the then branch, if (x == null) for the else branch or for the rest of the block when the then branch returns, breaks, continues or throws, the right side of x != null && … and x == null || …, an assignment of a proven value, x ??= y or x ||= y with y proven, assert(x != null), and x!, which proves x from then on. A ternary, a || b or a ?? b whose arms are both proven counts as proven too.

Four operators work on nullable values, as in Dart:

Field chains narrow like locals: after if (this.context != null) the chain this.context stays proven until a store through any prefix of it (this.context = …) or a method call on a proper prefix (this.reset()), since a method can store into its receiver's fields. A call on the chain itself (this.context.grow()) keeps the chain and only drops what sits below it. Inside a loop, every key the loop stores or calls through is unproven for the whole loop; a closure starts with no proofs, since captures are copies.

nullable.as
declare function print(s: string): void;

function firstName(names: Array<string?>): string {
    let n = names[0];
    if (n != null) {
        return n;          // narrowed to string
    }
    return "anonymous";
}

class User {
    name: string? = null;
    manager: User? = null;
}

export function demo(): void {
    let empty: Array<string?> = [null, "rasc"];
    let named: Array<string?> = ["rasc", null];
    print(firstName(empty));   // anonymous
    print(firstName(named));   // rasc

    let user = new User();
    print((user.manager?.name?.length ?? 0).toString());   // 0
    user.manager ??= new User();
    user.manager.name = "Ada";
    print(user.manager?.name ?? "nobody");                 // Ada
}
gotcha

Array<T?> literals need an explicitly annotated local before they can be assigned somewhere that expects the nullable element type.

gotcha

Narrowing never changes a type, only whether the checker warns, so x != null ? x : y still has the nullable type. A proof about a local or parameter also lets ! and ?. skip their runtime null check; one about a global or a field does not, since a call or another reference to the same object can null it without the checker seeing.

Reference classes

Ordinary classes are managed heap objects: a small header followed by naturally aligned fields. The header holds the runtime class id, which drives instanceof, virtual dispatch, and garbage collection. @unmanaged classes drop all of that — raw structs for low-level work, so they cannot implement interfaces, override methods, or extend or be extended by managed classes.

An interface, generic ones included, types a reference to any class that implements it, and calls through it dispatch on the runtime class. The compiler checks each implementation against every member of the interface; see Classes & Generics.

Value classes

struct types are plain-old-data with copy semantics — they live in wasm locals, flatten into their containers, and never allocate. They get their own chapter.

Function types

functions.as
function apply(g: (x: i32) => i32, v: i32): i32 {
    return g(v);
}

export function demo(): i32 {
    let f: (x: i32) => i32 = (x: i32): i32 => x + 1;
    return apply(f, 41);   // 42
}

Plain functions and capturing closures mix freely behind the same function type, and a function-typed field or accessor calls directly: obj.fn(x). See Closures & Iteration.

Enums and namespaces

enums.as
enum Color { Red, Green = 5, Blue }   // Blue == 6; values must be constant

namespace Geo {
    export function area(w: f64, h: f64): f64 { return w * h; }
}

export function demo(): f64 {
    return Geo.area(6, 7) + Color.Blue;   // 48
}

Nested namespace paths resolve through scopes and enums (String.UTF8.ErrorMode). Inside a namespace a member’s own name is that member, as in TypeScript, even in its own initializer, so namespace N { export const x = x + 1; } is an error, “'x' is read in its own initialization cycle”, rather than a read of an outer x. A member with a declared type reads its own zero there instead, as a top-level const y: i32 = y + 1 does.

SIMD

v128 works as locals, params, returns, and class fields, with f32x4 and i32x4 builtin families (splat, lane access, arithmetic, compares, abs/neg/sqrt, min/max, bitmask) plus the bitwise v128 operations, loads, stores and lane shuffles. SIMD is on by default in a Rive project. There is no v128.const, so there are no v128 globals. See SIMD for the full list and for keeping a scalar path beside the SIMD one with ASC_FEATURE_SIMD.

Type queries and layout builtins

sizeof<T>(), alignof<T>(), offsetof<T>("field") const-fold. For value classes they report payload size and alignment; for reference classes, pointer size. idof<T>() gives the runtime class id; isDefined, isBoolean, isArray, isFunction, and isNullable<T>() fold per monomorphized instance — pair them with constant if folding for compile-time specialization.