Wado

WEP: Wasm CM Component Import (use-based)

Context

Core-wasm asset import (use _ from "./x.wat" with { type: "wat" }) is done (WEP: WebAssembly Module Import Support, Phase 1). This WEP is the Component Model analogue: importing functions from an external .wasm component via use { Catalog } from "./catalog.wasm" with { type: "wasm" }, lowering/lifting values at the CM boundary, and composing the dependency component into the output so the result runs standalone.

This is the consumer side of WIT Interoperability roadmap items "read embedded WIT from external .wasm" and "construct world/interface/resource entries in the existing registries directly from parsed WIT".

Decisions

Design

Pipeline position

The loader builds an ast::Module for the component programmatically (no source text), inserts it into loaded, and the normal frontend (bind/analyze/annotate) produces symbols, types, and CmInterfaceRegistry entries — identical to how WASI bindings flow, except the module is synthesized from decoded WIT rather than parsed from lib/wasi/**.

Phases

  1. Fixture. wado compile --lib package-cm-catalog -o wado-compiler/tests/fixtures/sub/cm-catalog.wasm — the value-type ABI corpus, used as the dependency component for the e2e round-trip.

  2. Shared type-mapping core (refactor first). Make CmShape reusable and add a wit_parser::Type → ast::Type mapper that reuses the shared structural assembly and primitive correspondence.

  3. Loader: detect + decode + build AST. In handle_wasm_import, detect a component by its binary header (is_wasm_component) and branch. Decode via wit_component::decode; for each exported interface build a ast::Item::Interface (with #[cm(fq)] + per-method #[cm(fq#name)] + #[cm_params(...)]) and its named types as top-level items with #[cm(...)]. NamedType.source_interface is set to the interface FQ directly. Insert the module into loaded; store the component bytes as a dependency asset keyed by the canonical wasm:<path> namespace.

  4. Registry provenance + import plan. Record FQ → dependency-component namespace so resolve_import_plan classifies the interface as ImportKind::Component instead of FunctionInterface. Registration uses the same register_module_decls path the stdlib uses, hooked per-compilation via Arc::make_mut(&mut tysys.cm_interface_registry) (alongside the existing --lib register_lib_local_decls call).

  5. Binding synthesis. Reused unchanged: the type-driven lower(args)/lift(result) adapters are the same as for WASI imports.

  6. Codegen composition. The program imports each ImportKind::Component interface like a host interface, then compose_dependency_components composes the dependency in with wasm-compose (see "Codegen" below).

  7. E2E. tests/fixtures round-trips Catalog::id_* against the fixture, asserting lift(lower(x)) == x across the value-type surface at O0/O2.

Status

Working end-to-end: use { Iface } from "./c.wasm" with { type: "wasm" }Iface::method(x) resolves, composes, and round-trips at runtime. E2E fixture tests/fixtures/cm_component_import_catalog.wado round-trips the full synchronous value-type surface (primitives, string, enum, flags, newtype, List, Option, Result, records, variants, tuples, and nested combinations) against the composed cm-catalog.wasm at O0/O2, asserting whole-value == (auto-derived Eq) on every aggregate.

The async value types — stream<T> and future<T> — are not yet handled; that is Phase 8 below.

Codegen: fused composition via wasm-compose

The program component imports the dependency interface like a host function-interface (generate_cm_imports treats ImportKind::Component exactly like FunctionInterface). compose_dependency_components then statically composes it: using wasm-compose's in-memory CompositionGraph, each dependency component is instantiated and its exported interface connected to the program's matching import; both components' remaining imports (host WASI) are surfaced and merged by name, and the program's exports are re-exported.

This replaced an earlier host-mediated approach (canon lower the dependency's export into the program's imports) that validated but trapped CannotEnterComponent at runtime: with concurrency support on (always, under WASI P3), the canonical ABI forbids the host re-entering a top-level instance already on the stack, and wasmtime elides that check only for fused guest-to-guest adapters — exactly what wasm-compose produces. wasm-compose also handles the import union/forwarding automatically (the dependency's own wasi:cli/types / stderr panic-path imports become the composed component's imports).

Value-type surface (params + lower/lift)

The instance-type emitter and the binding synthesis cover the full value-type surface for component-import params and returns. The work was almost entirely removing drift between parallel code paths — for an imported component, a type's source_interface namespace is arbitrary (wado-lang:cm-catalog/...), which broke every WASI-only shortcut:

Working end-to-end (O0/O2), verified by cm_component_import_catalog: primitives, string, char, enum, flags, newtype, List, Option, Result<ok, err> (arbitrary err, including mismatched core classes), records, variants (incl. payload-bearing and tuple payloads), tuples, and nested combinations (list<record>, option<record>, tuple<record, _>, option<list>, result<list<record>, _>, list<tuple<record, _>>, …).

Notes