Wado

WEP: WIT Interoperability

Context

Wado's WIT support accumulated in pieces, each driven by a concrete need: the CLI world, the HTTP service world, WASI P3 effects, library publishing. The pieces live in separate WEPs, and the end-to-end goal they serve — producing and consuming an arbitrary Component Model component with no compiler change — was never stated in one place.

This WEP is that place. It states the goal, owns the shape-level decisions several of the point WEPs left open, and specifies the producer side end to end. The consumer side has since grown its own WEPs and is referenced rather than restated here.

WEP Scope
WIT and Wado Mapping The bidirectional type and structure mapping, and export as the CM boundary.
World Conformance and Export Syntax World selection and the export(World::name) mapping syntax.
TIR-Level CM Binding Synthesis Type-driven lift/lower synthesis at the boundary.
WIT Bundling in Component Binaries The component-type custom-section format.
WebAssembly Module Import Support Core-wasm asset import; the CM analogue lives in its own WEP below.
Wasm CM Component Import The consumer side: importing an external .wasm component.
Effect Reconstruction What effects an imported component's interface carries.
Target WASI P3 Only The CM target generation.

Goal

The compiler is WIT-driven in both directions, so that neither producing nor consuming a component requires per-component, per-world, or per-resource code in the compiler.

Producing: the frontend's view of a module — its declared interfaces, exported items, active world, and type table — is emitted as WIT and embedded in the output component, so a Wado artifact is a first-class CM citizen.

Consuming: a component's own type is decoded into compiler IR, and the binding synthesis lifts and lowers against that IR. There is no registry of "known modules": the set of supported worlds is the set of WITs parsed during this compilation.

Decision

interface is the only block declaration

WIT's organizing primitive is interface: a named group of free functions, resources with their methods, and types. Wado has one block keyword matching it. There is no parallel effect Foo { ... } block form.

interface Stdout { ... }

with clauses take interface names and effect tracking is unchanged. The effect keyword survives for exactly one purpose — polymorphic effect parameters, where the binder ranges over effect rows rather than naming a CM interface, a Wado concept WIT has no equivalent for:

fn wrapper<effect E>(f: fn() with E) with E { f(); }

The consequences are that one keyword covers both the import and the export side; that pub interface Geometry { ... } defines and groups while export Geometry in a world publishes, with no producer-side keyword duplication; and that a world's import Foo; is a reference to an interface declaration carrying its CM fully-qualified name, so the FQ is preserved by construction rather than recovered by heuristics.

Cross-package names are bare and unambiguous

WIT distinguishes wasi:filesystem/types from wasi:sockets/types by package. Wado needs no qualification syntax for this, because the two kinds of interface are handled asymmetrically:

The only collision the stdlib ever presented was Types, and both sides are type-only, so it collapses. Should two pub interface declarations ever be forced to share a Wado-side name, this decision is worth revisiting; until then from "<package>" / as qualifications would restate information the declaration already carries.

Purity and effects

WIT has no purity annotation, so purity is inferred rather than declared. An interface holding only types is not effectful: it never appears in a with clause and users use its types directly.

For an interface the host satisfies — WASI — an interface with functions is conservatively effectful at the call site. For an interface an imported component satisfies, effectfulness is reconstructed from that component's own host-leaf imports, so a purely computational component maps to a namespace and needs no with. See Effect Reconstruction.

Producer side

WIT is a frontend fact

Everything WIT describes — declared interfaces, exported items, the active world, the type table — is fully determined by name and type resolution. Monomorphization, lowering, optimization, and codegen contribute nothing that belongs in WIT. So WIT emission takes the frontend's semantic output plus the target world and produces two artifacts: a WIT text document for wado wit, and a component-type custom-section payload derived from that text for wado compile to append.

This keeps codegen's principle intact — it emits the package as is, with no knowledge of earlier phases. Embedding is a postprocess over finished component bytes plus precomputed WIT text, not a codegen concern.

Type mapping and naming

The Wado→WIT direction of the mapping table in WIT and Wado Mapping applies. Closures, polymorphic effect parameters in exported signatures, and generics with bounds that WIT cannot represent are out of scope; encountering one in an exported signature is a compile error naming the offending parameter at its source span.

Wado identifiers become WIT kebab-case (MyApi → my-api, set_level → set-level). Two declarations that collide after kebabification are a compile error surfacing both spans.

The structural constructors — option, list, tuple, result, future, stream — are assembled by one shared rule over a single-level shape classification. Both front-ends, resolved user types and CM-registry signatures, only classify their input and render leaves, so they cannot drift in how a shape becomes WIT. A resource value maps to own<R>, a reference to it to borrow<R>.

Interface grouping and the default interface

Libraries and the anonymous root world

A source file with only export items and no world entry point is a library, declared by [package].lib and built with wado compile --lib. Its world is synthesized from the entry module's export signatures rather than looked up in the stdlib world table. Library exports lift synchronously, and the default allocator is the free-list.

A component's type is anonymous — the binary encodes it as a bare list of imports and exports with no world name, and WIT models a world as a text-level label over that type. Decoding a library component yields world root { … }: the name is discarded and re-synthesized. A library's world therefore carries no identity; only its interface does, and the identity consumers import is the default interface <namespace>:<name>/<name>@<version>.

