Skip to content

Commit 48580fd

Browse files
author
Ralph Küpper
committed
fix(codegen): record a stable source_filename on emitted modules
`cargo-test` has been red on main since #8062/#8068/#8071 landed, on four perry-codegen tests that assert the textual and native construction paths emit byte-identical objects. The code they generate already converged; the objects differed only in the name LLVM records for the module. Nothing set `source_filename`, so LLVM fell back to whatever path reached the assembler. The textual pipeline writes each module to a per-call temp file, so its recorded name carried a random nonce (`perry_llvm_<nonce>.ll`); native construction recorded its in-memory module id (`perry_native_module`) instead. ELF stores that name as an `STT_FILE` symbol, so the two paths could never agree and neither was reproducible run to run. Mach-O records no such symbol, which is why every one of these tests passes on a macOS host and fails only on the Linux runner. Emit an explicit `source_filename` from all three module-header sites (`to_ir`, `skeleton_ir`, and the per-codegen-unit prologue) so the recorded name is the same constant on both paths and independent of the temp path. The `inprocess` fold-order test named its two arms apart itself, which put the same difference in a `.file` directive; both arms now emit under one name, leaving the generated code as the only thing the assertion compares.
1 parent 86967ca commit 48580fd

3 files changed

Lines changed: 112 additions & 3 deletions

File tree

