Skip to content

Conversation

@cfallin
Copy link
Member

@cfallin cfallin commented Jul 24, 2025

In #11285, we realized that Wasm semantics require us to match on dynamic instances of exception tags, rather than static tag types. This fundamentally requires the unwinder to be able to resolve the current Wasm instance for each Wasm frame on the stack that has any handlers, and our frame format does not provide this today.

We discussed many options, some of which solve the more general problem (Wasm vmctx for any frame), but ultimately landed on a notion of "dynamic context for evaluating tags", specific to Cranelift's exception-catch metadata; and storing that context and carrying it through to a place that is named in the unwind metadata. The reasoning is fairly straightforward: we cannot afford a more general approach that stores vmctx in every frame (I measured this at 20% overhead for a recursive-Fibonacci benchmark that is call-intensive); and inlining means that we may have multiple contexts at any given program point, each associated with a different slice of the handler tags; so we need a mechanism that, just for a try-call, intersperses contexts with tags (or puts a context on each tag) and stores these somewhere that the exception-unwind ABI doesn't clobber (e.g., on the stack).

This PR implements "option 4" from that issue, namely, dynamic exception contexts. The idea is that this is the dual to exception payload: while payload lets the unwinder communicate state to the catching code, context lets the unwinder take state from the catching code that lets it decide whether the tag is a match. Because of inlining, we need to either associate (optional) context with every tag, or intersperse context-updates with handler tags. I've opted for the latter for efficiency at the CLIF level (in most cases there will be multiple tags per context), though they are isomorphic.

The new tag-matching semantics are: when walking up the stack, upon reaching a try_call, evaluate catch-clauses in listed order. A context clause sets the current context. A tagN: block(...) clause attempts to match the throwing exception against tagN, evaluated in the current context, and branches to the named block if it matches. A default: block(...) always branches to the named block.

Note that this lets us assume less about tags than before, and this particularly manifests in the changes to the inliner. Whereas before, tagN is tagN and an inner handler for that tag shadows an outer handler (that is, tags always alias if identical indices); and whereas before, tagN is not tagM and so we can order the tags arbitrarily (that is, tags never alias if non-identical indices); now any two static tag indices may or may not alias depending on the dynamic context of each. Or, even in the same context, two may alias, because we leave the match-predicate as an unspecified (user-chosen) algorithm during unwinding. (This mirrors the reality that, for example, a Wasm instance may import two tags, and dynamically these tags may be equal or different at runtime, even instantiation-to-instantiation.) Cranelift's only job is to faithfully carry the list of contexts and tags through to the compiled-code metadata; and to ensure that they remain in the order they were specified in the CLIF.

This PR introduces the Cranelift-level feature, and it will be used in a subsequent PR that introduces Wasm exception handling. Because of that, I've opted not to update the clif-utils runtest "runtime" to read out contexts and do something with them -- we will have plenty of test coverage via a bunch of Wasm tests for corner cases such as the above. This PR does include filetests that show that contexts are carried through to spillslots and those appear in the metadata.

Fixes #11285.

@cfallin cfallin requested a review from fitzgen July 24, 2025 21:02
@cfallin cfallin requested review from a team as code owners July 24, 2025 21:02
@cfallin cfallin force-pushed the cranelift-exception-dynamic-context branch 5 times, most recently from 5a36866 to 4cf651d Compare July 24, 2025 21:33
@github-actions github-actions bot added cranelift Issues related to the Cranelift code generator cranelift:area:machinst Issues related to instruction selection and the new MachInst backend. cranelift:area:aarch64 Issues related to AArch64 backend. cranelift:area:x64 Issues related to x64 codegen labels Jul 25, 2025
Copy link
Member

@fitzgen fitzgen left a comment

Choose a reason for hiding this comment

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

Looks great! A couple nitpicks below, r=me with them addressed

cfallin added a commit to cfallin/wasmtime that referenced this pull request Jul 25, 2025
This PR introduces support for the [Wasm exception-handling proposal],
which introduces a conventional try/catch mechanism to WebAssembly. The
PR supports modules that use `try_table` to register handlers for a
lexical scope; and provides `throw` and `throw_ref` that allocate (in
the first case) and throw exception objects.

