test(codegen): pin the GC strategy onto the define line, in BOTH root lowerings - #8000
Conversation
…H lowerings The header/`to_ir` agreement test added alongside `LlFunction::define_header` cannot see the defect it was written for: with one shared renderer, dropping `gc "statepoint-example"` changes both sides identically and the test stays green. Verified by doing exactly that. The first replacement branched on `native_stack_roots_enabled()`. Under `cargo test` that predicate is false — no module has called `set_native_roots_for_target` — so the ON arm never executed and the same sabotage passed a second time. That is this change's own bug class, reproduced inside its own test, twice. `NativeRootsPin` exists for this. Both lowerings now run every time, and each arm asserts `stack_map_slot_count` first so neither can pass vacuously: native-roots must take the stack-map path AND name the strategy; shadow-stack must not take it AND must not name the strategy. Sabotage-verified with the fix committed first: dropping the strategy from the one renderer fails this test with the message naming #7332's shape, and the other two stay green. Restored and REBUILT to re-confirm; 935 perry-codegen lib tests pass under `--features llvm-inprocess`, and the three `define_header_tests` also pass WITHOUT the feature, i.e. in per-PR CI. Refs #7982.
📝 WalkthroughWalkthroughThe change adds regression coverage for GC strategy emission under native-root and shadow-stack lowering. It verifies stack-map slot accounting and documents prior vacuous test approaches and the final test setup. ChangesGC strategy pinning
Estimated code review effort: 2 (Simple) | ~10 minutes Mergeability Score: ⚪ Minimal · up to This localized test-focused change is merge-ready after normal checks; the only follow-up is to trim the changelog to describe shipped behavior rather than development history, with no actionable merge-blocking risk remaining. Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
1725769 to
4e3db54
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with 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.
Inline comments:
In `@changelog.d/8000-gc-strategy-define-header-pin.md`:
- Around line 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.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 50e4f153-b55d-43c7-9ba2-6fd88b6a4ce1
📒 Files selected for processing (3)
changelog.d/8000-gc-strategy-define-header-pin.mdcrates/perry-codegen/src/function.rsgc-handoff/KNOBS-NOTES.md
| 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`. |
There was a problem hiding this comment.
📐 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
Follow-up to #7998 (both refs #7982). Small, and it is a story about tests rather than about code.
#7998 made
LlFunction::define_headerthe single renderer of thedefine … {line, after the in-process native path's private copy silently lostgc "statepoint-example". Without that strategy RS4GC never runs on the module: it verifies, links and executes correctly on any program that does not collect, while having no precise roots at all.The test that shipped with #7998 does not pin that property, and my first two attempts to fix it failed the same way the original bug did.
to_ir==define_headeragreement test cannot see a dropped strategy — with one shared renderer, both sides change identically. Sabotage passed.native_stack_roots_enabled()never ran its ON arm undercargo test: no module has calledset_native_roots_for_target, so the predicate is false in the test process. Sabotage passed again.NativeRootsPin::{native,shadow}— and assertingstack_map_slot_countin each arm first, so neither can pass vacuously — goes red on the sabotage.That is this change's own bug class occurring twice inside its own test, which is why it is written up rather than quietly fixed.
Native-roots must take the stack-map path and name the strategy; shadow-stack must not take it and must not name it. Both arms carry a liveness assertion.
The tests live in
function.rs, which compiles without thellvm-inprocessfeature, so they run in per-PRcargo-testrather than only in the feature job.Sabotage-verified with the fix committed first: dropping the strategy from the one renderer fails this test with the message naming #7332's shape, and the other two stay green. Restored and rebuilt (not merely
git checkout-ed) to re-confirm. 935perry-codegenlib tests green under--features llvm-inprocess.Summary by CodeRabbit
Bug Fixes
Documentation