Skip to content

Conversation

@lukewagner
Copy link
Member

@lukewagner lukewagner commented Nov 29, 2025

This PR moves async from being a hint that is textually mangled into function names to an optional effect type that is part of a function type. WIT is not changed; just how it's encoded as a component import/export definition. For example,

foo: async func(s: string) -> string;

goes from being encoded (when imported) as:

(import "[async]foo" (func (param "s" string) (result string)))

to

(import "foo" (func async (param "s" string) (result string)))

Along with (and motivating) this change is a runtime behavioral change: if core wasm "blocks" (viz.: calls waitable-set.{wait,poll}, thread.suspend, or calls something that might block synchronously) and the current task has not returned and the current task's function type does not contain async, there is a trap. Thus, async moves from being a "hint" that can be violated to an invariant that a client (host or component) can depend on.

The motivation for this change is working through the browser/JS embedding (both in a near-term jco transpile context but also considering a longer-term native browser/JS embedding). If the lack of async doesn't guarantee the lack of blocking, then a browser/JS embedding would never be able to call a non-async component export in a synchronous context (with important use cases being: an addEventListener callback, constructors, getters and setters). This is certainly the case for jco transpile implemented in terms of JSPI (where "blocking" manifests as the JS glue code calling into wasm that returns a Promise that cannot be awaited in a synchronous JS context). But even in a native browser implementation, blocking in a synchronous context basically requires spinning a nested event loop which browsers are generally trying to kill off and prevent any new occurrences of. This change would also be beneficial for other, non-browser embeddings which have the same underlying implementation constraints as browsers.

This change also reduces the cognitive overhead for bindings generators and power users of the hint. As a hint, since a non-async import might block, the bindings generator had to ask: "do I, and how do I, expose a power-user option for overriding the hint to handle the blocking case". (This is especially tricky when considering native binding support, as we'd eventually like for all embeddings of components into language runtimes, where there's not the option of "build flags"). Conversely, if I'm a very careful programmer who wants to achieve maximum concurrency: if async is just a hint and callees "might" always block: should I override the hint and when? Do I just determine when to override experimentally or conservatively always use async? Having a checked effect avoids developers wasting their time with these obscure-but-otherwise-necessary questions.

Although this change would seem to give functions a "color", for the reasons described in this PR in Concurrency.md#summary, async at the WIT/Component level does not have the same infective quality as source-language async. This change is also backwards compatible with existing Preview 2 components (which are all non-async and have no way to block) and existing Preview 3 WASI worlds (which already mark all the exports as async and can thus always block, as you'd expect).

Comment on lines +126 to +128
imports in the same manner as [JSPI]. Thus, overall, `async` in WIT and the
Component Model does not behave like a "color" in the sense described by the
popular [What Color Is Your Function?] essay.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In what sense does it not behave like coloring? Irrespective of how they're lowered/lifted:
Async imports may only be called from within async exports, right?

To me, that sounds exactly like the virality described in "What Color Is Your Function?".

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The way I think about this is that you're correct that functions have colors but it's not as extreme as the essay. You're correct that a sync export cannot call an async import in a blocking fashion, but the reason it's not as extreme as the essay is that it's expected to be conventional that WIT worlds have async exports. In such a situation it doesn't matter if your guest language is doing sync or async things -- it'll "all work out".

So colored in one sense, not colored in a different sense.

Copy link
Member Author

@lukewagner lukewagner Dec 1, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to capture the reasoning for how this isn't coloring here in the PR, but the summary is:

  • Just because code is called through an async export doesn't mean the code has the be rewritten from a synchronous style to asynchronous style (unlike with source-language async/await), because you can still use the sync ABI for all imported/exported functions.
  • Practically speaking, most code will be called through an async export so it can block all it wants. Mostly only components virtualizing worlds containing sync imports will need to implement sync functions and in this advanced virtualization scenario, you usually need higher-order ("donut") composition (where the parent component both implements the imports of the child and calls the exports of the child). Once we allow recursive reentrance (it's currently a TODO) a component A virtualizing the world of a component B could then have a callstack A--calls-->B--calls-->A, and then A could implement B's sync imports by suspending in the inner A call to the outer A call, just like JSPI allows in browsers today where "A" is "native JS code". Thus sync will actually be able to call async, but only if you do this advanced "donut wrapping" composition.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, if I got it right:
At the component interface boundaries there is coloring. But because we can still mix-and-match ABIs, that coloring doesn't (necessarily) make its way into user code. Assuming the entrypoint export is async, of course.


Are non-async exports allowed to start/spawn async import calls, as long as they don't block on the newly created task?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are non-async exports allowed to start/spawn async import calls, as long as they don't block on the newly created task?

Yep! And once they return their value (via task.return) they can even waitable-set.wait on them to finish.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, got it. Makes sense to me then!

Copy link
Collaborator

@alexcrichton alexcrichton left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for writing this up!

Copy link
Collaborator

@dicej dicej left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. Would you be able to add one or more WAST tests to verify the new trapping behavior (and thread.yield-as-no-op, if feasible), either here or in a follow up PR?

@lukewagner
Copy link
Member Author

@dicej Yep! Is there already a dev release with these traps implemented that I can use to write the tests?

@pannous
Copy link

pannous commented Dec 1, 2025

Some time ago you promised not to go down the road of function coloring, yet here we are. Async is the goto keyword of our generation. The concept of asynchronicity belongs on the caller side, not on the callee side. There are alternatives; investing the time and handling it properly now will prevent decades of headaches and pain.

@dicej
Copy link
Collaborator

dicej commented Dec 1, 2025

@dicej Yep! Is there already a dev release with these traps implemented that I can use to write the tests?

If you can build this PR branch of Wasmtime, you should be set, e.g. cargo install --git https://github.com/dicej/wasmtime --branch trap-blocking-in-sync-tasks --features component-model-async --locked wasmtime-cli. I don't think we'll have a dev release until that PR is merged, but let me know if you have trouble and we'll figure something out.

@pannous
Copy link

pannous commented Dec 1, 2025

exports impose little to no requirements on the guest language's style of concurrency

OK sorry for commenting before reading the full PR. No requirements would be fine but what does "little requirements" mean

and if the caller may or may not call the function in a concurrent fashion (as it should be !) then why add async keyword anyways?

@lukewagner
Copy link
Member Author

OK sorry for commenting before reading the full PR. No requirements would be fine but what does "little requirements" mean

If we're just talking about source code compiled to run in a component with async exports using the sync ABI, I don't think there are any requirements; async just buys you more optionality for what you can do at runtime. The "little or" in "little or no requirements" in Concurrency.md#summary refers to cases where you need to call a component export in a synchronous context, so you might opt for non-async exports.

and if the caller may or may not call the function in a concurrent fashion (as it should be !) then why add async keyword anyways?

It's really just about capturing in the calling contract (= function type), as understood by both the caller and the callee (unlike lifting/lowering ABI options, which are encapsulated impl details of their respective component) the (runtime checked) fact that the callee will not block.

@lukewagner
Copy link
Member Author

Ok, added a nice beefy test/async/trap-if-block-and-sync.wast.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants