Skip to content

Async export with more than 16 flat result slots ICEs: task.return never takes the indirect form #1712

Description

@gfx

Summary

canon task.return receives its result as parameters, so MAX_FLAT_PARAMS (16) applies. A result flattening to more than 16 core values is passed indirectly — one i32 pointer to a buffer the guest lowered the value into:

def canon_task_return(result_type, opts: LiftOptions, flat_args):

— flattened with MAX_FLAT_PARAMS, vendor/component-model/design/mvp/CanonicalABI.md

Wado emits the flat parameters regardless of count, so the core import it declares disagrees with the canonical signature the component builds, and the WIR pipeline fails validation.

Reproduction

struct Wide {
    f0: u32,
    // … 20 `u32` fields in total
    f19: u32,
}

export async fn make_wide() -> Wide {
    task return Wide { f0: 0, /* … */ f19: 19 };
}

wado build --lib -O0:

thread 'main' panicked at wado-compiler/src/codegen.rs:163:9:
Internal compiler error: WIR pipeline generated invalid Wasm
Entry module: lib.wado
Validation error: type mismatch for export `task-return:make_wide` of module instantiation argument `wasi`
expected: (func (param i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32 i32))
found:    (func (param i32))

expected is what the guest declared; found is the canonical signature — a single out-pointer, i.e. the indirect form.

Where the cap is missing

compute_export_flat_return_types (synthesis/cm_binding/types.rs) flattens the whole result with no bound, and every consumer takes the list at face value: record_task_return_flat_params (synthesis/cm_binding.rs) types the shared NIR import from it, and both emission sites — push_result_task_return_epilogue (synthesis/cm_binding/export_adapter.rs) and the task return expansion (synthesis/cm_binding/task_return.rs) — pass one argument per slot. Nothing anywhere compares the count against 16.

Notes for the fix

Both halves of the indirect form already exist and are used by the synchronous lift:

  • lowering the value into a buffer — synthesize_lower_wasi_type_to_memory (synthesis/cm_binding/lower.rs), the same call push_sync_return_epilogue makes for a multi-value sync result;
  • releasing that buffer afterwards — synthesize_free_cm_value (synthesis/cm_binding/cm_free.rs), the memory walk post-return uses. task.return lifts eagerly, so the buffer is freed right after the call, exactly as the flat slots are today (Async-lifted exports leak their task.return payload: nothing frees the lowered buffer #1708).

So the work is the gate itself: pick the form on the flat-slot count, and thread the single-i32 signature through the import typing.

Found while fixing #1708 — that issue anticipated the indirect variant as a case the reclamation walk would have to handle, on the assumption the form was lowered. It is not, so there is no buffer to reclaim yet.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcompiler

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions