Wado

WEP: Effect Reconstruction from CM Component Imports

Status: Implemented (v1, synchronous value-type surface)

Guest effects cross the Component Model boundary in both directions, under one rule. A consumer's effect obligations are reconstructed from a dependency's real host-leaf imports rather than from the mere presence of a function-bearing interface (the consuming direction). A Wado library, symmetrically, turns a guest effect it leaves unhandled into a CM import that a consumer satisfies (the producing direction) — either by holding the underlying capability, or by supplying a provider component composed in as a fused sibling.

Async import/export (stream<T> / future<T>) is out of scope for v1.

Context

Wado's model is interface = effect = CM import/export (Design Philosophy), and Wasm CM Component Import maps an imported component's interface onto an effectful Wado interface, like WASI.

That over-applies to pure components. package-marl (Markdown → HTML, string→string, no I/O) exports an interface carrying no host capability, yet calling Marl::render(src) as a compiled component required with Marl, and that requirement propagated through the whole call tree — ceremony with no capability behind it.

Two observations sharpen it:

Rejected: generalize #[benign]

Silencing an imported interface's effect with #[benign] asserts a purity the compiler cannot check — the unsafe pattern reborn, admitting unnoticed non-determinism. The point is to derive purity, not trust it.

Decision

One rule spans both directions:

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

This is composition-relative: the same WIT interface is an effect when host-satisfied and a namespace when sibling-satisfied. The distinction is computed at link/compose time, not intrinsic to the declaration.

Consuming — reconstruct a dependency's obligations

Calling into an imported component requires the union of that component's own host-leaf imports, mapped to the underlying Wado effects (wasi:clocks/monotonic-clockMonotonicClock), not a with Marl token. A truly pure component reconstructs to no effect — derived, not asserted.

Because CM imports are explicit in the artifact, the reconstructed set is a sound over-approximation by construction: it can over-include an unused capability but never miss one. Hidden non-determinism is surfaced, not silenced — a component that secretly reads the clock forces with MonotonicClock on its callers.

The substitution unit moves with the rule: a consumer mocks the underlying capabilities a dependency uses (MonotonicClock), not "Marl". A pure component has nothing to substitute.

Producing — an unhandled guest effect becomes a CM import

A library may declare its own interface effect and leave it unhandled at the boundary. Compiled as a component, that effect lowers to a CM import of a synthesized interface — the mirror of reconstruction. marl can thus perform a Highlight effect it does not implement, leaving the choice to consumers. An effect handled inside the library stays internal and imports nothing.

Satisfying — hold the capability, or compose a provider

A reconstructed guest effect (a non-WASI import) materializes in the consumer's scope as an impl-able effect and is required like any other; using the dependency without providing it is a missing-effect error. Two ways to provide it:

A provider is a static link-time choice, not a per-call dynamic handler. Attempting the latter — the consumer's in-process handler receiving calls from inside the dependency — forms a component instantiation cycle the Component Model forbids; see Research: Callbacks across the CM Boundary for the surveyed alternatives (donut wrapping, a host effect pump, first-class function values) and why static provider composition is the one that ships.

Scope

Resources ride the same rule with no special path: a host-provided resource (wasi:*, Stream / Future) bottoms out at the host and stays an effect; a guest-implemented resource imported through fusion reconstructs like any other interface. The unit is "host-leaf import vs fused-component export," not "interface vs resource."

Consequences

Discoverability

The requirement is statically known at import time; tooling surfaces it — hover prints the reconstructed signature (... with MonotonicClock), and effect-error diagnostics name the full set with a provenance chain (Diagnostic Reason Chains): "render requires MonotonicClock — marl.wasm imports wasi:clocks/monotonic-clock."

Costs (the price of honesty)

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

Consistency wins

Supersedes

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

References