diff --git a/changelog.d/8058-split-native-ptrtoint-constants.md b/changelog.d/8058-split-native-ptrtoint-constants.md new file mode 100644 index 0000000000..9ca46e5a69 --- /dev/null +++ b/changelog.d/8058-split-native-ptrtoint-constants.md @@ -0,0 +1,7 @@ +### Fix relocatable constants in split native codegen + +Production-sized modules using split native codegen now materialize LLVM +`ptrtoint` constant operands for function and global references instead of +misparsing them as integer literals. Module-init wrappers can therefore pass +their `__init_body` function pointer through the runtime ABI while retaining a +real relocation in each independently emitted and partially linked object. diff --git a/crates/perry-codegen/src/dialect/types.rs b/crates/perry-codegen/src/dialect/types.rs index 0ed69c93ed..247a816695 100644 --- a/crates/perry-codegen/src/dialect/types.rs +++ b/crates/perry-codegen/src/dialect/types.rs @@ -202,6 +202,48 @@ pub(super) fn constant<'ctx>( } bail!("reference to unknown global @{name}"); } + // LLVM constant expressions can appear anywhere an ordinary constant is + // accepted. Perry emits this exact form when a non-entry module passes its + // `__init_body` function pointer through the integer-valued runtime ABI: + // + // call void @js_run_module_init_catching( + // i64 ptrtoint (ptr @module__init_body to i64)) + // + // The text backend has always delegated this to LLVM's assembler. The + // in-process reader used to feed the whole expression to `i128::parse`, so + // only split native codegen units failed, late in a large build, with + // `bad integer ptrtoint (...)`. Materialize the relocatable constant with + // LLVM's constant API; using a runtime instruction would be invalid here + // because this is an operand constant, not an SSA definition. + if let Some(body) = tok + .strip_prefix("ptrtoint (") + .and_then(|body| body.strip_suffix(')')) + { + let (source, destination) = body + .rsplit_once(" to ") + .ok_or_else(|| anyhow!("bad ptrtoint constant `{tok}`"))?; + let (source_ty, source_value) = ty_and_val(source)?; + let source_ty = basic_type(ctx, source_ty)?; + if !source_ty.is_pointer_type() { + bail!("ptrtoint source is not a pointer in `{tok}`"); + } + let destination_ty = basic_type(ctx, destination.trim())?; + let BasicTypeEnum::IntType(destination_ty) = destination_ty else { + bail!("ptrtoint destination is not an integer in `{tok}`"); + }; + let BasicTypeEnum::IntType(expected_ty) = ty else { + bail!("ptrtoint constant used as non-integer {ty:?}"); + }; + if destination_ty != expected_ty { + bail!( + "ptrtoint destination type {} disagrees with operand type {}", + destination_ty.print_to_string(), + expected_ty.print_to_string() + ); + } + let source = constant(ctx, module, source_ty, source_value)?.into_pointer_value(); + return Ok(source.const_to_int(destination_ty).into()); + } Ok(match tok { // The null of the OPERAND's type, not of address space 0: RS4GC emits // `store ptr addrspace(1) null, ptr %slot`, and an addrspace(0) null diff --git a/crates/perry-codegen/src/native_emit.rs b/crates/perry-codegen/src/native_emit.rs index 3e2f891fb9..b3705e41d1 100644 --- a/crates/perry-codegen/src/native_emit.rs +++ b/crates/perry-codegen/src/native_emit.rs @@ -557,6 +557,45 @@ fn debug_dump(module: &Module<'_>, module_prefix: &str) { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::module::LlModule; + use crate::types::{I64, VOID}; + + #[test] + fn split_units_emit_and_merge_init_body_pointer_constant() { + // Production webpack modules are large enough to use split native + // codegen. Every non-entry module's guard passes `__init_body` to the + // exception boundary as an i64 constant expression. Keep the callee + // and wrapper in separate units so this covers declaration lookup, + // relocatable ptrtoint construction, object emission, and the partial + // link -- a parse-only test would miss the production-graph failure + // tracked in #8057. + let mut module = LlModule::new(crate::codegen::default_target_triple()); + module.declare_function("js_run_module_init_catching", VOID, &[I64]); + + let body = module.define_function("fixture_js__init_body", VOID, vec![]); + body.create_block("entry").ret_void(); + + let wrapper = module.define_function("fixture_js__init", VOID, vec![]); + let entry = wrapper.create_block("entry"); + entry.call_void( + "js_run_module_init_catching", + &[(I64, "ptrtoint (ptr @fixture_js__init_body to i64)")], + ); + entry.ret_void(); + + let object = + compile_module_units_native(&mut module, 2, None, "split_ptrtoint_init_body_fixture") + .expect("split native units must emit and partial-link"); + assert!( + !object.is_empty(), + "merged object must contain emitted code" + ); + } +} + /// Differential harness: text-parsed arm vs natively-built arm, same LLVM, /// same plan. The verdict is **emitted object bytes** — the C-API builder /// constant-folds at construction (`zext i1 false`, `select i1 false, ...`),