Wado

WEP: WIT and Wado Mapping

Context

Wado compiles to WebAssembly Component Model and needs a clear mapping between WIT (WebAssembly Interface Types) and Wado language constructs. This mapping enables:

Decision

Define a clear bidirectional mapping between WIT constructs and Wado language features. This mapping guides:

  1. WIT generation: Auto-generate WIT from Wado source for embedding in compiled components
  2. WIT consumption: Import external WIT definitions into Wado (via wado-from-idl)
  3. Language design: Ensure Wado constructs align with Component Model concepts

Export Principle

Wado separates Wado visibility (internal package-scope, pub library-scope) from the Component Model boundary flag (export). See WEP: Visibility — internal / pub / export for the model; only export concerns this WEP:

Keyword Scope Purpose
internal Same package Share across files within the package
pub Wado library API Expose to other Wado packages (no CM)
export CM world boundary Expose to external components (⟹ pub)

This separation solves the common problem of "utility modules accidentally becoming public":

// utils.wado - internal utilities
internal fn helper() { ... }     // visible within the package only
internal struct Helper { ... }   // → NOT exposed at CM boundary

// api.wado - public API
export fn run() { ... }          // exposed at CM boundary
export struct Point { ... }      // exposed at CM boundary

Only items explicitly marked with export appear in the generated WIT and Component Model interface.

Exportable Items

export fn process() -> Result<T, E>   // function
export struct Point { x: i32, y: i32 } // record
export variant Shape { ... }           // variant
export enum Color { ... }              // enum
export type ID = String;               // type alias
export interface MyApi { ... }         // interface (see below)

Interface Block

Design Rationale

In CM/WIT, an interface is a collection of related types and functions that forms a reusable unit of API surface. It is not a behavioral abstraction (like Java interfaces or Rust traits) — it is an API boundary that groups a coherent set of functionality for cross-component interop.

In most source languages, this concept is implicit:

Language What serves as "interface" Mechanism
C Header file (.h) Declares types and function signatures; consumers #include the header
Zig Module's pub declarations File = module; public symbols form the API surface
Rust Crate's public API surface pub items in the crate root; no explicit "interface" keyword

In all three cases, the "interface" is simply whatever you made public. There is no separate syntax to declare it. CM differs because components need machine-readable contracts for language-agnostic interop.

Wado's interface block exists solely for CM purposes: it is a grouping syntax that declares which items form a named CM interface. It has no semantic meaning within Wado's type system — no namespace, no purity guarantee, no behavioral contract. Items are defined normally in Wado; the interface block references them by name.

Relationship with effect

Superseded: the effect/interface block-declaration split below is gone. WIT Interoperability unified block declarations into interface; effect survives only for polymorphic effect parameters (<effect E>). The mapping (both becoming WIT interface) still holds; only the dual block keyword is retired.

Both interface and effect map to WIT interface, but they serve different roles in Wado:

Wado WIT Direction Wado semantics
effect interface import Defines capability requirements; functions require with annotation
interface interface export Groups items for CM export; no Wado-level semantics

An effect has deep Wado meaning: it participates in effect tracking and constrains function signatures. An interface is purely organizational — it tells the CM layer how to package exports.

When generating WIT, both become interface:

// effect = import-side interface (Wado semantics: effect tracking)
interface Stdout {
    fn print(s: String);
}

// interface = export-side grouping (Wado semantics: none)
struct Point { x: i32, y: i32 }
fn distance(p1: &Point, p2: &Point) -> f64 { ... }

export interface Geometry {
    Point,
    distance,
}
// Generated WIT
interface stdout {
    print: func(s: string);
}

interface geometry {
    record point { x: s32, y: s32 }
    distance: func(p1: point, p2: point) -> f64;
}

A &T over a value type (here &Point) is transparent at the CM boundary: Wado value semantics copy the argument, so it maps to the bare value type (point), not borrow<point>. borrow<T> / own<T> are reserved for resources — a value-type record/variant/enum/flags can never be borrowed. See Reference → CM mapping.

Default Interface (Implicit Grouping)

Bare export declarations (not inside an explicit interface block) are automatically collected into a default interface. The default interface is named after [package].name from wado.toml, or derived from the entry file name in single-file mode.

