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:
- Runtime introspection of component types
- Tooling integration (composition, documentation generation)
- IDE support for component consumers
- Registry publishing with type information
Decision
Define a clear bidirectional mapping between WIT constructs and Wado language features. This mapping guides:
- WIT generation: Auto-generate WIT from Wado source for embedding in compiled components
- WIT consumption: Import external WIT definitions into Wado (via
wado-from-idl) - 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:
- Functions are always directly exportable; their parameter/result types can
be inline-structural (
list,tuple,option,result, anonymous record shapes). - Any export that references a named user type (
struct/variant/enum/flags/ type alias) is grouped into the default interface so the type is exported as a named, reusable definition. Keeping it as a world-local type would make it uncallable-by-name from other components.
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:
- Namespace:
wado(fixed, or configurable) - Name: entry module name or explicit declaration
- Version: optional, from project config
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:
- Imports: Collect from
CmInterfaceRegistry(used WASI interfaces via effects) - 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
- Explicit
exportrequired: Only items withexportkeyword are included - Transitive types: Types referenced in exported signatures are automatically included
- Interface grouping: Explicit
export interfacecreates named interfaces; top-level exports form implicit interface
Package Naming
Derived from wado.toml when present:
- WIT package:
{namespace}:{name}@{version}(e.g.,myorg:geometry@1.0.0) - Without
namespace:local:{name}@{version}(non-publishable packages) - Without
wado.toml(single-file mode):local:{filename}(e.g.,local:helloforhello.wado)
Implementation Plan
Phase 1: Basic Embedding
- [ ] Add
wit-componentdependency - [ ] Generate WIT text from
CmInterfaceRegistry+export fn - [ ] Parse with
wit-parserto getResolve - [ ] Call
embed_component_metadata()in codegen
Phase 2: Type Export
- [ ] Support
export struct/variant/enum/type - [ ] Collect transitive type dependencies
- [ ] Generate WIT record/variant/enum definitions
- [ ] Default interface generation (named after
[package].name)
Phase 3: Interface Syntax
- [ ] Parse
export interface Name { item, ... }blocks (name-listing syntax) - [ ] Validate listed names resolve to defined items
- [ ] Generate named interfaces in WIT
- [ ] Support multiple interfaces per module
- [ ]
#![no_default_interface]attribute
Phase 4: Export Effect
- [ ] Parse
export effect Name { item, ... }blocks - [ ] Generate exported interfaces from effects
- [ ] Ensure consumer-side effect tracking works across package boundaries
Phase 5: CLI Integration
- [ ] Add
--emit-witflag to output WIT text - [ ] Add
--no-wit-embedflag to disable embedding - [ ] Consider
wado witsubcommand for WIT inspection
Consequences
Positive
- Components are self-describing
- Better tooling integration
- Enables component composition
- Registry-ready artifacts
- Clear separation between internal (
pub) and external (export) visibility interfaceas CM-only grouping keeps Wado's type system simple (no second namespace mechanism)- Default interface from bare
exportcovers the common case with zero syntax overhead - Name-listing syntax makes the
interfaceblock a clear manifest of what's exported export effectreuses the existing effect concept for exported capabilities
Negative
- Slightly larger binary size (custom section)
- Additional dependency (
wit-component) - Must keep WIT generation in sync with codegen
Trade-offs
interfacehas no Wado-level semantics (no namespace, no purity). This is simpler but meansinterfaceis purely a CM concept that Wado developers only encounter when thinking about component boundaries. This is acceptable because most languages do not have an explicit "interface" concept either — it is an emergent property of public declarations.- Default interface naming depends on
[package].name, creating a coupling between the manifest and generated WIT. This is intentional — the package name is the natural identity for a component's API surface. #![no_default_interface]is opt-in strictness. The default behavior (implicit grouping) is convenient for simple components; the attribute is for advanced use cases with multiple interfaces.
