Wado

Components

A Wado program compiles to a Component Model component. This chapter says how Wado values cross a component boundary, how a component waits, and how Wado declarations link to Component Model definitions and resources. Which world a component targets, and what its entry point is, is in Worlds and Entry Points.

Type Mapping at Component Boundaries

A Wado value that crosses a component boundary is lowered to a Component Model value, and lifted back on the other side, by the Canonical ABI.

The table below is the Wado↔CM correspondence, read in both directions: Wado→CM when generating a component's exported interface, and CM→Wado when importing an external component (use { Iface } from "./c.wasm" with { type: "wasm" }, see Wasm Module and Component Imports). CM types are written in their WIT spelling.

Wado Type CM Type at Boundary Notes
bool bool Boolean value
char char Unicode scalar value
i8, i16, i32, i64 s8, s16, s32, s64 Signed integers
u8, u16, u32, u64 u8, u16, u32, u64 Unsigned integers
i128, u128 record { low, high } Prelude structs, so each crosses as its own record
f32, f64 f32, f64 Floating point
String string UTF-8 string
List<T> list<T> Dynamic array
TreeMap<K, V> map<K, V> K is bool, char, String, or an integer; a repeated key takes the last pair's value
[T1, T2, ...] tuple<T1, T2, ...> Tuple types
Option<T> option<T> Optional value
Result<T, E> result<T, E> Result type; result<ok> and bare result are the payload-elided forms
struct { ... } record { ... } Record
enum { ... } enum { ... } Enumeration without payloads
variant { ... } variant { ... } Variant/sum type with payloads
flags { ... } flags { ... } Bit flags
type T = U type t = U Newtype, as a named alias of its base; a stdlib one too (ByteList is byte-list)
resource resource Resource handle; owned and borrowed handles both map here
Stream<T> stream<T> Component Model async stream
Future<T> future<T> Component Model async future

A function type has no row: it crosses only as a callback.

f16, bf16 (Half Precision) and v128 have no Component Model type, so they do not cross a component boundary. Neither does a SIMD lane type such as f32x4, since each is a newtype over v128. An export fn whose signature names any of them is a compile error. Every export fn is checked, not only a world's entry point, because each one lands on the component's surface. A component that carries half precision data exports its bits, as a List<u16>.

Rationale: WEP: Half-Precision Primitives.

Callbacks

A function type crosses the Component Model boundary in one place: as a parameter of a #[cm] import, where it is a callback. Anywhere else it is a compile error: in an export fn's parameters or result, in an import's result, and inside a type that crosses, as a field or a payload (Option<fn(..)> included).

#[cm("example:demo/target", linearity = "unrestricted")]
resource Target {
    #[cm("example:demo/target#listen")]
    #[cm_params("self", "listener")]
    fn listen(&self, listener: fn mut(i32));
}

test {
    let target = mock_target();
    let mut seen: List<i32> = [];
    with Target => &mut EchoTarget {} do {
        target.listen(|v| seen.push(v));
    }
    assert seen == [7];                 // `EchoTarget` calls the listener with 7
}

Rationale: WEP: The Web Interface for Wado.

Concurrency Model

Wado follows the Component Model's concurrency model. It has no await: a wait blocks the current task until the value is ready, so a function that waits is not itself async.

Async Imports

A Component Model async func import is an interface operation declared async fn op(...) -> AsyncCall<T> (Async Operations). Calling it starts the call and returns an AsyncCall<T> at once. The caller decides when to wait:

A function or closure that starts an async call also holds the effects these methods declare, so it may later wait on that call without declaring them. A closure is a body of its own: a call it starts grants nothing to the function around it.

use { Client, Request, Response, ErrorCode } from "wasi:http";

fn fetch(req: Request) -> Result<Response, ErrorCode> with Client {
    let call = Client::send(req);   // the request starts; nothing waits yet
    // ... work here runs while the host handles the request ...
    return call.wait();             // blocks until the response arrives
}

test {
    let mut mock = MockClient {};
    let response = with Client => &mut mock do { fetch(new_request()) };
    assert response matches { Ok(_) };
}

An AsyncCall<T> is used once: after wait or cancel it must not be touched again. join only registers the call with the set, so the caller still owes the wait or cancel that ends it.

