Working through the test cases in wasip3-prototyping I think I may have found a bug/weird corner -- for a test component like async_backpressure_caller.component.wasm, the [async]run import does not seem to have a result, but is detected as an async function by the latest version of tooling.
In WIT the signature for the interface is:
package local:local;
// ...
interface run {
run: async func();
}
// ...
````
The world:
```wit
world backpressure-caller {
import backpressure;
import run;
export run;
}
Of course async is just a hint in the run interface, but what I'm finding is that the backpressure-caller's import is actually getting picked up as an async import properly (CanonicalOptions reports the import [async]run as async), but the type of the actual imported function is invalid -- it's missing the usual return i32.
backpressure_callee's run() is async, the WAT confirms an async lift:
....
;11;) (type $#type2)))
(import "wasi_snapshot_preview1" "proc_exit" (func $__imported_wasi_snapshot_preview1_proc_exit (;12;) (type $#type1)))
(table $#table0 (;0;) 79 79 funcref)
(memory $#memory0 (;0;) 17)
(global $__stack_pointer (;0;) (mut i32) i32.const 1048576)
(global $GOT.data.internal.__memory_base (;1;) i32 i32.const 0)
(export "memory" (memory $#memory0))
(export "_start" (func $_start))
(export "__main_void" (func $__main_void))
(export "local:local/backpressure#set-backpressure" (func $local:local/backpressure#set-backpressure))
(export "[async-lift]local:local/run#[async]run" (func $"[async-lift]local:local/run#[async]run"))
(export "[callback][async-lift]local:local/run#[async]run" (func $"[callback][async-lift]local:local/run#[async]run"))
(export "cabi_realloc" (func $cabi_realloc))
...
backpressure_caller code treats the import from backpressure_callee as async, the WAT confirms an async lower:
...
(type (;15;) (func (param i32 i32 i32 i32 i32 i32 i32) (result i32)))
(import "$root" "[subtask-cancel]" (func $_ZN14wit_bindgen_rt13async_support7subtask6cancel17h6ca32b751664aab8E (;0;) (type 5)))
(import "local:local/run" "[async-lower][async]run" (func $_ZN168_$LT$async_backpressure_caller..bindings..local..local..run..run..$u7b$$u7b$closure$u7d$$u7d$.._MySubtask$u20$as$u20$wit_bindgen_rt..async_support..subtask..Subtask$GT$11call_import4call17h0c04e01391ac0d1fE (;1;) (type 9)))
(import "local:local/backpressure" "set-backpressure" (func $_ZN25async_backpressure_caller8bindings5local5local12backpressure16set_backpressure11wit_import017hc771b6ca3da04845E (;2;) (type 1)))
(import "[export]local:local/run" "[task-return][async]run" (func $_ZN25async_backpressure_caller8bindings7exports5local5local3run22_export_async_run_cabi28_$u7b$$u7b$closure$u7d$$u7d$11wit_import017hd3657f94392c9a00E (;3;) (type 3)))
(import "$root" "[waitable-join]" (func $_ZN14wit_bindgen_rt13async_support12waitable_set4join17h7a11b9e1b4a3bcbdE (;4;) (type 0)))
...
BUT, in that very same WAT (the caller), at the top the import stub shows a function with no return values (which should be impossible for something coming in async):
(type $#type1 (;1;)
(instance
(type $#type0 (;0;) (func))
(export $#func0 (;0;) "[async]run" (func (type $#type0)))
)
)
(import "local:local/run" (instance $#instance1 (;1;) (type $#type1)))
;; ....
;; stub to make the import work
(core instance $#instance2 (;2;)
(export "[async-lower][async]run" (func $#func7))
)
...
;; instantiation of caller
(core instance $#instance7 (;7;) (instantiate $#module0
(with "$root" (instance $#instance1))
(with "local:local/run" (instance $#instance2))
(with "local:local/backpressure" (instance $#instance3))
(with "[export]local:local/run" (instance $#instance4))
(with "[export]$root" (instance $#instance5))
(with "wasi_snapshot_preview1" (instance $#instance6))
)
)
...
When I chase down the #instance2 -> #func7 types for the function I end up here:
(type $#type5 (;5;) (func (param i32 i32)))
This is also not the right signature it should have the usual i32 result param.
I don't have a minimal repro just yet (I will make one that just takes the component(s) in question and loads them into a Resolve and looks up the functions in question, since that's the code I'm working on is doing)...
Switching gears a little bit, maybe I can restate this whole thing more concisely:
(type $#type25 (;25;) (func))
(alias core export $#instance7 "[async-lift]local:local/run#[async]run" (core func $#func57 (;57;)))
(alias core export $#instance7 "[callback][async-lift]local:local/run#[async]run" (core func $#func58 (;58;)))
(func $#func18 (;18;) (type $#type25) (canon lift (core func $#func57) async (callback $#func58)))
The above chunk of WAT is from the caller -- the async run export
async_backpressure_caller.component.wasm
cannot have type 25 right? The run: async func() cannot actually have a type (func), it must be at least (func (result i32))
I've attached the wasm below just for ease, but you can also get it (or the callee wasm) from p3-prototyping
async_backpressure_caller.component.wasm
Keep in mind that the ABIs for async lifts and lowers are different. You're right that the core signature of an async lifted function should always have an i32 result, but the core signature of an async lowered function will not have a result if the component signature doesn't. Does that help explain what you're seeing, or do you actually see an async-lifted function with a core signature that's missing a result type?
Ah interesting -- so maybe this is what I'm seeing, but the spec says:
Comparing signatures, the differences are:
* Async-lowered functions always have a singlei32"status" code.
Am I misreading it here? What is the call path supposed to be for the async lowered fn without a status code? Is the caller supposed to treat it just like a sync function?
Oops, sorry, I was misremembering. Yeah, async lowers do always have a i32 return code for the status. I'll take a closer look at what you posted above tomorrow. Sorry for the misinformation (and I wish I could edit older Zulip posts).
Absolutely no problem, thanks for giving it some thought! I was kind of hoping I was just misreading/missing something obvious :sweat_smile: , because now I'm wondering if this goes all the way to bindgen.
and I wish I could edit older Zulip posts
The number of times this has bit me...
Okay, so looking at the .wasm file you posted:
(;@1b5a ;) (func $"[async-lift]local:local/run#[async]run" (;32;) (type 9) (result i32)
Looks correct; the core signature returns an i32 result, as expected.
(;@1c37 ;) (func $"[callback][async-lift]local:local/run#[async]run" (;33;) (type 7) (param i32 i32 i32) (result i32))
Also looks correct; all callbacks should have that exact core signature.
(;@1b5d1 ;) (type (;25;) (func))
(;@1b5d8 ;) (alias core export 7 "[async-lift]local:local/run#[async]run" (core func (;57;)))
(;@1b603 ;) (alias core export 7 "[callback][async-lift]local:local/run#[async]run" (core func (;58;)))
(;@1b63b ;) (func (;18;) (type 25) (canon lift (core func 57) async (callback 58)))
Also looks correct; this time, type 25 is the lifted (i.e. component-level) signature, which takes no parameters and returns no result, just like the WIT file said. So I think it's all as expected?
OK, maybe the code in Jco right now is looking at the wrong signature, and we need to actually dig for the core func that was pointed to by the lifted signature, I'll try to make that update and/or extract out the logic into a repro that's easier to read.
Will be out on monday but should have something by tuesday!
Last updated: Dec 06 2025 at 06:05 UTC