Skip to content
Merged
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
3 changes: 3 additions & 0 deletions changelog.d/8062-native-rs4gc-root-lowering.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
### Fixed

- Make direct in-process LLVM construction apply Perry's finalized precise-root lowering before RS4GC in both single and split codegen units, restoring byte-identical text/native objects without weakening the differential oracle (#8052).
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -649,6 +649,18 @@ impl LlFunction {
self.blocks.len()
}

/// Whether final rendering must lower shadow-slot bindings into native
/// `addrspace(1)` roots before RS4GC runs.
///
/// The direct C-API backend normally consumes [`FinalItem`]s before
/// [`Self::to_ir`] applies this whole-function lowering. Its callers use
/// this bit to select the finalized text stream for mapped functions;
/// otherwise the native module keeps `js_shadow_slot_bind` calls while
/// claiming `gc "statepoint-example"`, and the collector gets no roots.
pub(crate) fn stack_map_requested(&self) -> bool {
self.stack_map_requested
}

/// Label of the last-created block — convenience for expression codegen
/// that needs to feed a phi node the predecessor label after compiling a
/// sub-expression whose control flow may have split.
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -681,6 +681,11 @@ impl LlModule {
let mut ir = String::new();
ir.push_str("; Generated by perry-codegen\n");
ir.push_str(&format!("target triple = \"{}\"\n\n", self.target_triple));
if crate::codegen::helpers::native_stack_roots_enabled()
&& self.target_triple.contains("apple")
{
ir.push_str("module asm \".no_dead_strip __LLVM_StackMaps\"\n\n");
}
for sc in &self.string_constants {
ir.push_str(sc);
ir.push('\n');
Expand All @@ -703,6 +708,9 @@ impl LlModule {
ir.push_str(decl);
ir.push('\n');
}
if crate::codegen::helpers::native_stack_roots_enabled() {
push_statepoint_declarations(&mut ir);
}
ir.push('\n');
self.push_attrs_and_metadata(&mut ir);
ir
Expand Down
126 changes: 109 additions & 17 deletions crates/perry-codegen/src/native_emit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,14 @@ fn stream_functions<'ctx>(
let header = synth_define_header(f, force_external);
let mut stream = crate::dialect::FnStream::begin(context, module, &header)
.map_err(|e| anyhow!("native IR construction failed in @{}: {:#}", f.name, e))?;
if f.personality.is_some() {
// The invoke-EH phi-predecessor rewrite (#7302) needs
// whole-function analysis and therefore text; the line reader
// DOES understand invoke/landingpad, so this path constructs
// natively from the rewritten text rather than falling back to
// clang.
if f.personality.is_some() || f.stack_map_requested() {
// Invoke-EH phi predecessors and precise-root lowering both need
// whole-function analysis. The latter turns shadow-slot binds
// into addrspace(1) roots before RS4GC; streaming the pre-lowered
// FinalItems would produce a verifier-clean module with no roots.
// This remains native construction: only one finalized function
// is materialized and fed through the closed dialect line reader,
// never parsed as module-scale IR.
let fn_text = f.to_ir();
for line in fn_text.lines().skip(1) {
stream.line(line).map_err(|e| {
Expand Down Expand Up @@ -213,16 +215,29 @@ fn freeze_unit(
skeleton.push_str(&crate::module::declare_line_for(&f));
skeleton.push('\n');
let mut items = Vec::new();
f.for_each_final_item::<anyhow::Error>(&mut |item| {
use crate::function::FinalItem as FI;
items.push(match item {
FI::Label(s) => FrozenItem::Label(s.to_string()),
FI::Blank => FrozenItem::Blank,
FI::Text(s) => FrozenItem::Text(s.to_string()),
FI::Inst(i) => FrozenItem::Inst(i.clone()),
});
Ok(())
})?;
if f.stack_map_requested() {
// `to_ir` is where precise roots are lowered. Freeze its body as
// owned lines so worker threads still receive an immutable payload
// and the module-scale text graph is never retained.
items.extend(
f.to_ir()
.lines()
.skip(1)
.filter(|line| *line != "}")
.map(|line| FrozenItem::Text(line.to_string())),
);
} else {
f.for_each_final_item::<anyhow::Error>(&mut |item| {
use crate::function::FinalItem as FI;
items.push(match item {
FI::Label(s) => FrozenItem::Label(s.to_string()),
FI::Blank => FrozenItem::Blank,
FI::Text(s) => FrozenItem::Text(s.to_string()),
FI::Inst(i) => FrozenItem::Inst(i.clone()),
});
Ok(())
})?;
}
functions.push(FrozenFunction {
name: f.name.clone(),
header: synth_define_header(&f, true),
Expand Down Expand Up @@ -561,7 +576,84 @@ fn debug_dump(module: &Module<'_>, module_prefix: &str) {
mod tests {
use super::*;
use crate::module::LlModule;
use crate::types::{I64, VOID};
use crate::types::{I32, I64, PTR, VOID};

fn precise_root_fixture(extra_plain_function: bool) -> LlModule {
let mut module = LlModule::new(crate::codegen::default_target_triple());
module.declare_function("js_shadow_slot_bind", VOID, &[I32, PTR]);
module.declare_function("js_map_alloc", I64, &[I32]);

let function = module.define_function("native_root_diff_fixture", I64, vec![]);
function.enable_shadow_frame(0);
let root_index = function
.reserve_shadow_slot()
.expect("native root fixture reserves one precise-root slot");
let root = function.alloca_entry(I64);
function.entry_allocas_push_store(I64, "0", &root);
function.entry_setup_call_void(
"js_shadow_slot_bind",
&[(I32, &root_index.to_string()), (PTR, &root)],
);
let entry = function.create_block("entry");
let value = entry.call(I64, "js_map_alloc", &[(I32, "0")]);
entry.store(I64, &value, &root);
entry.ret(I64, &value);
if extra_plain_function {
let plain = module.define_function("native_root_diff_plain", VOID, vec![]);
plain.create_block("entry").ret_void();
}
module
}

#[test]
fn native_construction_lowers_precise_roots_before_rs4gc() {
let _native = crate::codegen::helpers::NativeRootsPin::native();
let module = precise_root_fixture(false);

let text_ir = module.to_ir();
assert!(
text_ir.contains("alloca ptr addrspace(1)"),
"control arm must demonstrably lower a precise root:\n{text_ir}"
);
assert!(
!text_ir.contains("call void @js_shadow_slot_bind"),
"native-root lowering must consume the shadow-stack bind:\n{text_ir}"
);

let text = crate::linker::compile_ll_to_object(&text_ir, None)
.expect("trusted text arm emits an object");
let native = compile_module_native(&module, None, "native_root_diff_fixture")
.expect("direct native arm emits an object");
assert_eq!(
native, text,
"a mapped function must be byte-identical after both arms run RS4GC; \
a behavior-only check is vacuous until a collection"
);
}

#[test]
fn split_native_construction_lowers_precise_roots_before_rs4gc() {
let _native = crate::codegen::helpers::NativeRootsPin::native();
let text_module = precise_root_fixture(true);
let units = text_module.render_codegen_units(2);
assert_eq!(units.len(), 2, "fixture must exercise two real units");
let text = crate::linker::compile_units_to_object(&units, None)
.expect("trusted text units emit and partial-link");

let mut native_module = precise_root_fixture(true);
let native = compile_module_units_native(
&mut native_module,
2,
None,
"split_native_root_diff_fixture",
)
.expect("direct native units emit and partial-link");
assert_eq!(
native, text,
"split native units must freeze finalized precise-root IR, not \
pre-lowered shadow-slot calls"
);
}

#[test]
fn split_units_emit_and_merge_init_body_pointer_constant() {
Expand Down
Loading