Product boundary
Split native codegen units can emit a mapped function with no compact GC map. The native-roots decision is stored in thread-local state on the module producer, but compile_module_units_native always emits units on newly spawned worker threads. Those workers start with both NATIVE_ROOTS_OVERRIDE unset and NATIVE_ROOTS_TARGET_OK=false.
This is not an Actions-only freshness failure. It is a source-causal product hole:
- codegen::compile_module decides the target lowering with set_native_roots_for_target on the producer thread. Tests can make the same decision with NativeRootsPin::native.
- compile_module_units_native freezes units, then std::thread::scope spawns worker threads.
- Each worker calls native_plan_args and optimize_and_emit_module. Both ultimately consult rs4gc_enabled on that worker.
- The worker-local default is false, so the plan does not request the assembly/compact-map path and optimize_and_emit skips STATEPOINT_REWRITE_PASSES.
- A function whose IR contains addrspace(1) roots and a GC strategy therefore reaches object emission without statepoint rewriting or __perry_gcmap.
Deterministic local reproduction
At exact main f9475fb, with LLVM 22.1.4:
LLVM_SYS_221_PREFIX=/opt/homebrew/opt/llvm \
PERRY_CODEGEN_UNIT_JOBS=1 \
cargo test --profile perry-dev -p perry-codegen \
--features llvm-inprocess --lib \
native_emit::tests::split_native_construction_lowers_precise_roots_before_rs4gc \
-- --exact --nocapture
Result: FAIL. The text reference stays on the pinned producer when jobs=1 and includes __PERRY_GCMAP,__perry_gcmap; the native arm still spawns one worker and omits the map.
Without the explicit one-job arm on a higher-core host, compile_units_to_object also spawns workers. Both reference and native arms then lose the same thread-local decision and can compare equal while both are unmapped. That explains the earlier local false green and makes the current regression sensitive to machine parallelism.
The same deterministic boundary appeared in main runs 31766084651 and 31766266548 under LLVM 22.1.8: 990/991 unit tests passed; the native object was 808 bytes with no __perry_gcmap, while the text object was 1040 bytes with the map. Run 31766266548 failed before native smoke.
Required fix / acceptance
- Resolve the native-roots/backend decision once on the producer and pass it explicitly in every frozen unit payload or worker closure. Do not have the LLVM emission layer re-read producer-owned thread-local state.
- Use that same explicit value for plan selection and STATEPOINT_REWRITE_PASSES so assembly/map finishing cannot disagree with rewriting.
- Make the split precise-root regression force the deterministic jobs=1 asymmetry (or add an equivalent explicit worker-decision assertion), and positively prove the resulting object contains a non-empty __perry_gcmap before byte equality.
- Retain a negative unsupported-target/shadow-stack arm so explicit propagation cannot turn the decision into an unconditional true.
- Pass the focused regression with LLVM 22.1.4 locally. A CI result is freshness bookkeeping, not proof.
Discovered while reconciling #7966. That issue must remain open until this product boundary is fixed and the llvm-inprocess gate is locally non-vacuous.
Product boundary
Split native codegen units can emit a mapped function with no compact GC map. The native-roots decision is stored in thread-local state on the module producer, but compile_module_units_native always emits units on newly spawned worker threads. Those workers start with both NATIVE_ROOTS_OVERRIDE unset and NATIVE_ROOTS_TARGET_OK=false.
This is not an Actions-only freshness failure. It is a source-causal product hole:
Deterministic local reproduction
At exact main f9475fb, with LLVM 22.1.4:
Result: FAIL. The text reference stays on the pinned producer when jobs=1 and includes __PERRY_GCMAP,__perry_gcmap; the native arm still spawns one worker and omits the map.
Without the explicit one-job arm on a higher-core host, compile_units_to_object also spawns workers. Both reference and native arms then lose the same thread-local decision and can compare equal while both are unmapped. That explains the earlier local false green and makes the current regression sensitive to machine parallelism.
The same deterministic boundary appeared in main runs 31766084651 and 31766266548 under LLVM 22.1.8: 990/991 unit tests passed; the native object was 808 bytes with no __perry_gcmap, while the text object was 1040 bytes with the map. Run 31766266548 failed before native smoke.
Required fix / acceptance
Discovered while reconciling #7966. That issue must remain open until this product boundary is fixed and the llvm-inprocess gate is locally non-vacuous.