// [package].name = "geometry"
export struct Point { x: i32, y: i32 }
export fn distance(p1: &Point, p2: &Point) -> f64 { ... }
export fn origin() -> Point { ... }
// Generated WIT — default interface named "geometry"
interface geometry {
    record point { x: s32, y: s32 }
    distance: func(p1: point, p2: point) -> f64;
    origin: func() -> point;
}

world geometry {
    export geometry;
}

The world and the default interface both derive from [package].name, so a single-interface package produces world geometry { export geometry; }. WIT keeps worlds and interfaces in separate namespaces, so the shared name is valid and unambiguous — it is the common single-package shape (e.g. as produced by cargo-component). It reads slightly redundant but needs no disambiguation.

When only functions are exported (no referenced user types), they become direct world exports instead of forming an interface:

export fn run() { ... }
world my-app {
    export run: func();
}

This also applies to world-conformance entry points like export fn run() (wasi:cli/command) and export fn handle(...) (wasi:http/service), which are always direct world exports regardless of other exports.

Why named types force an interface

The split is not "CM forbids direct exports with types" — a world can define types locally (a world item can be a type definition) and export functions that reference them. The real reason is reuse: a world's export list admits only functions and interfaces, never a bare type, so a named type can be surfaced to consumers as a reusable, use-able entity only through an exported interface. A type defined directly in a world is world-local and invisible to other components.

So the rule follows the reusability goal, not a representation limit:

When a component needs to export multiple interfaces, or when the default grouping is not sufficient, use explicit interface blocks. The block lists names of items defined elsewhere — it does not contain definitions:

// Define items normally
struct Point { x: i32, y: i32 }
struct Color { r: u8, g: u8, b: u8 }
fn distance(p1: &Point, p2: &Point) -> f64 { ... }
fn blend(a: &Color, b: &Color) -> Color { ... }
fn origin() -> Point { ... }

// Group into separate CM interfaces
export interface Geometry {
    Point,
    distance,
    origin,
}

export interface Colors {
    Color,
    blend,
}
interface geometry {
    record point { x: s32, y: s32 }
    distance: func(p1: point, p2: point) -> f64;
    origin: func() -> point;
}

interface colors {
    record color { r: u8, g: u8, b: u8 }
    blend: func(a: color, b: color) -> color;
}

world my-app {
    export geometry;
    export colors;
}

Items listed in an export interface are exported through that interface regardless of their original visibility. They do not need the export keyword individually.

#![no_default_interface]

By default, bare export items form a default interface. The #![no_default_interface] attribute disables this, requiring all exports to be placed in explicit interface blocks:

#![no_default_interface]

// World-conformance entry points remain direct world exports
export fn run() with Stdout { ... }

// Other items must be explicitly grouped
struct Point { x: i32, y: i32 }
fn distance(p1: &Point, p2: &Point) -> f64 { ... }

export interface Geometry {
    Point,
    distance,
}
world my-app {
    import wasi:cli/stdout@0.3.0;
    export run: func();
    export geometry;
}

interface geometry {
    record point { x: s32, y: s32 }
    distance: func(p1: point, p2: point) -> f64;
}

With #![no_default_interface], a bare export struct Point (not in any interface block) is a compile error. This ensures the developer is explicit about which CM interface each item belongs to.

Exporting Effects

An effect with the export keyword becomes an exported CM interface. This is for components that provide a capability for other components to consume:

// Producer component: provides logging capability
export interface Logger {
    log,
    set_level,
}

fn log(message: String) {
    // implementation
}

fn set_level(level: i32) {
    // implementation
}
// Generated WIT
interface logger {
    log: func(message: string);
    set-level: func(level: s32);
}

world logging-service {
    export logger;
}

The consumer imports this as a regular effect:

// Consumer component
use { log } from "wasi:logging";  // or whatever the import path is

fn do_work() with Logger {
    log("processing...");
}

