WEP: WIT Interoperability
Context
Wado has accumulated WIT-related design and implementation in pieces, driven by
concrete needs (CLI world, HTTP service world, WASI P3 effects, library
publishing). The pieces are scattered across multiple WEPs, and the end-to-end
goal — being able to consume an arbitrary .wasm component (or .wit package)
that the compiler has never seen before — has never been written down in one
place.
This WEP collects the existing pieces, captures the current state of the
implementation, and states the long-term goal explicitly. It also makes a
shape-level decision (unifying effect and interface) that several earlier
WEPs left open or implicitly inconsistent.
Existing WEPs in this area
| WEP | Scope |
|---|---|
| WIT and Wado Mapping | Bidirectional type/structure mapping. export as the CM boundary (visibility model: Visibility WEP). Originally split interface and effect; superseded by the unification decision below. |
| World Conformance and Export Syntax | contract <World>; declaration and export(World::name) mapping syntax. |
| TIR-Level CM Binding Synthesis | Type-driven lift/lower binding synthesis at the TIR layer. |
| WIT Bundling in Component Binaries | The producer side: embed WIT into output .wasm via component-type custom section. |
| WebAssembly Module Import Support | Phase 1 (core wasm asset import) is delivered. Phase 2 (CM-boundary external .wasm) is now subsumed by this WEP. |
| Target WASI P3 Only | The CM target is fixed to WASI Preview 3. |
Goal
Make the Wado compiler fully WIT-driven so that an arbitrary CM component can be consumed without compiler changes. Concretely:
- The user writes
use { Foo } from "./external.wasm" with { type: "wasm" };(or depends on a packaged component viawado.toml). - The compiler reads the
component-typecustom section embedded in the component (the section described by WIT Bundling; the producer can compute the WIT —wado wit, Phase 1 — but embedding it into the binary is Phase 2, not yet done). - The use resolver constructs Wado IR (worlds, interfaces, resources, types) directly from that embedded WIT.
- The CM binding synthesis lifts/lowers values at the boundary based on that IR, without any per-component, per-world, or per-resource hand-coding in the compiler.
There is no separate registry of "known WASI modules". The set of supported worlds equals the set of WITs the compiler has parsed during this compilation.
Key Decision: Unify effect block declarations into interface
WIT's organizing primitive is interface: a named group of free functions,
resources (with their methods), and types. Wado previously had effect as a
parallel block-declaration keyword (effect Foo { ... }). For WIT
interoperability the block form collapses into interface:
effect Foo { ... }→interface Foo { ... }.
interface Stdout { ... } // formerly effect Stdout { ... }
The with clause continues to take interface names. Effect tracking
semantics are unchanged.
The effect keyword is retained for one purpose: polymorphic effect
parameters (<effect E>). Here E is a type-level binder over effect
rows — a different concept from a CM interface, and one WIT does not have
an equivalent for. Keeping the keyword for this case makes the distinction
explicit and avoids overloading interface with a meaning it does not
have in WIT.
fn wrapper<effect E>(f: fn() with E) with E { f(); }
What this resolves
- One keyword (
interface) for both the import and export block forms, matching WIT's vocabulary directly.effectkeeps a narrow, well-defined role (polymorphic effect parameters). pub interface Geometry { ... }defines and groups;export Geometryfrom a world publishes it. No producer-side keyword duplication.- World import information loss is gone:
import Fooin a world is a reference to aninterface Foodeclaration carrying#[cm(...)], so the FQ name is preserved by construction.
Cross-package disambiguation (resolved by omission)
WIT distinguishes wasi:filesystem/types from wasi:sockets/types by
package. Wado handles this asymmetrically:
- Function-bearing WIT interfaces have a Wado-side
pub interface Foowith#[cm(...)]carrying the FQ. Worlds reference them by bare name (import Stdout;); the FQ is recovered from the interface declaration, so there is no need forfrom "..."qualification. - Type-only WIT interfaces (e.g.
wasi:filesystem/types,wasi:sockets/types) have no Wado-sidepub interfacedeclaration; their resources and types reach call sites viause { Descriptor } from "wasi:filesystem/types.wado".wado-from-idlomits these from world declarations entirely. The previousimport Types { ... }blocks (which produced the cross-package collision) are gone.
Result: import / export in worlds is always bare and unambiguous, and
the proposed from "<package>" / as qualifications are unnecessary.
Pure interfaces
WIT has no purity annotation. An interface that contains only types (no
functions) is simply not effectful — it never appears in a with clause, and
users use its types directly. No new syntax is needed. An interface with
functions is conservatively treated as effectful by the call site.
Superseded for imported .wasm components by
Effect Reconstruction from CM Component Imports.
A fused component's interface is
effectful only insofar as the component's own host-leaf imports are — a
purely-computational import needs no with. The conservative rule stands
unchanged for host-satisfied (WASI) interfaces.
Migration Plan
The migration runs on a single feature branch and lands as one merge:
- [x] Replace
effect <Name> { ... }withinterface <Name> { ... }acrosslib/wasi/**,lib/core/**,wado-compilerinternals, examples, fixtures, and docs. - [x] Update
wado-from-idlso its generated stdlib emitsinterfaceblocks. - [x] Add
cm_interface_fqtoWorldImportInfoand populate it from the referenced interface's#[cm(...)]. - [x] World imports/exports are bare WIT-faithful interface refs (
import Foo;/export Foo;); the brace-block form has been removed. - [x] Retain the
effectkeyword for polymorphic effect parameters (<effect E>). The block-declaration unification does not extend to type parameters: an effect variable binds an effect row, which is a Wado-specific concept WIT has no equivalent for. The current parser support stays. - [x] Update WEP: WIT and Wado Mapping to mark the interface/effect split as superseded.
There is no compatibility shim and no deprecation period. Wado is pre-stable and this is a source-level rename plus a small registry change; a single landing keeps the codebase consistent.
Why no from "<package>" / as qualifications
The earlier draft of this WEP proposed cross-package qualifications
(import { Types } from "wasi:filesystem", import { Types as SocketsTypes } from "wasi:sockets") for resolving local-name collisions in worlds. After
auditing the stdlib and consumers, we removed that requirement:
- The only collision in the current stdlib was
Types(filesystem vs sockets), and neither side declarespub interface Typesin Wado: those WIT interfaces are type-only, and their resources/types reach call sites through ordinaryuse { Descriptor } from "wasi:filesystem/types.wado".wado-from-idltherefore omitsimport Types;from the world altogether, collapsing the collision. - World imports inside
wado-compilerare consumed exclusively byWorldInfo::imports_interface(name)(e.g., the kiln gating). Aliases would have no consumer;fromwould only restate information already carried by the referencedpub interface Foo's#[cm(...)].
If a future WIT file forces a non-removable collision (two pub interface
declarations sharing a Wado-side name), reconsider; until then the bare
form is unambiguous and minimal.
Non-Goals
- Supporting WASI Preview 1 or Preview 2. The CM target is P3.
- Parsing standalone
.wittext files at runtime as a primary input. The primary input is the embeddedcomponent-typesection. Standalone.witis an authoring-time concern handled bywado-from-idl. - Replacing
wado-from-idl. It remains the build-time path for stdlib generation and for projects that want hand-curated.wadobindings.
Current State
What is already WIT-driven
| Aspect | Mechanism |
|---|---|
| World definitions | Parsed from lib/wasi/**/worlds.wado; #[cm("...")] carries the FQ name. |
| Interface → CM import binding | Each method declares #[cm_import("wasi:cli/stdout@...#write-via-stream")]. |
| Resources, structs, enums, variants, flags, newtypes | Registered from stdlib .wado parsing; #[cm(...)] carries CM-side names. |
Entry function names (run, handle) |
Pulled from the world's declared export items, not hardcoded strings. |
| HTTP detection | Namespace prefix wasi:http/ plus return-type shape (Result<Response, _>). |
| CM lift/lower | synthesize_lift / synthesize_lower_to_flat are recursive and type-driven. |
(interface, name) scoping for type lookups |
source_interface field disambiguates same-named types across interfaces (e.g. two ErrorCodes). |
What is still hardcoded or specialized
- Default world fallback:
package.rsuses"wasi:cli/command"when no--worldflag is given. Acceptable as a default; not a structural problem. - Synthetic
TEST_WORLDconstant inworld_registry.rs. Used for the test harness; does not block external WIT support. stdlib::ALL_WASI_MODULESis aninclude_str!-driven static list. Adding a new WASI/CM library currently requires either putting the binding.wadoin this list or feeding it throughCompilerHost. Neither path reads embedded WIT directly.wado-compilerstill relies onwado-from-idl-generated.wadofiles as the source of truth; no path reads embedded WIT from external.wasmyet (consumer side, still open).wit-parser,wit-encoder, andwit-componentare all inwado-compiler's[dependencies]for the producer side; the consumer-sidecomponent-typereader still reuses these crates when it lands.- Producer-side WIT embedding (the
component-typecustom section described in WIT Bundling) is done (Phase 2):wado compileembeds by default viawit_bundle::embed_component_type. Note the Phase 2 finding below — the Wado component is already self-describing, so the section is additive full-fidelity metadata rather than the inspection mechanism.
Resolved since the first draft: HTTP handler specialization in
codegen/component.rs is gone — CM imports/exports are now emitted from the
WIR import plan and registry-derived descriptors with no HTTP/world literals
(see §"Codegen Genericization", P1–P5).
Stale items already cleaned up
- The
__pending_trailers_txWasm global is gone. Trailers handling no longer relies on a global; the previous reference in WEP: TIR-Level CM Binding Synthesis has been removed. synthesize_result_export_adapterhas been renamed tosynthesize_result_export_binding. Naming is now consistent withsynthesize_void_export_bindingandsynthesize_general_export_binding.
Producer Side: WIT Generation and Embedding
This section is the detailed design for the producer-side roadmap item
"embed component-type in output". It implements the format spec from
WIT Bundling and the Wado↔WIT mapping
from WIT and Wado Mapping. Anything
left open in those two documents (default behavior, CLI shape, where the
generator hooks into the pipeline) is resolved here.
Position in the pipeline
WIT generation is a function of the frontend output. Describing a component's interface — declared interfaces, exported items, the active world, the type table — is fully determined by name resolution and type resolution. Monomorphization, lowering (TIR → NIR → WIR), optimization, and codegen do not contribute information that belongs in WIT.
The producer side therefore takes Semantics
plus the target world FQ and produces:
- A WIT text document (consumed by
wado wit). - A
component-typecustom section payload, derived from that WIT text viawit-parser+wit-component::metadata::encode(embedded bywado compile, which is default-on).
Codegen is unaffected. The codegen.rs principle ("emits Package as is,
without knowledge of earlier phases") still holds; the WIT bundle is
appended as a postprocess step that takes the completed component bytes
plus the precomputed WIT text.
Semantics additions
Semantics is the contract output of the frontend, so every fact needed
to emit WIT lives on it. Phase 0 landed the registry accessors; Phase 1's
emitter read interfaces and exported items off those registries and the
loaded TIR directly, so no further Semantics accessor was required.
Phase 0 (landed):
| Accessor | Returns |
|---|---|
Semantics::world_registry() |
Option<&'static WorldRegistry> — the parsed world table. |
Semantics::cm_interface_registry() |
Option<&'static CmInterfaceRegistry> — the parsed CM interface table. |
Both registries are already built by Elaborator::annotate_modules (which
calls CmInterfaceRegistry::build_from_stdlib, an OnceLock-cached
singleton) and live on AnnotateState. The accessors surface them
without re-running stdlib parsing, so LSP, wado wit, and batch
compilation share the same instance. FlatPackage / Package /
NirPackage continue to carry the same &'static references they
already held — Phase 0 added access without changing how the data is
threaded.
Phase 1 (landed, simpler than planned): the proposed Semantics::interfaces()
/ exported_items() indices were not needed. emit_wit_text reads the
exported items off the loaded TIR modules and the interface bodies off the
Phase 0 registries directly, so no new Semantics accessor was added. The
remaining inputs are WitEmitOptions fields threaded from the CLI:
default_interface_name ([package].name or entry-file stem) and
world_imports (the faithful WIR import plan — a project/codegen fact, not a
frontend one).
The wir-build / codegen path continues to consume Package as today. The
new WIT path is a sibling reader of Semantics; it does not touch
Package.
Module layout
Two new modules under wado-compiler/src/:
wit_emit.rs—emit_wit_text(&Semantics, &WitEmitOptions) -> Result<String, WitEmitError>.wit_bundle.rs—embed_component_type(component_bytes: &[u8], &Semantics, &WitEmitOptions) -> Result<Vec<u8>, WitEmitError>.
wit_emit depends on wit-encoder for WIT text production. wit_bundle
depends on wit_emit for text, parses it with wit-parser into a
Resolve, encodes the component-type via wit-component::metadata::encode,
and appends the custom section to the component the codegen already
produced.
wit-encoder and wit-component are pinned in [workspace.dependencies]
alongside the existing wit-parser (all at generation 0.246, matching
wasmtime's tree). wado-compiler adds them to its own Cargo.toml in
Phase 1.
Type mapping
Implements the table in WIT and Wado Mapping §"WIT to Wado Type Mapping". Direction here is Wado → WIT (the existing table reads in both directions, but the producer only needs one).
Out of scope for the first cut: closures, polymorphic effect parameters in exported signatures, generics with non-WIT-representable bounds. Encountering one in an exported signature is a compile error with a diagnostic that names the offending parameter and points at its source span.
Name conversion: Wado identifiers (distance, MyApi, set_level) become
WIT kebab-case (distance, my-api, set-level). Conflicts after
kebabification (e.g. two declarations colliding) are a compile error at WIT
emit time with both source spans surfaced.
The structural constructors (option / list / tuple / result /
future / stream) are assembled by one shared rule (wit_emit::assemble
over a single-level CmShape classification). The two front-ends — resolved
types (user exports) and CM-registry AST signatures (full-scope interface
reconstruction) — only classify their input and render leaves, so the two
paths cannot drift in how a shape becomes WIT. own<R>/borrow<R> map from a
resource value / &resource respectively.
Interface grouping
Drives off the pub interface and export interface shapes already
established in WIT and Wado Mapping:
- Bare
export fn/export struct/ ... form a default interface named afterdefault_interface_name. If only functions are exported (no types), they become direct world exports instead. export interface Foo { item1, item2, ... }blocks group named items into one WIT interface. Items are defined elsewhere; the block lists names.- World-conformance entry points (
fn runforwasi:cli/command,fn handleforwasi:http/service) are always direct world exports. - Types referenced transitively by exported signatures are pulled into the owning interface even if not explicitly listed.
#![no_default_interface]disables the default-interface fallback; every non-entry-point export must live in an explicitexport interface.- When no entry point is present at all, the world is emitted empty and the interfaces stand alone in the package (see "World-less libraries").
Imported interface resolution and scope
For every import Foo; in the target world, the emitter looks up the
referenced pub interface Foo via Semantics::interfaces, reads its
#[cm("...")] FQ name, and uses that FQ name in world { import ...; }.
What the WIT document contains for each referenced interface depends on the requested emit scope:
| Scope | Behavior |
|---|---|
full |
Inline the full body of every interface referenced by the world (user-authored and stdlib WASI/CM). |
local |
Inline only user-authored interfaces. Stdlib references appear as import wasi:cli/stdout@<v>; with no body. |
full matches the wit_component::metadata::encode toolchain convention
and produces a self-describing component that wasm-tools component wit
can decode without a registry. local produces a smaller WIT focused on
the package's own contract; consumers need an external WIT registry to
resolve wasi:* references. Both are well-defined; the choice is
deployment-policy, not technical.
The default scope is full. The scope table above applies to wado wit
text (wado wit --scope <full|local>).
Revised in the Phase 2 finding: scope does not apply to embedding. An embedded
component-typemust be self-contained, so it is alwaysfull; alocalsection is not encodable. The earlier plan for a[wit].scopemanifest key and an--embed-wit=<scope>resolution order is therefore dropped — see §"Phase 2 finding" and the Phase 3 entry.
Stdlib interfaces under lib/wasi/** are emitted with the same machinery
as user interfaces; there is no special-case "stdlib" code path. Each
pub interface is a uniform building block.
Faithful imports: a WIR-level component interface plan
The world's import set must equal what the compiled component actually
imports — otherwise the embedded component-type (Phase 2) misrepresents
the binary. Deriving the set semantically (effect rows + type closure) is
not faithful: it over-includes type-alias-only interfaces (wasi:clocks/types,
duration = u64) and misses implicit imports (wasi:cli/stderr, written by
the assert / panic path with no with clause).
Decision: build the complete import/export interface set once, as structured
data, at the WIR layer (after DCE, so used_wasi_functions is populated),
and have codegen merely emit it. This restores the codegen.rs principle
("emit Package as is, without knowledge of earlier phases"). wado wit and
the Phase 2 embedder read the same plan; full-scope nested-package bodies
stay a separate type-closure (which follows type aliases, so use duration
resolves).
Done (R1–R3, each gated by the full E2E suite):
- The plan is built in
wir_build::component_imports(resolve_import_plan), stored inWirPackage::{import_plan, imported_cm_interfaces}. Every entry carries anImportKind(SharedTypes,FunctionInterface,ResourceUsingInterface,ResourceSource,ResourceGetter,HttpTypes,HttpClient) so codegen knows the encoding without re-deriving any interface/world decision.tests/wit_import_plan.rsasserts the plan equals the compiled component's CM imports across the CLI corpus, the HTTP service, and a pure-compute program. - The WIT emitter reads it via
WitEmitOptions::world_imports;wado witimports are faithful (implicitwasi:cli/stderrin, alias-onlywasi:clocks/typesout) andfullscope still self-describes.
Embedding target and format
Wado emits a complete component in a single pass, so there is no
intermediate "core module with bindings" step analogous to
wit-bindgen + wasm-tools component new. The component-type custom
section is therefore added to the outer component, not to a nested core
module.
The section payload matches wit_component::metadata::encode() exactly:
- A serialized component binary typing the world.
- A
wit-component-encodingsubsection declaring UTF-8 (Wado's native string encoding). - A
producerssubsection identifying the Wado compiler version.
Consumers run wasm-tools component wit output.wasm, but note (per the Phase 2
finding above) that for a Wado component this reconstructs WIT from the
component's own type, not from the appended component-type section. The
embedded section is consumed by tools that read it explicitly (wkg,
wasm-tools metadata, relink flows) and decodes standalone as a WIT package.
Round-trip verification therefore splits: (a) decode(component) matches
wado wit semantically, and (b) the embedded payload decodes as a
DecodedWasm::WitPackage. Both are covered by tests/wit_bundle.rs.
Embedding policy
Two roles, clearly separated:
wado witwrites the WIT as text, for humans to read and for tooling to diff. It is an inspection aid, not the deliverable.wado compileembeds the binarycomponent-typesection. This is what makes the output a first-class Component Model citizen — composable withwac, publishable withwkg, transpilable withjco, and consumable by another Wado compiler.
Because Wado treats single-file scripts as first-class, wado compile
embeds WIT by default — with or without a wado.toml. --no-embed-wit
is the single, explicit opt-out. (The section is always the self-contained
full closure; there is no scope knob — see the Phase 2 finding.)
-Os is the exception: it is the production build for frontend delivery
(jco-transpiled to core Wasm + JS for the browser), where the WIT
metadata is dead weight that never reaches a CM host. So -Os defaults
to no embedding, exactly as if --no-embed-wit were passed. An explicit
--embed-wit still forces embedding under -Os for the rare case that
wants both the smallest symbols and a self-describing component.
Embedding is a property of producing a distributable artifact, so it
applies to wado compile only. wado run, wado serve, and wado test
compile to an ephemeral in-memory component and never embed WIT, keeping
the inner dev loop small and fast.
CLI surfaces
wado wit — standalone WIT text:
wado wit file.wado # stdout, default scope (full)
wado wit --scope local -o file.wit file.wado # file output, user-only WIT
wado wit --world wasi:http/service file.wado # pick the target world
Backed by wit_emit::emit_wit_text. Runs wado_compiler::semantics() —
the public async Semantics-only entry, which parses, loads, and analyzes,
then stops; no monomorphize, no lower, no codegen. Mirrors wado dump in
pipeline depth, but carries no -O level: WIT is a pre-codegen fact, so
the optimization level is irrelevant. Takes a single positional target —
a .wado file or a directory (see "Input resolution"); passing more than
one is an error. --world reuses the same flag and
default (wasi:cli/command, overridable by the __DATA__ world) as
wado compile. --scope is optional and defaults per the resolution
order above. When the file has no world entry point (a library-shaped
.wado with only pub interface / bare export items), the emitter
produces an empty world — see "World-less libraries" under Open
Design Questions.
wado compile — embed WIT in the compiled component (default on):
wado compile file.wado # embeds (default)
wado compile --no-embed-wit file.wado # explicit opt-out
wado compile --embed-wit file.wado # force on (e.g. under -Os)
Revised in the Phase 2 finding: the embedded section is always the self-contained full closure, so
wado compilehas no scope value. Both--embed-witand--no-embed-wittake no value and are mutually exclusive. The--embed-wit=<scope>/[wit].scopeknobs sketched in this section's earlier draft were dropped;local/fullsurvives only onwado wit --scope(text output).
Input resolution
wado wit does not take a .wado file only. The positional input is a
target that resolves through manifest::resolve_input:
| Input | Resolution |
|---|---|
foo.wado |
A file is a single Wado source — analyze it directly. |
| a directory | A directory is a Wado package — load <dir>/wado.toml and resolve the entry source from it. |
| (omitted) | Discover the nearest wado.toml upward from the cwd. |
file = source, dir = package is the whole rule — no third "manifest
file" form. A bare wado.toml path is not a target; point at its directory
instead. All three forms already work in resolve_input, and wado compile routes through the same helper, so wado wit and wado compile
share one input model with no new plumbing.
When resolution goes through a manifest, the CLI loads it (via
load_nearest_manifest) to source two inputs that single-file mode lacks:
WitEmitOptions::default_interface_name from [package].name, and the
--scope default from [wit].scope. A bare .wado file falls back to the
file stem for the name and full for the scope.
World selection follows from the same resolution: --world wins when
given; otherwise the manifest's declared entry picks both the source and
the world, in precedence order command → service → lib. A lib-only
project resolves to the library case (empty world; see "World-less
libraries").
Implementation phases
Each phase ends with green E2E tests for the listed fixtures.
-
[x] Phase 0 — Dependencies and
Semanticsgroundwork. Added the wasm-tools WIT crates to[workspace.dependencies]; exposedSemantics::{world_registry, cm_interface_registry}()over theOnceLock-cached&'staticregistries built byElaborator::annotate_modules; renamedWasiRegistry→CmInterfaceRegistryand the CM-generalwasi_*/Wasi*helpers tocm_*/Cm*(genuinely WASI-scoped methods keep thewasi_prefix). Non-breaking surface addition. -
[x] Phase 1 —
wado wittext emission.wado wit(wado-cli/src/wit.rs,Cmd::Wit) takes a single file-or-directory target viaresolve_input, runswado_compiler::semantics()with no-O, and bails silently whenSemantics::is_complete()is false.wado-compiler/src/wit_emit.rs(emit_wit_text) does type mapping, kebabification, interface grouping, transitive-type closure, and bothfull/localscopes; no-entry-point files emit an empty world.tests/wit.rsasserts the rendered text per shape (empty world, functions-only direct exports, record default interface, CLIrun, full-scope resources/interfaces, string/list/option) and re-parses each withwit-parser.- Deviation from the original plan:
Semantics::interfaces()/exported_items()were not added. The emitter reads theCmInterfaceRegistry/WorldRegistrydirectly and takes the faithful world import set from the WIR import plan, soWitEmitOptionscarriesworld_imports: Vec<String>(alongsidescope,world_fq,default_interface_name) instead. Onlywit-parserandwit-encoderare pulled intowado-compiler;wit-componentlands with Phase 2.
- Deviation from the original plan:
-
[x] Phase 2 —
wado compileembedding (default on, scopefull)- [x]
wado-compiler/src/wit_bundle.rs: text →Resolve→wit_component::metadata::encode→ custom-section append (embed_component_type/encode_component_type).wit-parserandwit-componentmoved intowado-compiler's[dependencies]. - [x]
--embed-witand--no-embed-witflags onCompileOptions, mutually exclusive and value-less; embedding is on by default except under-Oswhich defaults off (an explicit--embed-witstill forces it). The embedded section is always the self-contained full closure, so there is no scope knob onwado compile(see the finding below).wado run/serve/testnever embed (they do not route throughcompile::run). - [x] Hook: postprocess in the
wado compileCLI path (compile::maybe_embed_wit), not incompile_with_options. This keeps the shared compile entry — used byrun/serve/test— embedding-free, matching "applies towado compileonly". The faithful world import set comes from the sameresolve_world_importsthewado witpath uses;Semanticsis re-derived with one extra frontend pass (wado_compiler::semantics), sincecompile_with_optionsconsumes its ownSemanticsintoPackage. - [x] Tests:
tests/wit_bundle.rsasserts, per world shape, that the un-embedded component already self-describes (decode→DecodedWasm::Component), that embedding is byte-additive and leaves the component decodable, and that the encoded payload decodes as a standaloneDecodedWasm::WitPackage.compile::embed_policy_testspins the default-on/-Os-off/--no-embed-wit/--embed-witresolution.
- [x]
Phase 2 finding: the component already self-describes
A Wado artifact is a full Component Model component, not a core module, so it
is already self-describing: wit_parser::decode (the wasm-tools component wit
backend) reconstructs the WIT from the component's own typed CM imports/exports
via its decode_component path — verified empirically on the CLI corpus. decode
only consults a component-type payload when the whole file is a WIT-package
blob (is_wit_package() keys off top-level component-type exports); a
component-type custom section on an already-formed component is opaquely
carried, never read by component wit.
The embedded section is therefore additive full-fidelity metadata, not the
mechanism that makes the binary inspectable. Its value over the intrinsic type:
the component's own type is always tree-shaken to the used surface, whereas the
embedded payload carries the complete upstream interface bodies, exact package
versions, and a producers record (the metadata::encode convention wkg /
wasm-tools metadata / relink flows consume). The earlier framing in
WIT Bundling ("a standalone .wasm cannot
describe its own interface") holds for core modules, not for Wado's component
output; the round-trip therefore splits in two: (a) decode(component) matches
wado wit semantically, and (b) the embedded payload decodes as a WIT package.
Consequence for scope: an embedded section must be self-contained, because
metadata::encode types the world against a fully-resolved Resolve. So
embedding always emits the full interface closure; a local
(registry-referencing) document does not re-parse standalone. This is a Wasm CM
binary property (component types are structural and self-contained), not a Wado
choice. local / full therefore stays a wado wit text concept (wado wit --scope) only. Consequently wado compile carries no scope knob:
--embed-wit / --no-embed-wit take no value, and the originally-planned
[wit].scope manifest override (Phase 3) is dropped — it would have nothing to
tune for the binary.
Phase 3 — Manifest scope override: dropped, not implemented. Embedding is
always the self-contained full closure (see the finding above), so there is no
embed-time scope to put in wado.toml. wado wit --scope remains the only
place local / full is meaningful (text output). WIT Bundling's status is
reconciled to "implemented, default-on, no scope knob".
Open Design Questions
World-less libraries (wado compile --lib)
A .wado file with only export items and no world entry point
(fn run / fn handle) is a library. It is declared by [package].lib in
wado.toml and built with wado compile --lib ([package].namespace is
required for --lib, and --lib is mutually exclusive with --world). The
library world is synthesized from the entry module's export signatures and
carried on Package.lib_world_info — it cannot live in the &'static stdlib
WorldRegistry, so it is special-cased like the test world. Library exports
use a synchronous lift (the core function returns the value directly), and the
default allocator is freelist.
World naming: the anonymous root
A component's type is anonymous: the Component Model binary encodes it as
componenttype ::= 0x41 vec(componentdecl) (Binary.md) — a bare list of
imports/exports with no world name — and WIT models a world as "an equivalent
of a component type" whose name is only a text-level label (WIT.md).
wasm-tools component wit confirms this: decoding a Wado library component
yields world root { … export <namespace>:<name>/<name> }. The world name is
discarded on decode and re-synthesized as root.
So a library's world carries no identity; only its interface does. The
identity consumers import is the default interface
<namespace>:<name>/<name>@<version>. The library world is the anonymous root
and is emitted as root (matching the decode convention), kept distinct from
the default interface so the two never share one namespace:package/<name>
slot — WIT worlds and interfaces occupy a single per-package namespace, so
naming both after the package is a duplicate item error that silently drops
the component-type embed. [package].name may therefore not be root. This
splits two previously-conflated values: the world's emit name (root, textual
only) and the default-interface FQ (ns:name/name, the codegen identity and
the component's real export). The anonymous component binary is unaffected.
wado wit --lib renders this same shape (world root exporting the default
interface), so the human-readable text and the embedded component-type agree.
Export grouping follows the same A/B rule the WIT producer applies
(wit_emit, mirroring WEP: WIT and Wado Mapping → "Default Interface"):
- A — no exported signature references a named user type: the
export fns are direct world exports (export id: func(...)), structural types (list/option/tuple/result/string) inlined. - B — any exported signature references a named user type
(
struct/variant/enum/flags/type alias): the exports plus the transitive closure of their named types are grouped into the default interface named after[package].name, exported as an instance. This is required for reuse — a world export list admits only functions and interfaces, never a bare type, so a named type reaches consumers as a reusable,use-able entity only through an exported interface.
The codegen mirrors this without world-specific special-casing:
- The keystone is registry-driven type resolution. The entry module's
exported (and transitively referenced) named types are registered into
CmInterfaceRegistryunder the default-interface FQ. Both the export type plan (resolve_cm_export_type) and the CM type emitter resolve named types purely through this registry, so lib-local types need no special path once registered. - The CM value-type emitter is the single recursive engine that already
builds the full value surface (
list/option/tuple/result/record/variant/enum/flags/newtype/nested) for WASI imports. It is generalized over a type sink so it emits either into an imported/exportedInstanceTypeor as top-level component defined types for direct world exports — one engine, no parallel export-side reimplementation. package-cm-catalogis the corpus enumerating the value-type ABI surface;tests/cm_catalog.rsround-trips a crafted value through everyid_*export (lift(lower(x)) == x) at-O0/-O2, and the same fixture runs under the test world viae2e.rs.
wado wit (without --lib) still wraps a no-entry-point file's interface in
the resolved default world (an otherwise empty world), which keeps its output
valid, round-trippable WIT.
World structure faithfulness
WIT worlds are containers: a world bundles a set of imports and exports, and
the same interface can appear in multiple worlds with different roles. The
current Wado treatment flattens this — interfaces are globally visible, and
the user imports them with use regardless of which world is targeted.
Levels of faithfulness to consider:
- L1 (current): worlds declare entry points; interfaces are globally visible.
- L2:
contract <World>;declarations verify that the module's interface usage is a subset of the world's imports (WEP: World Conformance, not yet implemented). - L3: each world owns a scope of usable interfaces. Importing an interface not declared by the active world is a compile error.
- L4: full WIT structure —
include,with, world inheritance.
External WIT consumption realistically requires at least L2 and probably L3:
without per-world scope, two unrelated worlds whose imports share a name
(e.g. two Logger interfaces from different packages) would still need to be
disambiguated only by the cross-package syntax above, which is workable but
fragile if the user forgets to qualify.
contract declaration
WEP: World Conformance and Export Syntax defines the syntax. The parser does
not implement it. Designing L2/L3 above means deciding the runtime behavior
of contract before parser work starts.
"All methods" import form (resolved)
wado-from-idl now emits bare import Foo; (matching WIT's
import stdout;), and the brace-block form has been removed from the
parser. Per-method tree-shaking is driven by used_wasi_functions
(populated from actual call sites), so nothing depends on the world
declaration for which methods are eligible.
Roadmap
This WEP is a roadmap document. Each item below either has its own WEP or will get one when work starts.
- [x] Type-driven CM binding synthesis (WEP: TIR-Level CM Binding Synthesis).
- [x] Unify
effectblock declarations intointerface(landed; see Migration Plan above). Theeffectkeyword survives in polymorphic effect parameters (<effect E>) — see Migration Plan for the rationale. - [x] World imports/exports are bare WIT-faithful interface refs
(
import Foo;/export Foo;); brace-form removed.WorldImportInfoandWorldExportInfocarrycm_interface_fqresolved from the referencedpub interface Foo's#[cm(...)]. - [x] Producer side: emit WIT text and embed
component-typein output (WEP: WIT Bundling for the format; this WEP §"Producer Side: WIT Generation and Embedding" for the detailed design). Phase 0 (Semanticsrefactor), Phase 1 (wado wittext), and Phase 2 (wado compileembeds WIT by default, full closure,--no-embed-witto opt out) are done. Phase 3 ([wit].scopeinwado.toml) was dropped — embedding is always self-contained, so there is no embed-time scope to tune. - [ ] Decide world structure faithfulness level (L2 vs L3) and document.
- [ ] Implement
contractdeclaration with the chosen scope rules (revise WEP: World Conformance accordingly). - [x] Decouple HTTP handler specialization from codegen: driven from
WorldExportInfo::from_interface_fq(P1/P2) rather than the return-type sniffer (returns_http_response) and the post-hocappend_http_handler_export. See §"Codegen Genericization". - [ ] Add
wit-componentas awado-compilerdependency (consumer side).wit-parseris already in[workspace.dependencies]forwado-from-idl; the consumer-side use-resolver reads embeddedcomponent-typefrom external.wasmimports via the same crate. - [ ] Construct world / interface / resource entries in the existing registries directly from parsed WIT, on the same code path as stdlib-derived entries.
- [ ]
wado compile --lib: export the entry module'sexport fns under a package-named library world, with A/B grouping (direct world exports vs default interface) matching the WIT producer. Plumbing and primitive sync exports landed (M1–M2); containers,result/stringreturns, lib-local named-type registration + emission, and default-interface grouping remain.package-cm-catalog+tests/cm_catalog.rstrack and drive the surface. - [ ] Close the binding-synthesis gaps for arbitrary exports: container and
user-named-type lift/lower,
result/stringreturns, and return-via-outptr when flat count exceedsMAX_FLAT_RESULTS. Sync export support and primitive lift/lower landed with--lib. - [x] Retire ad-hoc HTTP detection from the import/codegen path: the
has_http_handler_exportpackage field andappend_http_handler_exportare gone (P2/P5).WorldInfo::has_http_handler_exportsurvives only as the allocator-default heuristic (HTTP service → free-list). - [x] Emit
wasi:cli/run@<v>andwasi:http/handler@<v>as proper CM instance exports inemit_world_exports(P2:append_interface_instance_exports).
Codegen Genericization: Removing HTTP/world Special-Casing
Done (P1–P5). codegen/component.rs now encodes CM imports/exports from the
WIR import plan and registry-derived descriptors with no package == "http",
namespace_prefix == "wasi:http/", returns_http_response,
{pkg}-handler-result, or get_package_version("http") literals. HTTP and kiln
(emit_kiln_world_types) fall out of one generic mechanism. Key pieces:
- Handler detection is structural (
WorldExportInfo::is_handler_instance_export:from_interface_fq.is_some()+ non-unitResult).append_http_handler_exportis replaced byappend_interface_instance_exports, which wraps everyfrom_interface_fq = Some(fq)export in anfq-named instance (CLIrunis nowwasi:cli/run@<v>); runtime drivers bind it via the generatedCommandbindings. - A structural CM type interner in
ComponentModelContext(CmTypeKey → type-index, recursiveintern_cm_type) resolves theresult<own<response>, error-code>transport composite that no signature names; the export lift and theClientimport resolve to one index, dropping the{pkg}-handler-resultname. World exports resolve type names within their own package first (resolve_cm_source_with_prefix), fixing a kiln/HTTPResponsecollision. wasi:http/{types,client}are now ordinaryResourceDefiningInterface/ResourceUsingInterfaceentries classified structurally byresolve_import_plan; the HTTP constructors are declared inlib/wasi/http/types.wadowith#[cm(...)]and lowered generically. The deadhas_http_handler_exportfield is removed (WorldInfo::has_http_handler_exportsurvives only as the allocator-default heuristic);tests/wit_import_plan.rskeeps the plan faithful to the emitted bytes.
Scope was the stdlib-derived registry path only; external .wasm consumption is
a separate item.
Consequences
Positive
- A single, documented end-to-end goal for WIT support replaces a scatter of point WEPs.
- One keyword (
interface) for block declarations on both the import and export sides, matching WIT's vocabulary directly. Theeffectkeyword keeps a narrow, well-defined role (polymorphic effect parameters<effect E>). - World imports are traceable to WIT FQ names by construction, removing the
fragile method-name-based disambiguation in
CmInterfaceRegistry. - Adding a new WASI or third-party CM library no longer requires patching the compiler or the stdlib list.
- The producer (WIT Bundling) and consumer (this WEP) sides become symmetrical: a Wado-compiled component can be consumed by another Wado compiler with no extra metadata path.
Negative
- The block-form rename
effect Foo { ... }→interface Foo { ... }is a one-shot, no-deprecation change against the stdlib, fixtures, and any user code. It has already landed. - Bringing
wit-encoder/wit-componentinto the compiler increases the dependency surface and binary size of the compiler itself. - L3 world scoping changes the meaning of
use: auseof an interface not in the active world's imports becomes a compile error. This is a user-visible behavior change.
Trade-offs
- Reading WIT directly from
.wasmmakes external components first-class but diverges from the current "the.wadofile is the source of truth" model. The two paths will coexist: stdlib stays.wado-first viawado-from-idl; external imports become WIT-first. - Reusing
interfacefor both effectful and pure groupings means the presence of effect tracking is decided per-call (does this function call an effectful interface member?) rather than per-declaration. This is closer to WIT's lack of purity annotation and removes a Wado-specific concept users had to learn.