Handling an Async Operation

A handler method for an async operation is a plain fn that returns T, and it resumes with a T. The caller still receives an AsyncCall<T>, one that has already completed, so its .wait() returns the value at once.

impl Client for MockClient {
    fn send(&mut self, request: Request) -> Result<Response, ErrorCode> {
        resume Result::<Response, ErrorCode>::Ok(canned_response());
    }
}

test {
    let mut mock = MockClient {};
    with Client => &mut mock do {
        let call = Client::send(new_request());
        assert call.wait() matches { Ok(_) };   // completed already
    };
}

Rationale: WEP: Generic AsyncCall<T>.

Async Exports

An export async fn uses the Component Model async calling convention. Its body delivers the result with task return and may keep running afterwards, for example to write a response's trailers.

Streams and Futures

Stream<T> and Future<T> are unbuffered channels. A write blocks until the other end reads, and a read blocks until the other end writes, so the two ends must be driven by different tasks.

Linking and Resources

Attribute Syntax for Component Model Linking

#[cm(...)] links a Wado definition to the Component Model definition it stands for:

// Link an effect interface to a CM interface
#[cm("example:cli/console@0.1.0")]
pub interface Console {
    #[cm("example:cli/console@0.1.0#write-via-stream")]
    fn write_via_stream(data: Stream<u8>) -> Future<Result<(), ErrorCode>>;
}

// Link a resource to a CM resource
#[cm("example:cli/terminal-output@0.1.0#terminal-output")]
pub resource TerminalOutput;

// Link an enum to a CM enum, and each case to its WIT case
#[cm("example:cli/types@0.1.0#error-code")]
pub enum ErrorCode {
    #[cm("io")]
    Io,
    #[cm("illegal-byte-sequence")]
    IllegalByteSequence,
    #[cm("pipe")]
    Pipe,
}

test {
    assert ErrorCode::IllegalByteSequence != ErrorCode::Pipe;   // the CM name changes no Wado name
}

#[cm_params("name", ...)] on an operation gives the CM-side names of its parameters. Without it, each parameter's CM name is its Wado name in kebab-case.

Resource Linearity

A #[cm(...)] resource may declare what may be done with its handle: linearity = "affine" or linearity = "unrestricted". Omitting the field reads as "affine", and a resource without #[cm(...)] is affine.

An affine resource is move-only and carries a drop obligation (see Resource Ownership). An unrestricted one owns nothing, so it is an ordinary copyable value. Assigning or passing one leaves the original usable, and nothing is dropped at the end of a scope.

The representation follows from the linearity. An affine resource crosses the Component Model boundary as an own / borrow handle, an unrestricted one as a plain f64 the host interprets (see Handle Encoding). as converts an unrestricted handle to or from f64, keeping every bit, or upcasts it to a resource it extends. No other cast accepts one, so an affine resource and an unrestricted one never convert into each other.

An unrestricted resource is not a Component Model resource. Its operations are plain CM functions that take the handle as an ordinary parameter, so a #[cm(...)] name written in the [constructor]T, [method]T.m or [static]T.m form is an error on one. classes = "..." (see Resource Inheritance) is accepted only beside linearity = "unrestricted".

Resource Ownership

An affine resource is move-only. Assigning it, passing it by value, returning it, placing it in an aggregate, and calling a method that takes self by value each move it. Using a binding after it has moved is a compile error.

pub resource Counter {
    fn bump(&self);
    fn consume(self);
}

fn eat(c: Counter) { }

fn misuse(c: Counter, d: Counter) {
    eat(c);          // moves `c`
    c.bump();        // ERROR: resource `c` used after it was moved
    d.consume();     // a by-value `self` moves `d` too
}

The check follows control flow. A move on one branch of an if or match counts after the branches meet, unless that branch diverges. A move inside a loop body is a use after move on the next iteration. A new let of the name, or an assignment to it, makes it usable again.

A value that holds an affine resource, directly or through a field, element or payload, is move-only in the same way. It moves as a whole: a resource is not moved out of it on its own, so a consuming method (below) is how one is taken out.

A method consumes a resource through a bare self receiver, and borrows it through &self or &mut self (see Method Receiver: self by Value). Option and Result take self by value in unwrap, expect, unwrap_or, unwrap_err and expect_err, so extracting a resource consumes the container and leaves the resource with one owner.