This PR builds on top of the work in bytecodealliance#10510 for Cranelift-level
exception support, bytecodealliance#10919 for an unwinder, and bytecodealliance#11230 for exception
objects built on top of GC, in addition a bunch of smaller fix and
enabling PRs around those. It is currently stacked on top of bytecodealliance#11321.

This PR does not yet provide host-boundary-crossing exceptions;
exceptions that are not caught in a given Wasm activation become traps
at the host boundary. That support will come in a subsequent PR.

Because exceptions do not yet cross the host boundary, this also does
not yet enable the `assert_exception` wast directive, and so cannot yet
support the spec-tests. That will also come in a subsequent PR.

[Wasm exception-handling proposal]: https://github.com/WebAssembly/exception-handling/
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jul 26, 2025
This PR introduces support for the [Wasm exception-handling proposal],
which introduces a conventional try/catch mechanism to WebAssembly. The
PR supports modules that use `try_table` to register handlers for a
lexical scope; and provides `throw` and `throw_ref` that allocate (in
the first case) and throw exception objects.

This PR builds on top of the work in bytecodealliance#10510 for Cranelift-level
exception support, bytecodealliance#10919 for an unwinder, and bytecodealliance#11230 for exception
objects built on top of GC, in addition a bunch of smaller fix and
enabling PRs around those. It is currently stacked on top of bytecodealliance#11321.

This PR does not yet provide host-boundary-crossing exceptions;
exceptions that are not caught in a given Wasm activation become traps
at the host boundary. That support will come in a subsequent PR.

Because exceptions do not yet cross the host boundary, this also does
not yet enable the `assert_exception` wast directive, and so cannot yet
support the spec-tests. That will also come in a subsequent PR.

[Wasm exception-handling proposal]: https://github.com/WebAssembly/exception-handling/
@cfallin cfallin force-pushed the cranelift-exception-dynamic-context branch 2 times, most recently from bf1adb2 to 2877420 Compare July 26, 2025 01:03
@cfallin cfallin enabled auto-merge July 26, 2025 01:05
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jul 26, 2025
This PR introduces support for the [Wasm exception-handling proposal],
which introduces a conventional try/catch mechanism to WebAssembly. The
PR supports modules that use `try_table` to register handlers for a
lexical scope; and provides `throw` and `throw_ref` that allocate (in
the first case) and throw exception objects.

This PR builds on top of the work in bytecodealliance#10510 for Cranelift-level
exception support, bytecodealliance#10919 for an unwinder, and bytecodealliance#11230 for exception
objects built on top of GC, in addition a bunch of smaller fix and
enabling PRs around those. It is currently stacked on top of bytecodealliance#11321.

This PR does not yet provide host-boundary-crossing exceptions;
exceptions that are not caught in a given Wasm activation become traps
at the host boundary. That support will come in a subsequent PR.

Because exceptions do not yet cross the host boundary, this also does
not yet enable the `assert_exception` wast directive, and so cannot yet
support the spec-tests. That will also come in a subsequent PR.

[Wasm exception-handling proposal]: https://github.com/WebAssembly/exception-handling/
In bytecodealliance#11285, we realized that Wasm semantics require us to match on
dynamic instances of exception tags, rather than static tag types. This
fundamentally requires the unwinder to be able to resolve the current
Wasm instance for each Wasm frame on the stack that has any handlers,
and our frame format does not provide this today.

We discussed many options, some of which solve the more general problem
(Wasm vmctx for any frame), but ultimately landed on a notion of
"dynamic context for evaluating tags", specific to Cranelift's
exception-catch metadata; and storing that context and carrying it
through to a place that is named in the unwind metadata. The reasoning
is fairly straightforward: we cannot afford a more general approach that
stores vmctx in every frame (I measured this at 20% overhead for a
recursive-Fibonacci benchmark that is call-intensive); and inlining
means that we may have *multiple* contexts at any given program point,
each associated with a different slice of the handler tags; so we need a
mechanism that, *just for a try-call*, intersperses contexts with tags
(or puts a context on each tag) and stores these somewhere that the
exception-unwind ABI doesn't clobber (e.g., on the stack).