So the library world is emitted as root, deliberately distinct from the default interface. Worlds and interfaces share one per-package namespace, so naming both after the package is a duplicate-item error that silently drops the embedded section — which is why [package].name may not be root. The world's emit name is textual only; the default-interface FQ is the codegen identity and the component's real export. wado wit --lib renders the same shape, so text and embedded section agree.

A library's exports, plus the transitive closure of their named types, are always grouped into the default interface and exported as an instance, applied alike by the WIT emitter and codegen. WIT forces the interface once a named type is exported: a world export list admits functions and interfaces, never a bare type, so a named type reaches consumers as a reusable entity only through an exported interface. Exports with no named type used to become direct world exports instead. That made one export's shape depend on another's signature, and a Wado consumer imports the default interface by bare name (WEP: A Package's Default Interface Is Its Module), so the library keeps the interface either way.

Codegen mirrors this with no world-specific special-casing. The entry module's exported and transitively referenced named types are registered under the default-interface FQ, and both the export type plan and the type emitter resolve named types purely through that registry, so library-local types need no separate path. The value-type emitter is the same recursive engine that serves WASI imports, generalized over a type sink so it emits either into an instance type or as top-level component types for direct world exports.

Faithful imports

The world's import set must equal what the compiled component actually imports; otherwise the embedded section misrepresents the binary. Deriving the set semantically from effect rows and the type closure is not faithful — it over-includes type-alias-only interfaces and misses implicit imports such as the stderr write on the assert and panic path, which no with clause mentions.

So the complete import and export interface set is built once as structured data at the WIR layer, after dead-code elimination has settled which functions are used, and codegen merely emits it. Each entry carries a kind that tells codegen how to encode it without re-deriving any interface or world decision. wado wit and the embedder read the same plan; the full-scope nested-package bodies stay a separate type closure, which follows aliases so a used alias resolves.

Text and section are different deliverables

A Wado artifact is a full component, not a core module, so it already self-describes: a decoder reconstructs the WIT from the component's own typed imports and exports. A component-type custom section on an already-formed component is carried opaquely and never read by that path.

The embedded section is therefore additive full-fidelity metadata, not the mechanism that makes the binary inspectable. Its value over the intrinsic type is that the intrinsic type is always tree-shaken to the used surface, whereas the payload carries complete upstream interface bodies, exact package versions, and a producers record — the convention that packaging and relink tooling consumes. Round-tripping accordingly has two halves: decoding the component matches wado wit semantically, and the embedded payload decodes standalone as a WIT package.

This splits the two roles cleanly. wado wit writes text, for humans to read and tools to diff; it is an inspection aid, not the deliverable. wado compile embeds the binary section, which is what makes the output composable, publishable, transpilable, and consumable by another Wado compiler.

It also settles scope. wado wit --scope chooses whether interfaces referenced by the world are inlined in full or left as bare references for an external registry to resolve; full is the default and self-describes, local is smaller and focused on the package's own contract. Embedding has no such choice: a component type is structural and self-contained, so the section is always the full closure. That is a Wasm CM property, not a Wado policy, which is why there is no scope knob on wado compile and no manifest key for one.

Embedding policy

Because single-file scripts are first-class, wado compile embeds by default, with or without a manifest. --no-embed-wit is the single explicit opt-out.

-Os is the exception. It is the production build for frontend delivery, where the artifact is transpiled to core Wasm plus JavaScript and the metadata never reaches a CM host, so -Os defaults to no embedding. An explicit --embed-wit forces it back on for the rare build that wants both stripped symbols and a self-describing component.

Embedding is a property of producing a distributable artifact, so it applies to wado compile alone. wado run, wado serve, and wado test compile to an ephemeral in-memory component and never embed, keeping the inner loop fast.

CLI surface

wado wit file.wado                             # WIT text on stdout, full scope
wado wit --scope local -o file.wit file.wado   # user-authored interfaces only
wado wit --world wasi:http/service file.wado   # pick the target world
wado wit --lib .                               # the library world

wado compile file.wado                         # embeds (default)
wado compile --no-embed-wit file.wado          # opt out
wado compile --embed-wit file.wado             # force on, e.g. under -Os

wado wit runs the frontend and stops — no monomorphization, no lowering, no codegen — so it carries no -O level: WIT is a pre-codegen fact. --lib and --world are mutually exclusive.

The positional target follows one rule shared with wado compile: a file is a source, a directory is a package, and omitting it discovers the nearest manifest upward from the working directory. There is no third "manifest file" form; point at the directory instead. When resolution goes through a manifest, the manifest supplies the default interface name from [package].name, and the declared entry picks both source and world in the order command → service → lib. A bare file falls back to its stem for the name and the CLI world.

Consumer side

Reading an external component's own type and building compiler IR from it is specified by Wasm CM Component Import, with effect reconstruction in its own WEP. The full synchronous value-type surface is covered; resources, async value types, and world-level type exports are the remaining gaps, listed there.

Standalone .wit text is not a consumer input. The input is the component. WIT text is an authoring-time concern handled by wado-from-idl, which stays the build-time path for stdlib generation and for projects that want hand-curated bindings.

Remaining specializations

These are the places the compiler still names something specific. None blocks external WIT support; they are listed so the inventory stays honest.

Open questions

Non-goals

Consequences

References