concepts

Classes & Generics

Classes, interfaces, and virtual dispatch work like upstream AssemblyScript’s; generics are specialized to concrete types at compile time.

Classes, inheritance, interfaces

classes.as
class Base {
    name(): i32 { return 1; }
    describe(): i32 { return this.name() * 2; }
}

class A extends Base {
    name(): i32 { return 3; }
}

interface Shape { area(): i32; }

class Square implements Shape {
    side: i32 = 3;
    area(): i32 { return this.side * this.side; }
}

export function demo(): i32 {
    // Virtual dispatch via rtId: 6 + 9.
    return (new A() as Base).describe() + (new Square() as Shape).area();
}

Fields with initializers, constructors (implicitly returning this), methods, statics, getters/setters, extends with super(), instanceof via rtti. Calls through a base reference dispatch virtually, matching upstream AssemblyScript. Abstract methods compile to unreachable stubs with stock-asc enforcement — no new of an abstract class, missing implementations are errors.

Cascades

cascades.as
class Paint {
    color: u32 = 0;
    width: f32 = 1;
    child: Paint | null = null;
    thicker(by: f32): Paint {
        this.width += by;
        return this;
    }
}

function draw(p: Paint): f32 {
    return p.width;
}

export function demo(): f32 {
    // The receiver is evaluated once, each section runs on it in order,
    // and the whole expression is that receiver, now 3 wide.
    let stroke = new Paint()
        ..color = 0xff0000ff
        ..width = 2
        ..thicker(1);
    // Any expression position works: arguments, returns, initializers.
    return draw(stroke) + draw(new Paint()..thicker(4)..child = stroke);
}

Dart’s cascade operator. receiver..section..section evaluates the receiver once, runs each section on it as a statement, and yields the receiver, so setting up an object field by field stays an expression. A section is a member chain rooted at the receiver, optionally followed by an assignment: ..x = v, ..x += v, ..f(a), ..[i] = v, ..a.b = v. The receiver is the whole conditional expression, as in Dart: the cascade binds looser than postfix, binary, as and ternary operators and tighter than assignment, so v as Paint..color = c casts first and x = a..b = c sets a.b and then assigns a to x; parenthesize to cascade on an assignment result. A section’s right side never starts a nested cascade: in a..child = b..width = 2 both sections apply to a, as in Dart, and a nested cascade needs parentheses. Sections may continue on following lines; rasc fmt puts each section of a multi section cascade on its own line.

Receivers are reference classes and interfaces: numbers, strings and function values are rejected, a nullable receiver is an error (assert with ! first), and struct receivers are rejected since structs construct by value. Number literals keep their trailing dot, so after ..x = 1 the next section goes on a new line or after a space.

Generics

Generic functions, classes, and methods are fully monomorphized at compile time, the way C++ templates are: each distinct set of type arguments compiles to its own specialized copy, so a generic call costs exactly what the hand-written version would — no boxing, no dynamic dispatch. Type arguments are usually inferred from the parameters:

generics.as
function largest<T>(a: T, b: T): T {
    return a > b ? a : b;
}

export function demo(): f64 {
    let i = largest(3, 9);        // largest<i32>, inferred from the arguments
    let f = largest(0.25, 2.5);   // largest<f64> — a second compiled copy
    return (i as f64) + f;        // 9 + 2.5
}

Compilation is lazy and whole-program, so only the instantiations code actually reaches get compiled — a combination nothing calls costs nothing. Classes and methods take type parameters the same way, and a method can add its own on top of the class’s:

box.as
class Box<T> {
    value: T;
    constructor(value: T) { this.value = value; }
    map<U>(f: (v: T) => U): Box<U> {
        return new Box<U>(f(this.value));
    }
}

export function demo(): f64 {
    let b = new Box<i32>(21);
    let scaled = b.map<f64>((v: i32): f64 => (v as f64) * 2.0);
    return scaled.value;   // 42
}

One inference gap to know: a type parameter used only in a callback’s return type is not inferred from a lambda — the lambda’s own annotations do not flow backward into the call. So arr.map<i32>(...) needs its explicit argument, while filter, reduce, and forEach — whose type parameters all appear in parameter positions — infer fine:

inference.as
export function demo(): i32 {
    let xs = [1, 2, 3, 4];
    let evens = xs.filter((x: i32): bool => (x & 1) == 0);   // infers fine
    let doubled = xs.map<i32>((x: i32): i32 => x * 2);       // needs its <i32>
    return doubled[3] + evens.length;   // 8 + 2
}