This PR implements "option 4" from that issue, namely, *dynamic
exception contexts*. The idea is that this is the dual to exception
payload: while payload lets the unwinder communicate state *to* the
catching code, context lets the unwinder take state *from* the catching
code that lets it decide whether the tag is a match. Because of
inlining, we need to either associate (optional) context with every tag,
or intersperse context-updates with handler tags. I've opted for the
latter for efficiency at the CLIF level (in most cases there will be
multiple tags per context), though they are isomorphic.

The new tag-matching semantics are: when walking up the stack, upon
reaching a `try_call`, evaluate catch-clauses in listed order. A
`context` clause sets the current context. A `tagN: block(...)` clause
attempts to match the throwing exception against `tagN`, *evaluated in
the current context*, and branches to the named block if it matches. A
`default: block(...)` always branches to the named block.

Note that this lets us assume less about tags than before, and this
particularly manifests in the changes to the inliner. Whereas before,
`tagN` is `tagN` and an inner handler for that tag shadows an outer
handler (that is, tags always alias if identical indices); and whereas
before, `tagN` is not `tagM` and so we can order the tags arbitrarily
(that is, tags never alias if non-identical indices); now any two static
tag indices may or may not alias depending on the dynamic context of
each. Or, even in the same context, two may alias, because we leave the
match-predicate as an unspecified (user-chosen) algorithm during
unwinding. (This mirrors the reality that, for example, a Wasm instance
may import two tags, and dynamically these tags may be equal or
different at runtime, even instantiation-to-instantiation.) Cranelift's
only job is to faithfully carry the list of contexts and tags through to
the compiled-code metadata; and to ensure that they remain in the order
they were specified in the CLIF.

This PR introduces the Cranelift-level feature, and it will be used in
a subsequent PR that introduces Wasm exception handling. Because of
that, I've opted not to update the clif-utils runtest "runtime" to read
out contexts and do something with them -- we will have plenty of test
coverage via a bunch of Wasm tests for corner cases such as the above.
This PR does include filetests that show that contexts are carried
through to spillslots and those appear in the metadata.

Fixes bytecodealliance#11285.
@cfallin cfallin force-pushed the cranelift-exception-dynamic-context branch from 2877420 to 03067b9 Compare July 26, 2025 01:16
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jul 26, 2025
This PR introduces support for the [Wasm exception-handling proposal],
which introduces a conventional try/catch mechanism to WebAssembly. The
PR supports modules that use `try_table` to register handlers for a
lexical scope; and provides `throw` and `throw_ref` that allocate (in
the first case) and throw exception objects.

This PR builds on top of the work in bytecodealliance#10510 for Cranelift-level
exception support, bytecodealliance#10919 for an unwinder, and bytecodealliance#11230 for exception
objects built on top of GC, in addition a bunch of smaller fix and
enabling PRs around those. It is currently stacked on top of bytecodealliance#11321.

This PR does not yet provide host-boundary-crossing exceptions;
exceptions that are not caught in a given Wasm activation become traps
at the host boundary. That support will come in a subsequent PR.

Because exceptions do not yet cross the host boundary, this also does
not yet enable the `assert_exception` wast directive, and so cannot yet
support the spec-tests. That will also come in a subsequent PR.

[Wasm exception-handling proposal]: https://github.com/WebAssembly/exception-handling/
@cfallin cfallin added this pull request to the merge queue Jul 26, 2025
Merged via the queue into bytecodealliance:main with commit 4590076 Jul 26, 2025
72 checks passed
@cfallin cfallin deleted the cranelift-exception-dynamic-context branch July 26, 2025 02:00
cfallin added a commit to cfallin/wasmtime that referenced this pull request Jul 26, 2025
This PR introduces support for the [Wasm exception-handling proposal],
which introduces a conventional try/catch mechanism to WebAssembly. The
PR supports modules that use `try_table` to register handlers for a
lexical scope; and provides `throw` and `throw_ref` that allocate (in
the first case) and throw exception objects.

This PR builds on top of the work in bytecodealliance#10510 for Cranelift-level
exception support, bytecodealliance#10919 for an unwinder, and bytecodealliance#11230 for exception
objects built on top of GC, in addition a bunch of smaller fix and
enabling PRs around those. It is currently stacked on top of bytecodealliance#11321.

