Wado

WEP: TIR-Level CM Binding Synthesis

Context

The compiler needs to cross the Component Model (CM) boundary at two points: imports (calling WASI functions) and exports (exposing Wado functions to the host). This boundary crossing requires lowering Wado GC values to CM flat ABI (linear memory scalars) and lifting CM values back to Wado types.

Previously, this logic was scattered across codegen (raw Wasm instruction emission), per-type converter functions in internal.wado, and pattern-matching convention structs. Every new WIT type shape required hand-coded support in multiple places.

CM binding synthesis replaces all of that with a single type-driven recursive synthesizer that generates binding functions as TIR. Adapters flow through the normal compiler pipeline (monomorphize → lower → optimize → codegen) like any other function.

Architecture

Pipeline Position

load → analyze → resolve (TIR) → effect_check
                                       ↓
                                   synthesis (traits + inspect + cm_binding)
                                       ↓
                    monomorphize → lower → optimize → wir_build → wir_optimize → codegen

The CM binding synthesis runs as part of the synthesis phase (alongside trait synthesis and inspect synthesis), after effect checking and before monomorphize. Adapter functions go through monomorphization, optimization, WIR translation, and codegen like user code.

Note: wasm_plan (component planning) is now integrated into the wir_build phase as wir_build::component_plan::build_component_plan(), not a standalone pipeline phase.

Import Adapters

For each WASI function used by the program, cm_binding_gen synthesizes an binding that:

  1. Accepts Wado-typed parameters (String, List, structs, etc.)
  2. Lowers parameters to CM flat ABI (linear memory via builtins)
  3. Calls the lowered WASI function via CmRawCall
  4. Lifts the result from CM flat ABI back to Wado types
  5. Returns the Wado-typed result

All call sites are rewritten from the original WASI call to the binding call.

Export Adapters

For each world export, cm_binding_gen synthesizes an binding that wraps the user's function. The binding is what the component actually exports — it handles CM ABI translation and calls the user function internally.

Type-Driven Synthesis

The core synthesizer (synthesize_lift, synthesize_lower_to_flat) is recursive and type-driven:

Type Lowering Lifting Provider
i32, i64, f32, f64 identity / builtin::*_store builtin::*_load synthesizer inline
bool value as i32 i32_load8_u(addr) != 0 synthesizer inline
char value as i32 char::from_u32_unchecked synthesizer inline
String alloc + copy → (ptr, len) copy from linear memory → GC string internal::cm_lower_string, internal::memory_to_gc_string
List<u8> alloc + copy → (ptr, len) copy from linear memory → GC array internal::cm_lower_array_u8, internal::memory_to_gc_array
list<T>, option<T>, result<T, E>, record, variant recursive recursive synthesizer generates TIR

No per-type converter functions exist. The synthesizer handles all composite types by recursing into their structure.

Canonical ABI Layout

cm_abi.rs computes sizes, alignments, field offsets, and flat type sequences for any Canonical ABI type. This replaces the ad-hoc size/align constants that were previously scattered across convention structs.

CmRawCall

Raw CM calls are a dedicated TIR node:

TirExprKind::CmRawCall {
    local_name: "wasi:http/types/[method]fields.get",
    args: [self_handle, name_ptr, name_len, outptr],
}

Codegen resolves local_name to the imported function index. This node is also used for task-return and other CM intrinsics in export adapters. For stream/future/waitable-set CM operations, see WEP: Redesign Wasm CM Builtins as Resource Canonical Attributes.

Current State

Import Adapters — Complete

All WASI interfaces (cli, clocks, random, http, sockets) use binding synthesis. Both instance method calls (MethodCall, e.g., fields.append(name, value)) and resource static method calls (StaticCall, e.g., Response::new(headers, null, trailers)) are rewritten to binding calls. The old codegen CM paths (generate_cm_effect_call, generate_cm_resource_method_call) and per-type converters (cm_list_string_to_array, cm_option_string_to_option, etc.) have been deleted.

Export Adapters — Async Only

Export adapters currently handle two cases:

Void exports (() -> ())

Used by Command world's run() and test functions. The binding calls the user function, then calls task-return(0).

Result-returning exports ((...) -> Result<T, E>)

Used by Service world's handle(). The binding:

  1. Calls the user function (passing through parameters)
  2. Tests the Result discriminant
  3. Ok: lowers T to flat CM values via synthesize_lower_to_flat, calls task-return
  4. Err: lowers E (variant, struct, or primitive) to flat CM values, calls task-return

The lowering is fully generic — synthesize_lower_to_flat recurses into any type structure (primitives, strings, options, structs, variants) without hard-coded type names.

What's Generic vs What's Specific

Component Generic? Notes
synthesize_lower_to_flat Yes Recursive, type-driven. Only hard-codes "String" for cm_lower_string
synthesize_variant_lower_to_flat Yes Iterates variant cases from declaration
flatten_* flat type computation Yes Type-structure-based
cm_abi.rs layout computation Yes Pure Canonical ABI spec
Result Ok/Err dispatch Intentional Result is a language primitive with fixed case ordering
task-return in all adapters Async assumption WASI P3 is async-only; needs change for sync

Remaining Work: Generic CM Exports

The current export binding synthesis works for the two hosted worlds (Command, Service). To support arbitrary world exports — including publishing Wado libraries as .wasm components — the following work is needed.

Parameter Lifting

Current adapters pass user function parameters through unchanged. This works because:

For generic exports with composite parameters, the binding must lift flat CM params to Wado types:

// User writes:
export fn greet(name: String) -> String { ... }

// Adapter must synthesize:
fn __cm_export__greet(name_ptr: i32, name_len: i32) {
    let name = internal::memory_to_gc_string(name_ptr, name_len);
    let result = greet(name);
    // lower result...
}

This requires:

Implemented via synthesize_lift_from_flat_params which lifts flat CM params (i32/i64/f32/f64) to Wado-typed values. Handles primitives, bool, char, String, resources, List, Option, and tuples. The export binding now detects when param lifting is needed (via export_needs_param_lifting) and generates flat-typed binding params with lifting code.

Non-Result Return Types

Current adapters handle () and Result<T, E>. For generic exports, any return type should work:

export fn add(a: i32, b: i32) -> i32 { ... }
export fn get_name() -> String { ... }
export fn get_pair() -> [String, i32] { ... }

This requires:

Implemented via synthesize_general_export_binding which handles non-Result return types. The binding calls the user function, lowers the return value to flat CM values, and calls task-return(0, ...flat_values) — wrapping in Ok since CM exports wrap returns in result<T, error-context>.

Sync Export Support

All current adapters assume WASI P3 async semantics (task-return). For publishing .wasm components consumed by non-async hosts, sync exports are needed:

// Async (current): binding calls task-return with flat values
cm_raw_call task-return(disc, val1, val2, ...);

// Sync: binding returns flat values directly
return (val1, val2, ...);

This requires:

Export Validation

Parameter count validation is implemented: if the user's export function has a different number of parameters than the world declaration, a clear compile error is produced.

Summary

Task Difficulty Status Notes
Parameter lifting Medium Done synthesize_lift_from_flat_params
Non-Result return types Low Done synthesize_general_export_binding
Sync export support Medium Pending World metadata for async/sync distinction
Export signature validation Low Partial Parameter count validated; types not yet

The type-driven synthesizer (synthesize_lift, synthesize_lower_to_flat, flat type computation) is already generic. The remaining work is sync export support and full type validation.

Known Limitations and Edge Cases

Parameter Lifting Gaps

synthesize_lift_from_flat_params handles primitives, bool, char, String, resources, List (with linear memory round-trip), Option, and tuples. The following types are not yet implemented:

These gaps are safe for current worlds (Command, Service) but would need to be addressed for custom worlds with complex parameter types.

Flat Return Type Mismatch in General Adapter

synthesize_general_export_binding computes flat return types from the user function's return type, not from the world's result<T, error-context> wrapper. This means:

In practice this is safe because:

To fix: compute flat return types from the world's full result<T, error-context> type, and zero-fill any extra slots.

List Lifting Uses Temporary Linear Memory

For List<T> where T is not u8, synthesize_lift_from_flat_params writes flat params (ptr, len) to a temporary 8-byte linear memory block, then calls synthesize_lift which reads from that block. This:

Call-Site Flattening for Multi-Flat Parameters

Import adapters use two strategies depending on the parameter type:

The call-site flattening approach was chosen for Option<T> because binding-internal lowering faces a fundamental type mismatch: Wado's null literal generates ref.null (a GC nullable reference) at the Wasm level, but the binding would need to accept it as a parameter and extract an i32 discriminant + payload. Converting between GC references and i32 scalars requires non-trivial unwrapping logic (pattern matching, unboxing) that the TIR synthesizer cannot easily generate for all Option<T> instantiations.

By flattening at the call site:

The binding body becomes a simple pass-through for these parameters. This avoids the GC-to-scalar type mismatch entirely.

Current limitation: only literal null and OptionSome expressions are supported at call sites. Arbitrary Option<T> variables would require runtime null-check logic at the rewrite site, which is not yet implemented.

No Type-Level Validation

Parameter count is validated, but parameter types and return type compatibility are not checked. For example, the compiler won't error if the user declares export fn run(x: String) but the world expects run(x: i32). The binding would generate incorrect lifting code (treating i32 as String).

Consequences

Benefits

Risks