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
30 changes: 30 additions & 0 deletions changelog.d/8000-gc-strategy-define-header-pin.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
### Codegen: pin the GC strategy onto the define line, in both root lowerings (#7982 follow-up)

#7998 made `LlFunction::define_header` the single renderer of the `define … {`
line, after the in-process native path's private copy silently lost
`gc "statepoint-example"` — a natively-constructed module then got no RS4GC
pass and therefore **no precise roots at all**, while verifying, linking and
running correctly on any program that does not collect.

The test that shipped with it does not actually pin that property, and the two
attempts to fix it failed the same way the original bug did. Recorded because
it is this change's own bug class occurring inside its own test:

1. The `to_ir` == `define_header` agreement test **cannot** see a dropped
strategy: with one shared renderer both sides change identically. Sabotage
passed.
2. A dedicated strategy test that branched on `native_stack_roots_enabled()`
never ran its ON arm under `cargo test` — no module has called
`set_native_roots_for_target`, so the predicate is false in the test
process. Sabotage passed again.
3. Only pinning **both** lowerings with `NativeRootsPin::{native,shadow}`, and
asserting `stack_map_slot_count` in each arm first so neither can pass
vacuously, goes red on the sabotage.

Native-roots must take the stack-map path AND name the strategy; shadow-stack
must not take it AND must not name the strategy. The tests live in
`function.rs`, which compiles WITHOUT the `llvm-inprocess` feature, so they run
in per-PR `cargo-test` rather than only in the feature job.

Sabotage-verified with the fix committed first, then restored and REBUILT: 935
`perry-codegen` lib tests green under `--features llvm-inprocess`.
Comment on lines +9 to +30

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Keep the changelog fragment focused on shipped behavior.

The fragment mixes the final behavior with failed-test history, sabotage steps, and internal test-run details. Keep one concise entry that states the shipped contract: native-roots functions that reserve stack-map slots emit gc "statepoint-example"; shadow-stack functions do not; regression coverage covers both.

Based on learnings: “For changelog fragments under changelog.d/, describe the final shipped behavior as one coherent release-note entry. Do not include separate development-slice narratives.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@changelog.d/8000-gc-strategy-define-header-pin.md` around lines 9 - 30, The
changelog fragment should contain one concise release-note entry describing only
the shipped contract: native-roots functions reserving stack-map slots emit gc
"statepoint-example", shadow-stack functions do not, and regression tests cover
both paths. Remove the failed-test history, sabotage details, implementation
attempts, and internal test-run information from the fragment.

Source: Learnings

74 changes: 74 additions & 0 deletions crates/perry-codegen/src/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1022,6 +1022,80 @@ mod define_header_tests {
}
}

/// The property that was actually lost, asserted directly (#7982) — in
/// **both** lowerings, neither of them dark.
///
/// The header/`to_ir` agreement test above cannot see this: with one shared
/// renderer, dropping `gc "statepoint-example"` changes both sides
/// identically and that test stays green. Verified by doing exactly that.
///
/// The first version of this test read `native_stack_roots_enabled()` and
/// branched on it. Under `cargo test` that predicate is false — no module
/// has called `set_native_roots_for_target` — so the ON arm, the one that
/// matters, never executed and the same sabotage passed a second time. That
/// is the failure mode this whole change is about, reproduced inside its own
/// test. `NativeRootsPin` exists for precisely this; both arms now run every
/// time, and each carries a liveness assertion that its lowering was
/// actually selected.
#[test]
fn the_gc_strategy_appears_exactly_when_the_function_asks_for_a_stack_map() {
use crate::codegen::helpers::NativeRootsPin;
const STRATEGY: &str = "gc \"statepoint-example\"";

{
let _native = NativeRootsPin::native();
let plain = probe();
assert!(
!plain.define_header(false).contains(STRATEGY),
"a function that never reserved a stack-map slot must not claim \
a GC strategy, even under the native-roots lowering"
);

let mut mapped = probe();
mapped.enable_shadow_frame(0);
assert!(
mapped.reserve_shadow_slot().is_some(),
"a shadow-framed function must yield a root slot"
);
assert_eq!(
mapped.stack_map_slot_count, 1,
"under the native-roots pin the reservation must take the \
stack-map path — otherwise the assertion below is vacuous"
);
assert!(
mapped.define_header(false).contains(STRATEGY),
"a stack-map function's define line MUST name the GC strategy. \
Without it RS4GC never runs on the module, which verifies, \
links and executes correctly on any program that does not \
collect while having NO precise roots at all — #7332's shape, \
and exactly what the in-process native backend shipped until \
#7982"
);
}

{
let _shadow = NativeRootsPin::shadow();
let mut mapped = probe();
mapped.enable_shadow_frame(0);
assert!(
mapped.reserve_shadow_slot().is_some(),
"a shadow-framed function must yield a root slot in this \
lowering too"
);
assert_eq!(
mapped.stack_map_slot_count, 0,
"under the shadow-stack pin the reservation must NOT take the \
stack-map path"
);
assert!(
!mapped.define_header(false).contains(STRATEGY),
"the shadow-stack lowering must NOT name a GC strategy — RS4GC \
is not the backend in that build, and claiming it would run the \
pass over IR that has no addrspace(1) roots to rewrite"
);
}
}

/// `force_external` drops only the linkage keyword. The codegen-unit path
/// depends on that and on nothing else changing.
#[test]
Expand Down
11 changes: 11 additions & 0 deletions gc-handoff/KNOBS-NOTES.md
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,17 @@ Six shapes, each exposed by fixing the previous one:
Fixed structurally (one `LlFunction::define_header`), pinned in `function.rs`
which compiles WITHOUT the feature, so per-PR CI sees it.

★ The pin took THREE attempts, and the first two failed the same way the bug
did. (a) The `to_ir == define_header` agreement test cannot see a dropped
`gc "statepoint-example"` at all — one renderer means both sides change
identically; sabotage passed. (b) A dedicated strategy test that BRANCHED on
`native_stack_roots_enabled()` never ran its ON arm under `cargo test`: no
module has called `set_native_roots_for_target`, so the predicate is false in
the test process and the sabotage passed again. Only (c), pinning both
lowerings with `NativeRootsPin::{native,shadow}` and asserting
`stack_map_slot_count` in each arm so neither is vacuous, goes red on the
sabotage. Reproduced this PR's own bug class twice inside its own test.

### Are the `.ll` corpora live or still frozen? — **LIVE**

Refreshed to 497 / 1082 / 420 `addrspace(1)` sites (were 0/0/0). Two mechanisms
Expand Down
Loading