This PR does not yet provide host-boundary-crossing exceptions;
exceptions that are not caught in a given Wasm activation become traps
at the host boundary. That support will come in a subsequent PR.

Because exceptions do not yet cross the host boundary, this also does
not yet enable the `assert_exception` wast directive, and so cannot yet
support the spec-tests. That will also come in a subsequent PR.

[Wasm exception-handling proposal]: https://github.com/WebAssembly/exception-handling/
bongjunj pushed a commit to prosyslab/wasmtime that referenced this pull request Oct 20, 2025
…codealliance#11321)

In bytecodealliance#11285, we realized that Wasm semantics require us to match on
dynamic instances of exception tags, rather than static tag types. This
fundamentally requires the unwinder to be able to resolve the current
Wasm instance for each Wasm frame on the stack that has any handlers,
and our frame format does not provide this today.

We discussed many options, some of which solve the more general problem
(Wasm vmctx for any frame), but ultimately landed on a notion of
"dynamic context for evaluating tags", specific to Cranelift's
exception-catch metadata; and storing that context and carrying it
through to a place that is named in the unwind metadata. The reasoning
is fairly straightforward: we cannot afford a more general approach that
stores vmctx in every frame (I measured this at 20% overhead for a
recursive-Fibonacci benchmark that is call-intensive); and inlining
means that we may have *multiple* contexts at any given program point,
each associated with a different slice of the handler tags; so we need a
mechanism that, *just for a try-call*, intersperses contexts with tags
(or puts a context on each tag) and stores these somewhere that the
exception-unwind ABI doesn't clobber (e.g., on the stack).

This PR implements "option 4" from that issue, namely, *dynamic
exception contexts*. The idea is that this is the dual to exception
payload: while payload lets the unwinder communicate state *to* the
catching code, context lets the unwinder take state *from* the catching
code that lets it decide whether the tag is a match. Because of
inlining, we need to either associate (optional) context with every tag,
or intersperse context-updates with handler tags. I've opted for the
latter for efficiency at the CLIF level (in most cases there will be
multiple tags per context), though they are isomorphic.

The new tag-matching semantics are: when walking up the stack, upon
reaching a `try_call`, evaluate catch-clauses in listed order. A
`context` clause sets the current context. A `tagN: block(...)` clause
attempts to match the throwing exception against `tagN`, *evaluated in
the current context*, and branches to the named block if it matches. A
`default: block(...)` always branches to the named block.

Note that this lets us assume less about tags than before, and this
particularly manifests in the changes to the inliner. Whereas before,
`tagN` is `tagN` and an inner handler for that tag shadows an outer
handler (that is, tags always alias if identical indices); and whereas
before, `tagN` is not `tagM` and so we can order the tags arbitrarily
(that is, tags never alias if non-identical indices); now any two static
tag indices may or may not alias depending on the dynamic context of
each. Or, even in the same context, two may alias, because we leave the
match-predicate as an unspecified (user-chosen) algorithm during
unwinding. (This mirrors the reality that, for example, a Wasm instance
may import two tags, and dynamically these tags may be equal or
different at runtime, even instantiation-to-instantiation.) Cranelift's
only job is to faithfully carry the list of contexts and tags through to
the compiled-code metadata; and to ensure that they remain in the order
they were specified in the CLIF.

This PR introduces the Cranelift-level feature, and it will be used in
a subsequent PR that introduces Wasm exception handling. Because of
that, I've opted not to update the clif-utils runtest "runtime" to read
out contexts and do something with them -- we will have plenty of test
coverage via a bunch of Wasm tests for corner cases such as the above.
This PR does include filetests that show that contexts are carried
through to spillslots and those appear in the metadata.

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

Labels

cranelift:area:aarch64 Issues related to AArch64 backend. cranelift:area:machinst Issues related to instruction selection and the new MachInst backend. cranelift:area:x64 Issues related to x64 codegen cranelift Issues related to the Cranelift code generator

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Wasmtime exception support needs instance vmctx in each Wasm frame

4 participants