No Move Out of a Borrow

Returning a resource reached through a & or &mut parameter, &self included, is a compile error. A borrow leaves its referent with its owner, so the returned resource would have a second owner. The rule covers a field read, a dereference, a match binding over the borrowed value, and a let bound from any of these. A resource the function produces itself may be returned.

struct Holder { f: Fields }

impl Holder {
    fn peek(&self) -> Fields {
        return self.f;       // ERROR: cannot move resource `Fields` out of a borrow
    }
}
impl Holder {
    fn into_fields(self) -> Fields {
        return self.f;       // OK: the holder is consumed
    }
}

test {
    let holder = Holder { f: Fields::new() };
    assert holder.into_fields().copy_all().is_empty();
}

Drop

An owned resource that has not moved is dropped when its scope ends, on every path out of it. A move suppresses that drop, so each handle is dropped exactly once. An imported resource drops through the Component Model's resource.drop. A value holding resources drops each of them, a struct's fields in declaration order. A resource value discarded as a statement (Fields::new();, let _ = ...) is dropped there.

A resource's fn drop(self) consumes its receiver, so the scope does not drop it again, and a use after it is a use after move.

A trap does not unwind, so it runs no drops.

Handles at the Boundary

Wado position CM handle
By-value R parameter or result own<R>
&R or &mut R parameter, &self or &mut self borrow<R>
Bare self receiver own<R>, transferring the receiver

A reference crosses a Component Model import only as a borrowed resource handle. An import taking any other reference, such as &String, is a compile error.

Rationale: WEP: Resource Ownership.

Resource Inheritance

resource Child extends Parent declares that a child handle is usable wherever the parent is. The clause stands between the resource's name and its body, and names one resource as the parent. Both resources must declare linearity = "unrestricted", because an upcast copies the handle and an affine one may not be copied. Single inheritance only, and a cycle is an error.

#[cm("example:ui/target", linearity = "unrestricted", classes = "0..=1")]
resource Target {
    #[cm("example:ui/target#add-listener")]
    fn add_listener(&self, kind: String);
}

#[cm("example:ui/widget", linearity = "unrestricted", classes = "1..=1")]
resource Widget extends Target {
    #[cm("example:ui/widget#label")]
    fn label(&self) -> Option<String>;
}

fn use_it(w: Widget) {
    w.add_listener("click");   // inherited, no cast
    let t: Target = w;         // upcast is implicit
}

test {
    let w = 137438953472.0 as Widget;   // class 1, object 0
    let t: Target = w;
    assert t == w && t matches { _: Widget };
}

A generic resource takes no part in extends, on either side.

Subtyping

extends induces Child <: Parent. The relation is reflexive and transitive, and resources extends does not relate are incomparable.

let r: Option<HtmlInputElement> = Option::Some(input);
let n: Option<Node> = r;                    // ERROR: Option is invariant
let r: Option<HtmlInputElement> = Option::Some(input);
let n: Option<Node> = r.map(|el| el as Node);  // OK: each element is upcast
assert n matches { Some(_: HtmlInputElement) };

Methods

A method call resolves statically. recv.m() gathers every m declared along recv's extends chain and every m of a trait impl that applies to it. None is an error, and one resolves the call, upcasting the receiver to the resource that declares it. Two or more is ambiguous: write Declaring::method(&value) or Trait::method(&value) to pick one.

Handle Encoding

An unrestricted handle is a number the host mints: an integer-valued f64 below 2^53, equal to class * 2^37 + index. The class takes 16 bits and the index into the host's object table takes 37. The host hands out one handle per object, and tags each object with the class of the nearest ancestor of its runtime type that the program declares. A type the program does not name therefore reads as its nearest named ancestor.

classes = "lo..=hi" on the #[cm(...)] numbers the classes of an extends tree. A resource's own class is lo, and the resources extending it hold the rest of the range:

A type pattern narrowing to T tests whether the handle's class lies in T's range. A negative value, an infinity and a NaN lie in no range. A type pattern narrows only to a resource that declares classes.

Traits on Handles

Rationale: WEP: Resource Inheritance and Narrowing.