Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/8596-transitive-leaf.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
perf(gc): reduce native-root safepoint density with a whole-module Perry-GC effect closure (#8596). Direct calls to generated functions are now marked `gc-leaf-function` when every transitive path stays within proven non-collecting runtime helpers and generated callees; pure recursive SCCs are supported. Allocation/poll paths, indirect calls, unknown externals, and cross-module calls remain statepoints. The proof and annotations are shared by whole-module text emission, split codegen units, and native LLVM construction (including `invoke` inside `try`). Shadow-frame bookkeeping helpers are also classified non-collecting; their only allocation is the raw Rust shadow-buffer `Vec`, which cannot trigger Perry GC. This is the sound polling-style reduction available with LLVM statepoints: a caller edge whose callee may collect must retain its relocation map so moving GC can rewrite the suspended caller frame.
14 changes: 14 additions & 0 deletions crates/perry-codegen/src/dialect/eh.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> {
let callee = &after[..paren];
let close = rmatch_paren(after, paren)?;
let args_str = &after[paren + 1..close];
let trailing_attr = after[close + 1..].trim();

// `build_indirect_invoke` takes basic values (not metadata enums
// like the call path), so collect both shapes once.
Expand Down Expand Up @@ -85,6 +86,19 @@ impl<'ctx, 'm> FnReader<'ctx, 'm> {
if preserve_none {
site.set_call_convention(super::LLVM_CC_PRESERVE_NONE);
}
match trailing_attr {
"" => {}
// #8596: whole-module GC-effect closure can prove a direct
// generated callee transitively non-collecting even inside a try.
// The textual path places the call-site attribute before
// `to label`; reproduce it in the C-API path or native units gain
// statepoints the text units do not have.
"\"gc-leaf-function\"" => site.add_attribute(
inkwell::attributes::AttributeLoc::Function,
self.ctx.create_string_attribute("gc-leaf-function", ""),
),
other => bail!("unknown invoke callsite attribute `{other}`"),
}
// An invoke terminates its block; the emitted text continues in
// the inline continuation label, which arrives as the next line.
match site.try_as_basic_value() {
Expand Down
36 changes: 36 additions & 0 deletions crates/perry-codegen/src/dialect/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -381,3 +381,39 @@ fn preserve_none_constructs_on_define_call_and_invoke() {
"invoke site lost its calling convention:\n{printed}"
);
}

/// #8596: a transitive-leaf direct call inside `try` is an invoke, and LLVM's
/// call-site attribute sits between the argument list and `to label`. The
/// split-module native reader must carry it onto the CallBase or RS4GC silently
/// restores a statepoint that the text path removed.
#[test]
fn gc_leaf_attribute_constructs_on_invoke() {
let ctx = Context::create();
let skeleton = "declare void @pure()\n\
declare i32 @perry_eh_personality(i32, i32, i64, ptr, ptr)\n";
let module = crate::inprocess::parse_ir_text(&ctx, skeleton, "leaf_invoke_skel")
.expect("skeleton parses");
let function = "define void @trycaller() personality ptr @perry_eh_personality {\n\
entry:\n\
\x20 invoke void @pure() \"gc-leaf-function\" to label %ok unwind label %pad\n\
ok:\n\
\x20 ret void\n\
pad:\n\
\x20 %lp = landingpad { ptr, i32 } catch ptr null\n\
\x20 ret void\n\
}\n";
predeclare_function_from_text(&ctx, &module, function).expect("predeclare");
add_function_from_text(&ctx, &module, function).unwrap_or_else(|e| panic!("{e:#}"));
module
.verify()
.unwrap_or_else(|e| panic!("verifier rejected native module:\n{}", e.to_string()));
let printed = module.print_to_string().to_string();
let invoke = printed
.lines()
.find(|line| line.contains("invoke void @pure"))
.unwrap_or_else(|| panic!("no invoke in constructed module:\n{printed}"));
assert!(
invoke.contains("#0") && printed.contains("attributes #0 = { \"gc-leaf-function\" }"),
"invoke lost its gc-leaf-function call-site attribute:\n{printed}"
);
}
20 changes: 20 additions & 0 deletions crates/perry-codegen/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -895,6 +895,14 @@ impl LlFunction {
}

pub fn to_ir(&self) -> String {
self.to_ir_with_gc_leaf_callees(&HashSet::new())
}

/// Render with the module's transitive Perry-GC leaf closure available at
/// direct call sites. Standalone function tests use [`Self::to_ir`] and an
/// empty set; module and codegen-unit renderers compute the whole-module
/// fixed point before serializing any function.
pub(crate) fn to_ir_with_gc_leaf_callees(&self, gc_leaf_callees: &HashSet<String>) -> String {
let mut ir = self.define_header(false);
ir.push('\n');
self.for_each_final_line::<std::convert::Infallible>(&mut |line| {
Expand All @@ -921,6 +929,18 @@ impl LlFunction {
ir
};

// #8596: LLVM needs a statepoint at a caller edge exactly when the
// transitive callee can reach collection. The whole-module analysis
// proves direct generated callees that cannot; stamp those edges after
// root lowering (which separately handles audited runtime helpers).
// Unknown, indirect, cross-module and collecting callees remain
// unmarked and therefore remain statepoints.
let ir = if self.stack_map_requested && !gc_leaf_callees.is_empty() {
crate::gc_call_effects::annotate_transitive_leaf_calls(&ir, gc_leaf_callees)
} else {
ir
};

// RS4GC uses the unwind destination's landing pad **as the token** for
// the relocates it inserts on the exceptional edge, so
// `statepoint-example` requires that pad to be `landingpad token`.
Expand Down
Loading
Loading