Wado

WEP: Effect Reconstruction from CM Component Imports

Status: Implemented (v1)

The synchronous value-type surface is implemented: the loader collects each imported component's host-leaf imports, the import plan mirrors the composed binary, and the effect checker requires a direct E::op() call's reconstructed effects. Enforcement is scoped to host-backed effects — an effect with a #[cm] boundary (WASI or a CM component). A user-defined effect with no CM boundary is resolved by the handler machinery, so its operations (including a handler method's self-delegation) are not a direct-op requirement. The async import surface (stream/future) remains out of scope (see below).

Context

Wado's core model is interface = effect = CM import/export (see Design Philosophy "Effects are WASI capabilities"). WIT Interoperability unified block declarations into interface and decided that a function-bearing interface is conservatively treated as effectful by the call site. Wasm CM Component Import maps an imported .wasm component's exported interface onto an effectful Wado interface, exactly like WASI.

This is simple and consistent, but it over-applies to pure components. A pure package — package-marl (Markdown → HTML/Markdown, string→string, no I/O) or package-gale — exports an interface that carries no host capability. Imported as a .wasm component, calling Marl::render(src) nevertheless requires with Marl, and that requirement propagates through the entire call tree. The effect is real ceremony with no capability behind it.

Two observations sharpen the problem:

Rejected: generalize #[benign]

#[benign(E)] keeps the world import but elides with E propagation. Applying it to a whole imported interface would silence the effect. Rejected: it is an assertion of purity the compiler cannot check, the unsafe pattern reborn, and it risks admitting unnoticed non-determinism. The whole point is to derive purity, not to trust it.

Decision

Reconstruct a consumer's effect requirements from the imported component's actual host-leaf imports, rather than treating its exported interface as an opaque effect.

The reconstructed rule

An interface is an effect if, in the final composed component, it bottoms out as a host import. If it is satisfied by a fused sibling component, it is a transparent namespace whose effectfulness forwards to that component's own imports, recursively.

This is the honest inverse of #[benign]: CM imports are explicit, so the reconstructed set is a sound over-approximation by construction — it can over-include an unused capability but can never miss one. Hidden non-determinism is surfaced, not silenced: a component that secretly reads the clock forces with MonotonicClock on its callers.

Availability

The information is already in hand. wit_component::decode reconstructs the component's world, including its imports; wasm-compose already surfaces a dependency's remaining imports into the composed component; and WIT Interoperability's resolve_import_plan already asserts "type-level import set == compiled binary's CM imports". Decode runs in the loader (Phase 3 of Wasm CM Component Import), so a component's declared imports are available before effect-check runs in the frontend. The union over-approximation is a sound early estimate; DCE-based pruning refines the consumer's own usage later without affecting soundness.

Standard imports map through the existing machinery: a wasi:* FQ resolves to the existing Wado effect via CmInterfaceRegistry; a non-WASI interface is synthesized from decoded WIT, reusing the export-side synthesis already in Wasm CM Component Import.

The ambient panic path needs no subtraction here, because it no longer imports: log_stderr / log_stdout rides an existing stdio import or lowers to unreachable, gated on whether the target world provides a sink (NirPackage::provides_ambient_stdio_sink — true for wasi:cli/command, wasi:http/service, and the test world; false for --lib and kiln). So a purely-computational component's decoded imports are already empty of the panic path; what remains is exactly its real capabilities (plus type-only shared interfaces such as wasi:cli/types, which carry no effect).

Granularity

Resources are in scope — the same mechanism as interfaces

Resource operations are effects (Effect System Design "Resource Types as Effects"): every constructor / method / static is a host call. Reconstruction subsumes this rule rather than exempting resources from it. A resource lives in an interface, so the host-leaf criterion applies to the owning interface with no resource-specific path:

So the reconstruction unit is not "interface vs resource" but "host-leaf import vs fused-component export". Resources ride the same rule; the effect a caller sees is whatever host capabilities the resource's implementation transitively needs, empty for a purely-computational one.

Out of scope

Async import surface (stream<T> / future<T> in an imported component's signatures) is Phase 8 of Wasm CM Component Import and not wired yet. The synchronous value-type surface — where pure packages live — is the v1 target.

Consequences

The conceptual shift

interface = effect splits: an interface is either a host-leaf capability (an effect) or a fused-component namespace (not an effect). This is not intrinsic to the interface declaration — it is composition-relative. The same WIT interface is an effect when host-satisfied and a namespace when satisfied by a fused component. The distinction is not visible from the interface declaration alone; it is computed by the Wado compiler at link/compose time from the import plan, not decided by the runtime host.

The substitution unit moves with it: instead of mocking "Marl", a consumer mocks the underlying capabilities Marl uses (e.g. MonotonicClock). For a pure component there is nothing to substitute — correctly.

Discoverability

The requirement is statically known at import time, so this is not blind trial-and-error, provided tooling surfaces it:

Costs (the price of honesty)

Both costs are the flip side of Wado's "no hidden effects" principle: surfacing MonotonicClock is correct; with Marl was cheap precisely because it was dishonest.

Consistency wins

Supersedes

Each supersession is scoped — host-satisfied (WASI) interfaces and host-provided resources are unchanged.

References