Wado

Research: Component Model Pain Points

Where Wado's use of the Component Model (CM) meets a limit of the CM itself. Wado targets the CM on purpose, so a limit it hits is feedback for the spec, not a reason to leave it. Each entry names what Wado runs into, what the spec says about it, and where a report belongs.

The spec is the target. An engine that deviates from it is reported to that engine, as a separate matter. Wasmtime accepts reports only from people, not from tools (vendor/wasmtime/AGENTS.md).

Checked against:

A host object has no handle a GC guest can let go of

wado-lang:web binds the DOM. A page hands the program the same objects again and again, and a program keeps them in lists, struct fields and closures. The bindings therefore do not use CM resources for them. Each object crosses as an f64 index into a table the glue owns, and nothing ever releases an entry: every object the program has seen stays alive for the life of the page (Resource Inheritance, "Lifecycle").

What the spec offers instead:

So a GC guest holding a host object must drop every handle by hand, including the ones inside its own heap, or leak. The GC ABI option as scoped changes how a handle is represented, not the drop obligation it carries, so it does not lift this by itself.

What to report to the CM: a GC-managed guest needs a handle that is released when the guest's collector finds it unreachable. That is the JS side's FinalizationRegistry path, made available to the guest.

A handle has no identity

A program asks whether two handles name the same object: == on two elements, removeEventListener finding the listener addEventListener registered, a map keyed by element.

The CM gives no way to ask. Lowering an own or a borrow creates a new table index each time (CanonicalABI.md, lower_own, lower_borrow), so one object crossing twice arrives as two unequal indices. resource.rep would reveal the representation, but only to the instance that implements the resource (CanonicalABI.md, "canon resource.rep"). An importer needs a host function per resource type to compare two handles.

The web bindings avoid this only because their handles are not CM resources: the glue interns each object, so one object always crosses as one number.

What to report to the CM: an importer cannot tell whether two handles name the same resource.

Resource types have no subtyping

WebIDL interfaces form single-inheritance chains (EventTarget → Node → Element → HTMLElement → HTMLInputElement), and one object is an instance of every type up its chain.

The CM has no relation between resource types:

An upcast is therefore a host call minting a second handle, and a downcast a host call returning option<own<Sub>>, each with its own drop obligation. Resource Inheritance builds the hierarchy in Wado instead and lowers the whole family to one universal handle. That works only because the handle is not a CM resource.

What to report to the CM: a type import bounded by another resource type (sub $Parent), so a handle lifts where its supertype is expected.

A closure cannot cross

An event listener, requestAnimationFrame and queueMicrotask all take a function. The CM has no function values. Concurrency.md lists them as future work: "allow function closures to be passed as first-class values, supporting the 'callback' pattern in many pre-existing APIs, including Web APIs".

Web § Callbacks encodes a closure as a u32 key and exports one trampoline per argument shape for the host to call back through. What that costs:

What to report to the CM: nothing new. The need is already on the list; the web bindings add a concrete consumer and the cost of the workaround.

Checked and not a CM gap

Recursive reentrance

dispatch_event runs its listeners during the import call, so the listener reenters the component (Web § Callbacks).

The spec no longer forbids this. WebAssembly/component-model#650 (2026-05-21) and WebAssembly/component-model#705 (2026-08-28) removed the may_enter flag and its trap. Concurrency.md, "Reentrance", now describes a host reentering the caller's instance through a recursive export call as possible, and leaves its hazards to the component's documented API.

The failure the web bindings record under jco is therefore jco's, not the spec's.

Exceptions from the host

A DOM operation that throws traps in the web bindings (Web § Exceptions).

The CM has a channel for it. The JS API turns an exception thrown by a function whose WIT result is a result into that result's error case (Explainer.md, "JS API"). What is missing is knowing which operations throw: WebIDL does not declare it, so the generator cannot choose result for them. That is a question for WebIDL, not the CM.

Engine deviations

Found while checking the entries above. Each is the engine's to fix, not the spec's.

jco

A program that dispatches an event to its own listener, compiled by Wado and transpiled by jco, run on Node against jsdom:

export fn run() with Dom {
    let body = Dom::document().body().unwrap();
    body.add_event_listener("ping", |_: Event| {
        Dom::document().body().unwrap().set_id("pinged");
    }, null);
    body.dispatch_event(Event::new("ping"));
}

scripts/jco/package-lock.json keeps 1.30.0 until the 1.35.0 defect is fixed.

wasmtime

The same reentry, with the synchronous import implemented by func_wrap_async and the export called back with TypedFunc::call_async, panics inside wasmtime instead of returning an error. The call fails before the listener runs, and the future's drop guard then unwraps a second error: `subtask.cancel` called after terminal status delivered (src/runtime/component/concurrent/func.rs, SignalOnDrop::drop). The first error is lost. A library panic reachable from the public API is a defect whatever the call's validity; a person has to file it.