cfallin requested fitzgen for a review on PR #11321.
cfallin opened PR #11321 from cfallin:cranelift-exception-dynamic-context to bytecodealliance:main:
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. Acontextclause sets the current context. AtagN: block(...)clause attempts to match the throwing exception againsttagN, evaluated in the current context, and branches to the named block if it matches. Adefault: 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,
tagNistagNand an inner handler for that tag shadows an outer handler (that is, tags always alias if identical indices); and whereas before,tagNis nottagMand 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.
<!--
Please make sure you include the following information:
If this work has been discussed elsewhere, please include a link to that
conversation. If it was discussed in an issue, just mention "issue #...".Explain why this change is needed. If the details are in an issue already,
this can be brief.Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.htmlPlease ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->
cfallin requested wasmtime-compiler-reviewers for a review on PR #11321.
cfallin requested wasmtime-core-reviewers for a review on PR #11321.
cfallin updated PR #11321.
cfallin edited PR #11321:
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. Acontextclause sets the current context. AtagN: block(...)clause attempts to match the throwing exception againsttagN, evaluated in the current context, and branches to the named block if it matches. Adefault: 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,
tagNistagNand an inner handler for that tag shadows an outer handler (that is, tags always alias if identical indices); and whereas before,tagNis nottagMand 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.
<!--
Please make sure you include the following information:
If this work has been discussed elsewhere, please include a link to that
conversation. If it was discussed in an issue, just mention "issue #...".Explain why this change is needed. If the details are in an issue already,
this can be brief.Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.htmlPlease ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->
cfallin updated PR #11321.
cfallin updated PR #11321.
cfallin updated PR #11321.
cfallin updated PR #11321.
bjorn3 submitted PR review.
bjorn3 created PR review comment:
Is this using the same numbering scheme as DWARF debuginfo? If so can this reuse the enum we use for variable location debuginfo? If not what numbering scheme is used?
cfallin submitted PR review.
cfallin created PR review comment:
Right now it's a little ad-hoc -- it propagates through whatever numbering scheme we use in
PReg, which on x86 is the ISA encoding's numbering (here) and on all others mirrors the GPR numbering directly (NisxNon aarch64,xNon riscv64,rNon s390x,xNon pulley). I was worried about putting an unwind dependency in a core type but perhaps I can pull the enum the other way -- good idea.
cfallin submitted PR review.
cfallin created PR review comment:
(err, here, rather)
cfallin submitted PR review.
cfallin created PR review comment:
Hmm, actually, this creates a bunch of new complexity: it requires pulling in the register mapper from the unwind implementation which ordinarily only exists on SysV platforms, conditionalizing this enum arm existence on the
unwindfeature, and some other mess as well. I only added this for genericity based on yesterday's Cranelift meeting (support context in regs/stack in general, assert stack only on the Wasmtime side), but I would rather not build out a bunch of functionality for register-based contexts if it's not actually used; and if it is eventually in Wasmtime, we will likely define our own register mappings. So I am going to go ahead and declare what we have now as Cranelift's "native" dynamic exception context register mapping, and we can refine this later as needed -- or if folks would prefer (@alexcrichton?) I can remove theGPRarm here and put the assert and stack-only constraint back into Cranelift.
alexcrichton submitted PR review.
alexcrichton created PR review comment:
Would it be possible to use a literal
PReghere? That way embedders could remap if they like, but there's a canonical definition for each register (the various backend helpers). Otherwise I'd agree it's not worth too much infrastructure just yet and it's ok to assert with a todo that a register doesn't pop out
cfallin submitted PR review.
cfallin created PR review comment:
Perhaps; but we don't export regalloc2 types from the public API anywhere else, nor those backend helpers? The latter especially mean that the
PRegisn't much more meaningful than the rawhw_enc-- that's the integer we are returning currently.In fullness of time this seems like something an assembler layer should own -- canonical register names and ISA-defined numbering, then we can use this mapping in our public APIs -- but I guess no matter which type we wrap around that index today, the issue is that we don't have any public API surface that defines it.
cfallin submitted PR review.
cfallin created PR review comment:
(Except for the unwind stuff, of course, but that has the above platform-dependence and feature-dependence issues)
alexcrichton submitted PR review.
alexcrichton created PR review comment:
Ok makes sense, personally I'm ambivalent about leaving this as
u8or just removing the variant for now, could go either way.
cfallin created PR review comment:
Cool; maybe the deciding factor then is, as long as this PR has the change to rely on the ABI clobbers to force us to the stack case for the tail-call ABI (a good suggestion!), let's keep the register case, so we don't have to fall back to a compile panic or disallow contexts for try-calls in SysV ABI (which are already supported).
cfallin submitted PR review.
fitzgen submitted PR review:
Looks great! A couple nitpicks below, r=me with them addressed
fitzgen created PR review comment:
This length invariant no longer holds because context entries do not have an associated target, right?
fitzgen created PR review comment:
This method would previously give only exceptional edges, but now gives all kinds of table items. That seems fine if callers were not relying on that property, but we should definitely update the method documentation and probably the method name as well.
fitzgen created PR review comment:
Double checking my understanding: we don't need to check basic properties of the context value here (like its definition dominates this use) because these checks are handled via the generic methods of getting all uses and defs of a particular instruction elsewhere in the verifier, right?
fitzgen created PR review comment:
#[expect(so that it turns into a warning (and CI failure) if we forget to remove this once it is actually used
fitzgen created PR review comment:
Maybe makes sense to define
add_call_sitesuch that it doesn't take any handlers, and then a newadd_try_call_sitethat does take them? Would cut down on the empty iterator argument stuff to clean up all these call sites a little.Actually, even better would be if we pushed the try call info into the caller, so that call sites just looked like
sink.add_call_site(info.try_call_info())and then
add_call_sitewould internally do theif let Some(...).
cfallin updated PR #11321.
cfallin submitted PR review.
cfallin created PR review comment:
Went with the former option -- interpreting the
TryCallInfoto get handlers requires aFrameLayoutas well, and pushing that into theMachBufferfelt like too much coupling (vs. taking an iterator overMachExceptionHandlers, which are a buffer-defined concept).
cfallin submitted PR review.
cfallin created PR review comment:
Yep, exactly -- the context values are included in the instruction's values iterator (
DataFlowGraph::inst_values()) now, so are verified to exist and be valid SSA uses that way.
cfallin submitted PR review.
cfallin created PR review comment:
Done!
cfallin submitted PR review.
cfallin created PR review comment:
Updated, thanks!
cfallin updated PR #11321.
cfallin submitted PR review.
cfallin created PR review comment:
Done!
cfallin has enabled auto merge for PR #11321.
cfallin updated PR #11321.
cfallin merged PR #11321.
Last updated: Dec 06 2025 at 07:03 UTC