The effect keyword (rather than plain interface) signals to the consumer that these functions have side effects and require with annotations. This distinction is a Wado-level concept — WIT has no purity annotation (see Component Model Issue #321).

Transitive Type Inclusion

Types referenced in exported function signatures are automatically included in the interface, even if not explicitly listed:

struct Point { x: i32, y: i32 }
fn origin() -> Point { return Point { x: 0, y: 0 }; }

export interface Geometry {
    origin,
    // Point is automatically included because origin() returns it
}

This avoids forcing developers to manually list every type dependency.

Shared Types Across Interfaces

A named type referenced by two exported interfaces must remain one type, or consumers see two structurally-equal but distinct WIT types and lose type identity across the boundary. WIT models this with use: the type is defined in exactly one owning interface, and every other interface references it via use owner.{T}.

struct Point { x: i32, y: i32 }
fn origin() -> Point { ... }
fn translate(p: Point, dx: i32, dy: i32) -> Point { ... }

export interface Geometry { origin }      // owns Point (first referencer)
export interface Transforms { translate } // borrows it
interface geometry {
    record point { x: s32, y: s32 }
    origin: func() -> point;
}

interface transforms {
    use geometry.{point};
    translate: func(p: point, dx: s32, dy: s32) -> point;
}

The owning interface is chosen deterministically (first referencer in module-then-declaration order). Transitive inclusion defines the type once in the owner; later interfaces emit a use instead of a second definition. The same applies to the implicit default interface when it coexists with explicit ones.

WIT to Wado Type Mapping

Primitive Types

WIT Wado Notes
bool bool Direct mapping
s8, s16, s32, s64 i8, i16, i32, i64 Signed integers
u8, u16, u32, u64 u8, u16, u32, u64 Unsigned integers
f32, f64 f32, f64 Floats
char char Unicode scalar value
string String UTF-8 string

Compound Types

WIT Wado Notes
list<T> List<T> Dynamic array
option<T> Option<T> Optional value
result<T, E> Result<T, E> Result type
tuple<T, U, ...> [T, U, ...] Tuple type

User-Defined Types

WIT Wado Notes
record struct Named fields
variant variant Tagged union with payloads
enum enum Discriminated values without payloads
flags flags Bitfield
resource resource Handle type
type alias type Type synonym

The producer side (wado wit) emits all of these, including flags and the reconstruction of resource interfaces (methods, constructors, statics, and borrow of self). Consuming an external resource from a .wasm/.wit the compiler has never seen is the open item tracked in WIT Interoperability.

Reference → CM mapping

Wado references (&T, &mut T) have no value-type equivalent in the Component Model, because Wado values are copied across the boundary:

Wado WIT Notes
&Resource borrow<resource> A non-owning resource handle
Resource (owned) own<resource> Ownership transfers across the boundary
&T / &mut T (value) T Transparent; the value type is copied

Only resources have borrow/own. A & over a value type (record/variant/enum/flags/primitive/container) is peeled and the bare value type is emitted.

Functions

WIT Wado Notes
func(a: t1) -> t2 fn f(a: t1) -> t2 Function signature
func() -> result<T, E> fn f() -> Result<T, E> Fallible function
async function async fn Async in WASI P3

WIT Structure Mapping

Package

package wado:my-app@1.0.0;

Derived from:

World

world my-app {
    import wasi:cli/stdout@0.3.0;
    import wasi:cli/stderr@0.3.0;
    export my-api;
    export run: func();
}

Wado equivalent:

// Imports derived from effect usage
use {Stdout} from "wasi:cli";

// Explicit interface export
export interface MyApi { ... }

// Direct function export
export fn run() with Stdout {
    println("Hello!");
}

Auto-Generation Strategy

World Generation

When no explicit world is declared, generate one from:

  1. Imports: Collect from CmInterfaceRegistry (used WASI interfaces via effects)
  2. Exports: Collect from items marked with export
┌─────────────────────┐    ┌─────────────────────┐
│    CmInterfaceRegistry     │    │   export items      │
│  (used effects)     │    │  (fn, struct, etc.) │
└─────────┬───────────┘    └──────────┬──────────┘
          │                           │
          └───────────┬───────────────┘
                      ▼
              ┌───────────────┐
              │ WIT Generator │
              └───────┬───────┘
                      ▼
              ┌───────────────┐
              │   Resolve     │
              │   (wit-parser)│
              └───────┬───────┘
                      ▼
              ┌───────────────┐
              │ embed_metadata│
              └───────────────┘

Export Collection Rules

  1. Explicit export required: Only items with export keyword are included
  2. Transitive types: Types referenced in exported signatures are automatically included
  3. Interface grouping: Explicit export interface creates named interfaces; top-level exports form implicit interface

Package Naming

Derived from wado.toml when present:

Implementation Plan

Phase 1: Basic Embedding

Phase 2: Type Export

Phase 3: Interface Syntax

Phase 4: Export Effect

Phase 5: CLI Integration

Consequences

Positive

Negative

Trade-offs

References