Skip to content

Commit 4590076

Browse files
authored
Cranelift: support dynamic contexts in exception-handler lists. (#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. 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.
1 parent 815c10d commit 4590076

File tree

37 files changed

+1771
-322
lines changed

37 files changed

+1771
-322
lines changed

cranelift/codegen/src/inline.rs

Lines changed: 29 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@
2020
//! Cranelift the body of the callee that is to be inlined.
2121
2222
use crate::cursor::{Cursor as _, FuncCursor};
23-
use crate::ir::{self, InstBuilder as _};
23+
use crate::ir::{self, ExceptionTableData, ExceptionTableItem, InstBuilder as _};
2424
use crate::result::CodegenResult;
2525
use crate::trace;
2626
use crate::traversals::Dfs;
@@ -202,15 +202,6 @@ struct InliningAllocs {
202202
/// do not require set-membership testing, so a hash set is not a good
203203
/// choice either.
204204
calls_needing_exception_table_fixup: Vec<ir::Inst>,
205-
206-
/// The set of existing tags already caught by an inlined `try_call`
207-
/// instruction's exception table, for filtering out duplicate tags when
208-
/// merging exception tables.
209-
///
210-
/// Note: this is a `HashSet`, and not an `EntitySet`, because tags indices
211-
/// are completely arbitrary and there is no guarantee that they start from
212-
/// zero or are contiguous.
213-
existing_exception_tags: crate::HashSet<Option<ir::ExceptionTag>>,
214205
}
215206

216207
impl InliningAllocs {
@@ -219,7 +210,6 @@ impl InliningAllocs {
219210
values,
220211
constants,
221212
calls_needing_exception_table_fixup,
222-
existing_exception_tags,
223213
} = self;
224214

225215
values.clear();
@@ -232,11 +222,6 @@ impl InliningAllocs {
232222
// `calls_needing_exception_table_fixup` because it is a sparse set and
233223
// we don't know how large it needs to be ahead of time.
234224
calls_needing_exception_table_fixup.clear();
235-
236-
// Note: We do not reserve capacity for `existing_exception_tags`
237-
// because it is a sparse set and we don't know how large it needs to be
238-
// ahead of time.
239-
existing_exception_tags.clear();
240225
}
241226

242227
fn set_inlined_value(
@@ -616,31 +601,24 @@ fn fixup_inlined_call_exception_tables(
616601
exception,
617602
..
618603
} => {
619-
// Gather the set of tags that this instruction's exception
620-
// table already has entries for.
621-
allocs.existing_exception_tags.clear();
622-
allocs.existing_exception_tags.extend(
604+
// Construct a new exception table that consists of
605+
// the inlined instruction's exception table match
606+
// sequence, with the inlining site's exception table
607+
// appended. This will ensure that the first-match
608+
// semantics emulates the original behavior of
609+
// matching in the inner frame first.
610+
let sig = func.dfg.exception_tables[exception].signature();
611+
let normal_return = *func.dfg.exception_tables[exception].normal_return();
612+
let exception_data = ExceptionTableData::new(
613+
sig,
614+
normal_return,
623615
func.dfg.exception_tables[exception]
624-
.catches()
625-
.map(|(c, _)| c),
626-
);
627-
628-
// Add only the catch edges from our original `try_call`'s
629-
// exception table that are not already handled by this
630-
// instruction.
631-
for i in 0..func.dfg.exception_tables[call_exception_table].len_catches() {
632-
let exception_tables = &mut func.stencil.dfg.exception_tables;
633-
let value_lists = &mut func.stencil.dfg.value_lists;
634-
635-
let (tag, block_call) =
636-
exception_tables[call_exception_table].get_catch(i).unwrap();
637-
if allocs.existing_exception_tags.contains(&tag) {
638-
continue;
639-
}
616+
.items()
617+
.chain(func.dfg.exception_tables[call_exception_table].items()),
618+
)
619+
.deep_clone(&mut func.dfg.value_lists);
640620

641-
let block_call = block_call.deep_clone(value_lists);
642-
exception_tables[exception].push_catch(tag, block_call);
643-
}
621+
func.dfg.exception_tables[exception] = exception_data;
644622
}
645623

646624
otherwise => unreachable!("unknown non-return call instruction: {otherwise:?}"),
@@ -829,8 +807,18 @@ impl<'a> ir::instructions::InstructionMapper for InliningInstRemapper<'a> {
829807
let inlined_sig_ref = self.map_sig_ref(exception_table.signature());
830808
let inlined_normal_return = self.map_block_call(*exception_table.normal_return());
831809
let inlined_table = exception_table
832-
.catches()
833-
.map(|(tag, callee_block_call)| (tag, self.map_block_call(*callee_block_call)))
810+
.items()
811+
.map(|item| match item {
812+
ExceptionTableItem::Tag(tag, block_call) => {
813+
ExceptionTableItem::Tag(tag, self.map_block_call(block_call))
814+
}
815+
ExceptionTableItem::Default(block_call) => {
816+
ExceptionTableItem::Default(self.map_block_call(block_call))
817+
}
818+
ExceptionTableItem::Context(value) => {
819+
ExceptionTableItem::Context(self.map_value(value))
820+
}
821+
})
834822
.collect::<SmallVec<[_; 8]>>();
835823
self.func
836824
.dfg

cranelift/codegen/src/ir/dfg.rs

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -888,16 +888,25 @@ impl DataFlowGraph {
888888
&'dfg self,
889889
inst: Inst,
890890
) -> impl DoubleEndedIterator<Item = Value> + 'dfg {
891-
self.inst_args(inst).iter().copied().chain(
892-
self.insts[inst]
893-
.branch_destination(&self.jump_tables, &self.exception_tables)
894-
.into_iter()
895-
.flat_map(|branch| {
896-
branch
897-
.args(&self.value_lists)
898-
.filter_map(|arg| arg.as_value())
899-
}),
900-
)
891+
self.inst_args(inst)
892+
.iter()
893+
.copied()
894+
.chain(
895+
self.insts[inst]
896+
.branch_destination(&self.jump_tables, &self.exception_tables)
897+
.into_iter()
898+
.flat_map(|branch| {
899+
branch
900+
.args(&self.value_lists)
901+
.filter_map(|arg| arg.as_value())
902+
}),
903+
)
904+
.chain(
905+
self.insts[inst]
906+
.exception_table()
907+
.into_iter()
908+
.flat_map(|et| self.exception_tables[et].contexts()),
909+
)
901910
}
902911

903912
/// Map a function over the values of the instruction.

0 commit comments

Comments
 (0)