crates/perry-codegen/src/inprocess.rs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -519,8 +519,14 @@ mod tests {
519519
optimize_and_emit_module(&module, &target, &["-O3".into(), "-S".into()], true)
520520
.expect("fixture emits assembly")
521521
};
522-
let text = emit(&text_ir, "constant_fold_text");
523-
let folded = emit(&folded_ir, "constant_fold_native");
522+
// Both arms must be emitted under the SAME module name. The name
523+
// becomes the module id, and on ELF the assembler writes it into the
524+
// object as a `.file` directive — so two differently-named arms differ
525+
// by that one line no matter how perfectly the code itself converged.
526+
// Mach-O records no such directive, which is why naming them apart only
527+
// ever failed on Linux (#8087).
528+
let text = emit(&text_ir, "constant_fold_order");
529+
let folded = emit(&folded_ir, "constant_fold_order");
524530
assert_eq!(
525531
text, folded,
526532
"construction-time constant folding must converge before RS4GC assigns root liveness"

crates/perry-codegen/src/module.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,18 @@ pub struct LlModule {
349349
fp_flags: FpFlags,
350350
}
351351

352+
/// The `source_filename` every Perry-emitted module records.
353+
///
354+
/// Without it, LLVM records whatever path the caller handed the assembler. The
355+
/// textual pipeline writes each module to a per-call temp file
356+
/// (`perry_llvm_<nonce>.ll`), so the recorded name carried a random nonce,
357+
/// while native construction recorded its in-memory module id instead. ELF
358+
/// stores that name as an `STT_FILE` symbol, so the two construction paths
359+
/// could never produce byte-identical objects and neither was reproducible
360+
/// across runs. Mach-O records no such symbol, which is why this was invisible
361+
/// on macOS hosts and only ever failed on Linux (#8087).
362+
pub(crate) const MODULE_SOURCE_NAME: &str = "perry_module";
363+
352364
impl LlModule {
353365
pub(crate) fn declaration_lines(&self) -> impl Iterator<Item = (&str, &str)> {
354366
self.declarations
@@ -680,6 +692,7 @@ impl LlModule {
680692
pub(crate) fn skeleton_ir(&self) -> String {
681693
let mut ir = String::new();
682694
ir.push_str("; Generated by perry-codegen\n");
695+
ir.push_str(&format!("source_filename = \"{MODULE_SOURCE_NAME}\"\n"));
683696
ir.push_str(&format!("target triple = \"{}\"\n\n", self.target_triple));
684697
if crate::codegen::helpers::native_stack_roots_enabled()
685698
&& self.target_triple.contains("apple")
@@ -720,6 +733,7 @@ impl LlModule {
720733
pub fn to_ir(&self) -> String {
721734
let mut ir = String::new();
722735
ir.push_str("; Generated by perry-codegen\n");
736+
ir.push_str(&format!("source_filename = \"{MODULE_SOURCE_NAME}\"\n"));
723737
ir.push_str(&format!("target triple = \"{}\"\n\n", self.target_triple));
724738
if crate::codegen::helpers::native_stack_roots_enabled()
725739
&& self.target_triple.contains("apple")
@@ -1030,6 +1044,7 @@ impl LlModule {
10301044
let defined: HashSet<&str> = bucket.iter().map(|f| f.name.as_str()).collect();
10311045
let mut pre = String::new();
10321046
pre.push_str("; Generated by perry-codegen (codegen unit)\n");
1047+
pre.push_str(&format!("source_filename = \"{MODULE_SOURCE_NAME}\"\n"));
10331048
pre.push_str(&format!("target triple = \"{}\"\n\n", self.target_triple));
10341049
if crate::codegen::helpers::native_stack_roots_enabled()
10351050
&& self.target_triple.contains("apple")
@@ -1470,6 +1485,55 @@ mod tests {
14701485
);
14711486
}
14721487

1488+
#[test]
1489+
fn every_module_header_declares_the_same_source_filename() {
1490+
// #8087: the recorded source name is what ELF stores as the object's
1491+
// `STT_FILE` symbol. If a header site omits it, LLVM substitutes the
1492+
// path that reached the assembler — a per-call temp name on the textual
1493+
// path, the in-memory module id on the native one — and the two
1494+
// construction paths can no longer produce byte-identical objects.
1495+
// Mach-O records no such symbol, so a macOS-only check of this would be
1496+
// vacuous; asserting on the emitted TEXT keeps it host-independent.
1497+
let declaration = format!("source_filename = \"{MODULE_SOURCE_NAME}\"");
1498+
1499+
let mut m = LlModule::new("x86_64-unknown-linux-gnu");
1500+
for name in ["first", "second"] {
1501+
let f = m.define_function(name, I32, vec![]);
1502+
f.create_block("entry").ret(I32, "0");
1503+
}
1504+
1505+
assert!(
1506+
m.to_ir().contains(&declaration),
1507+
"to_ir must declare the source filename:\n{}",
1508+
m.to_ir()
1509+
);
1510+
1511+
// A real split: every unit is compiled separately, so every unit
1512+
// prologue needs the declaration, not just the first.
1513+
let units = m.render_codegen_units(2);
1514+
assert_eq!(units.len(), 2, "fixture must exercise a real split");
1515+
for (i, unit) in units.iter().enumerate() {
1516+
assert!(
1517+
unit.contains(&declaration),
1518+
"codegen unit {i} must declare the source filename:\n{unit}"
1519+
);
1520+
}
1521+
}
1522+
1523+
#[cfg(feature = "llvm-inprocess")]
1524+
#[test]
1525+
fn skeleton_ir_declares_the_same_source_filename_as_to_ir() {
1526+
// The native path parses `skeleton_ir`; the textual path compiles
1527+
// `to_ir`. They must record the same name or #8087 returns.
1528+
let mut m = LlModule::new("x86_64-unknown-linux-gnu");
1529+
let f = m.define_function("only", I32, vec![]);
1530+
f.create_block("entry").ret(I32, "0");
1531+
1532+
let declaration = format!("source_filename = \"{MODULE_SOURCE_NAME}\"");
1533+
assert!(m.skeleton_ir().contains(&declaration));
1534+
assert!(m.to_ir().contains(&declaration));
1535+
}
1536+
14731537
#[test]
14741538
fn render_codegen_units_single_unit_matches_to_ir() {
14751539
let mut m = LlModule::new("arm64-apple-macosx15.0.0");

crates/perry-codegen/src/native_emit.rs

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -594,7 +594,14 @@ mod tests {
594594
use crate::types::{I1, I32, I64, PTR, VOID};
595595

596596
fn precise_root_fixture(extra_plain_function: bool) -> LlModule {
597-
let mut module = LlModule::new(crate::codegen::default_target_triple());
597+
precise_root_fixture_for(
598+
&crate::codegen::default_target_triple(),
599+
extra_plain_function,
600+
)
601+
}
602+
603+
fn precise_root_fixture_for(triple: &str, extra_plain_function: bool) -> LlModule {
604+
let mut module = LlModule::new(triple);
598605
module.declare_function_with_ret_attrs("js_shadow_frame_enter", PTR, &[I32], "nonnull");
599606
module.declare_function("js_shadow_frame_pop", VOID, &[I64]);
600607
module.declare_function("js_shadow_slot_bind", VOID, &[I32, PTR]);
@@ -785,6 +792,38 @@ mod tests {
785792
);
786793
}
787794

795+
/// #8087: the same construction-path comparison, pinned to an **ELF**
796+
/// target rather than the host's.
797+
///
798+
/// The three sibling tests above ran only against the host triple, so on a
799+
/// macOS developer machine they exercised Mach-O exclusively — and Mach-O
800+
/// records no `STT_FILE` symbol. That is precisely why a module-name
801+
/// difference that made all of them fail on the Linux runner was invisible
802+
/// locally for two days. Naming the object format explicitly keeps this
803+
/// check honest on every host.
804+
#[test]
805+
fn native_and_text_arms_agree_on_an_elf_target() {
806+
const ELF_TRIPLE: &str = "x86_64-unknown-linux-gnu";
807+
let _native = crate::codegen::helpers::NativeRootsPin::native();
808+
let module = precise_root_fixture_for(ELF_TRIPLE, false);
809+
810+
let text = crate::linker::compile_ll_to_object(&module.to_ir(), Some(ELF_TRIPLE))
811+
.expect("trusted text arm emits an ELF object");
812+
let native = compile_module_native(&module, Some(ELF_TRIPLE), "native_root_elf_fixture")
813+
.expect("direct native arm emits an ELF object");
814+
815+
assert_eq!(
816+
&text[..4],
817+
b"\x7fELF",
818+
"fixture must actually produce ELF, or this test proves nothing"
819+
);
820+
assert_eq!(
821+
native, text,
822+
"native and text construction must emit byte-identical ELF objects; \
823+
a difference here is a recorded-name or lowering divergence (#8087)"
824+
);
825+
}
826+
788827
#[test]
789828
fn split_native_construction_propagates_shadow_backend_to_workers() {
790829
let _shadow = crate::codegen::helpers::NativeRootsPin::shadow();

0 commit comments

Comments
 (0)