Skip to content

test(gc): run the unit-test build in production's conservative-scan mode - #7147

Merged
proggeramlug merged 5 commits into
mainfrom
fix/7146-gc-test-precise-rooting
Aug 1, 2026
Merged

test(gc): run the unit-test build in production's conservative-scan mode#7147
proggeramlug merged 5 commits into
mainfrom
fix/7146-gc-test-precise-rooting

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

What

Makes the perry-runtime unit-test build run production's GC root
configuration, so the suite can see the bug class it exists to catch.

gc::roots::conservative_stack_scan_mode() defaulted the test build to
ConservativeStackScanMode::Full while production resolves
Auto -> SkipDisabled. Three consequences, all in the direction that hides
defects:

  1. A missing precise root was rescued under test and wrong in production.
    Exactly gc: the relocating minor produces a deterministic wrong answer in the SHIPPED default on an ordinary event-loop workload #7055's shape. The suite filtered out the failure class it exists
    to catch.
  2. The copying minor was ineligible under test
    (CopiedMinorFallbackReason::ConservativeStack), so relocation went largely
    unexercised.
  3. A conservative scan keeps arbitrary native-stack garbage alive, which is
    the wrong direction for "this object should have been collected" — the
    tests most dependent on precision are the ones it degraded most.

The comment justifying it described how some tests were written, not a
requirement of testing.

Why it was cheap

The correct mechanism already existed and the tests already used it
(RuntimeHandleScope / gc_temp_root_*, plus the isolation guards, which pin
Auto themselves). 351 of the 531 gc tests were already running in
production's mode.
Flipping the default for the rest cost exactly one test
conversion.

The one conversion

test_minor_preserves_old_to_young_edge_across_minors asserted the parent's
slot still held ptr_bits(child) — the address captured before the first
minor. That only held because Full made the minor non-moving. With precise
roots the copying minor relocates the child and correctly rewrites the slot:

  left: 9222531965582835720
 right: 9222531965580476424

Reading a raw local across a relocating collection is the defect class this
suite exists to catch, so the test now does what generated code does and
re-reads the child out of the parent's slot.

Probe measurement (scan off): cycles 0–2 run copied=1 promoted=0 with the RS
edge intact; cycle 3 reports promoted=1 and the child leaves the nursery, at
which point the edge is old→old and the remembered set correctly retires it. The
loop now handles that explicitly instead of asserting an RS entry that should
not exist, with an rs_covered_cycles >= 2 floor so it cannot go vacuous.

Also fixes #7145

js_shadow_frame_pop's corrupted-handle guard used debug_assert!(false, …)
inside an extern "C" fn. In a debug build the assert fires, the panic cannot
unwind across the boundary, and the process aborts — so
cargo test -p perry-runtime --lib in the dev profile SIGABRTed the whole
test binary. CI runs --release, where debug_assert! compiles out, so the
gate was structurally blind to it: an entire profile of the runtime suite was
un-runnable and no gate could go red.

Both that site and js_gc_temp_root_push's identical (unreachable, therefore
never-caught) overflow guard now use the
report_growth_stub_skipped_below_heap_min pattern — #[cold], one line on
stderr, once per process, so it cannot perturb a stdout parity comparison. Skip
behaviour is unchanged in both profiles.

Evidence (Mac mini, darwin-arm64, release, --test-threads=1)

configuration before after
default 1574 pass / 0 fail 1574 pass / 0 fail
PERRY_CONSERVATIVE_STACK_SCAN=0 1573 pass / 1 fail 1574 pass / 0 fail

The red-then-green is the point: the scan-off arm failed before the conversion
and passes after.

Sabotage-verified both directions — injecting remembered_set_clear() after the
minor (simulating the #6181 dropped edge) turns the converted test red in both
scan modes, so it still catches the bug it was written for.

ConservativeScanAutoGuard is deleted: with the default already Auto it set
the value it was going to get anyway and could no longer fail. Per CLAUDE.md's
kill-policy a mode that cannot be exercised is deleted, not left for a future
bisect to trust. Its single user moves to ConservativeScanDisabledGuard, which
asserts the stronger property it actually needs.

Unrelated, folded in because it blocks every PR

crates/perry-codegen/src/linker.rs:96 landed with a rustfmt violation in
#7135, so cargo fmt --all -- --check is red on main as of a3b31c0 and
lint fails on every open PR. One rustfmt-only commit here unblocks it.

Not in this PR

Removing ConservativeStackScanMode::Full, the six force_full_scan() sites
and the PERRY_CONSERVATIVE_STACK_SCAN knob is a production behaviour
change and is deliberately separate — see the follow-up. This PR is test-build
only.

Worth recording here: PERRY_CONSERVATIVE_STACK_SCAN=full fails 134 of 1574
runtime tests on main today (verified against pristine main roots.rs, so
not caused by this PR). The env value beats the per-thread override, so the
isolation guards cannot opt back out. That is an untested shipped
configuration — and the gc_ratchet baseline is measured under it.

Closes #7145

Summary by CodeRabbit

  • Bug Fixes

    • Improved garbage collection reliability across production and test builds.
    • Fixed relocation handling for objects referenced through repeated minor collections.
    • Replaced certain runtime assertion failures with one-time diagnostic messages, allowing safe early returns without unexpected termination.
  • Tests

    • Expanded coverage for promoted objects, relocated references, tagged pointers, and multi-cycle collection behavior.
    • Updated test configuration to explicitly control native-stack scanning.
  • Maintenance

    • Applied formatting and documentation updates for consistency.

Ralph Küpper added 4 commits July 31, 2026 22:27
…asserts

`js_shadow_frame_pop`'s corrupted-handle guard used `debug_assert!(false, …)`
inside an `extern "C"` fn. In a debug build the assert fires, the panic cannot
unwind across the `extern "C"` boundary, and the process aborts — so
`cargo test -p perry-runtime --lib` in the dev profile SIGABRTed the whole test
binary at `gc::tests::shadow_stack_ops::out_of_range_frame_pop_is_ignored`,
which drives that path on purpose.

CI runs the suite `--release`, where `debug_assert!` compiles out, so the gate
was structurally blind to it: an entire profile of the runtime test suite was
un-runnable and no gate could go red.

Both sites now use the `report_growth_stub_skipped_below_heap_min` pattern —
a `#[cold]` fn emitting one line on stderr, once per process, so it cannot
perturb a stdout parity comparison. Skip behaviour is unchanged in both
profiles.

`js_gc_temp_root_push`'s overflow guard had the identical defect (unreachable
in practice — it needs u32::MAX live temp roots — hence never caught) and is
fixed the same way rather than left as a latent abort.

Closes #7145

Claude-Session: https://claude.ai/code/session_018ZFER8EEg8K7ez2n6oDrT9
…tale local

`test_minor_preserves_old_to_young_edge_across_minors` asserted the parent's
slot still held `ptr_bits(child)` -- the address captured *before* the first
minor. That only held because the unit-test build defaults the conservative
native-stack scan to `Full`, which makes the copying minor ineligible
(`CopiedMinorFallbackReason::ConservativeStack`) and therefore non-moving.

With precise roots only (`PERRY_CONSERVATIVE_STACK_SCAN=0`, which is what
production resolves to) the copying minor runs and RELOCATES the child on every
cycle, correctly rewriting the parent's slot -- and the assertion compared the
new address against the stale local:

  left: 9222531965582835720
  right: 9222531965580476424

Reading a raw local across a relocating collection is precisely the defect
class this suite exists to catch, so the test now does what generated code
does: it re-reads the child out of the parent's slot after each minor.

Measured (probe, scan off): cycles 0-2 run `copied=1 promoted=0` with the RS
edge intact; cycle 3 reports `promoted=1` and the child leaves the nursery, at
which point the edge is old->old and the remembered set correctly retires it.
The loop now handles that explicitly -- a tenured child must be live old-gen
memory, not swept -- instead of asserting an RS entry that should not exist.

A `rs_covered_cycles >= 2` floor keeps the loop from going vacuous if the child
is ever tenured early, and the slot is checked to still round-trip through
`ptr_bits` so a corrupted tag cannot pass as a relocation.

Claude-Session: https://claude.ai/code/session_018ZFER8EEg8K7ez2n6oDrT9
The test build defaulted `conservative_stack_scan_mode()` to `Full` while
production resolves `Auto -> SkipDisabled`. That made the suite a DIFFERENT GC
configuration from the thing it certifies, in the direction that hides bugs:

* a missing precise root -- the #7055 shape -- was rescued by the native scan
  under test and was a live wrong answer in production, so the suite filtered
  out exactly the failure class it exists to catch;
* the copying minor was ineligible under test
  (`CopiedMinorFallbackReason::ConservativeStack`), so relocation went largely
  unexercised;
* a conservative scan keeps arbitrary native-stack garbage alive, which is
  exactly the wrong direction for "this object should have been collected".

The justification in the comment described how some tests were written, not a
requirement of testing. The correct mechanism already existed and the tests
already used it -- `RuntimeHandleScope` / `gc_temp_root_*`, plus the isolation
guards, which pin `Auto` themselves. 351 of the 531 gc tests were already
running in production's mode; flipping the default for the rest cost exactly one
test conversion (the preceding commit).

Measured on the mini (release, --test-threads=1): 1574 passed / 0 failed with
the default flipped, and unchanged under an explicit
`PERRY_CONSERVATIVE_STACK_SCAN=0`.

Also drops `ConservativeScanAutoGuard`. Its whole job was opting a thread back
to `Auto` from the `Full` default; with the default already `Auto` it set the
value it was going to get anyway and could no longer fail, so per CLAUDE.md's
kill-policy it is deleted rather than left as an untested no-op. Its single user
moves to `ConservativeScanDisabledGuard`, which asserts the stronger property
that test actually needs (objects held only as native locals must be COLLECTED).

Unrelated, and folded in only because it blocks `lint` on every open PR:
`crates/perry-codegen/src/linker.rs:96` landed with a rustfmt violation in
#7135, so `cargo fmt --all -- --check` is red on `main` as of a3b31c0.

Claude-Session: https://claude.ai/code/session_018ZFER8EEg8K7ez2n6oDrT9
#7135 landed a formatting violation at linker.rs:96, so `cargo fmt --all --
--check` is red on `main` (a3b31c0) and blocks `lint` on every open PR.
rustfmt-only, no behaviour change.

Claude-Session: https://claude.ai/code/session_018ZFER8EEg8K7ez2n6oDrT9
@coderabbitai

coderabbitai Bot commented Jul 31, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The change aligns test and production conservative GC scanning, updates GC tests for relocated references, replaces C-ABI debug assertions with non-panicking diagnostics, removes an obsolete test guard, and reformats one linker expression.

Changes

GC root handling

Layer / File(s) Summary
Unified conservative scan defaults
crates/perry-runtime/src/gc/roots.rs, crates/perry-runtime/src/gc/tests/support.rs, crates/perry-runtime/src/gc/tests/oldgen.rs, changelog.d/7147-gc-test-build-production-scan-mode.md
All builds use Auto conservative scanning. Tests that require disabled scanning use ConservativeScanDisabledGuard.
Non-panicking root-stack diagnostics
crates/perry-runtime/src/gc/roots/shadow_stack.rs, crates/perry-runtime/src/gc/roots/temp_roots.rs, changelog.d/7147-gc-test-build-production-scan-mode.md
Malformed frame pops and temporary-root overflow report once per process and preserve skip behavior without unwinding across C-ABI boundaries.
Relocating remembered-set test coverage
crates/perry-runtime/src/gc/tests/oldgen.rs, changelog.d/7147-gc-test-build-production-scan-mode.md
The repeated minor-GC test rereads children through the parent slot, tracks relocation and promotion, validates tagged pointers, and requires multiple remembered-set cycles.

Linker formatting

Layer / File(s) Summary
Fallback linker write formatting
crates/perry-codegen/src/linker.rs, changelog.d/7147-gc-test-build-production-scan-mode.md
The fallback .ll write error-context expression was reformatted without behavioral changes.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: andrewtdiz

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary change: running GC unit tests with production conservative-scan behavior.
Description check ✅ Passed The description is detailed and covers the change, rationale, related issue, test evidence, and scope, despite using headings different from the template.
Linked Issues check ✅ Passed The changes satisfy #7145 by replacing both extern-C debug assertions with non-panicking diagnostics while preserving skip behavior.
Out of Scope Changes check ✅ Passed The GC configuration, test updates, diagnostics, formatting fix, and changelog entry are all covered by the stated PR objectives.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7146-gc-test-precise-rooting

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 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 `@crates/perry-runtime/src/gc/roots.rs`:
- Around line 253-277: Update the comment for
ScopedRootScannerRegistryGuard::new to remove the claim that its Auto override
opts out of a test-only Full default. Keep the override if it preserves test
isolation, and describe that current isolation purpose instead.

In `@crates/perry-runtime/src/gc/roots/shadow_stack.rs`:
- Around line 562-567: Make both C-ABI diagnostics non-panicking by replacing
the eprintln! calls in crates/perry-runtime/src/gc/roots/shadow_stack.rs:562-567
and crates/perry-runtime/src/gc/roots/temp_roots.rs:77-81 with writeln! to a
locked stderr handle, explicitly discarding any write error. Preserve the
existing diagnostic messages and behavior otherwise.

In `@crates/perry-runtime/src/gc/tests/oldgen.rs`:
- Around line 754-757: Update the test around ConservativeScanDisabledGuard and
collect_minor_trace to reload child_header after collection from a rewritten
root or the relocated parent slot before performing the mark assertion. Do not
dereference the pre-collection child_header after relocation; preserve the
existing root-scanning setup and assertion behavior.
🪄 Autofix (Beta)

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: ce8ac6ae-01ca-4de6-adf1-0c8b447c2597

📥 Commits

Reviewing files that changed from the base of the PR and between a3b31c0 and 845683f.

📒 Files selected for processing (7)
  • changelog.d/7147-gc-test-build-production-scan-mode.md
  • crates/perry-codegen/src/linker.rs
  • crates/perry-runtime/src/gc/roots.rs
  • crates/perry-runtime/src/gc/roots/shadow_stack.rs
  • crates/perry-runtime/src/gc/roots/temp_roots.rs
  • crates/perry-runtime/src/gc/tests/oldgen.rs
  • crates/perry-runtime/src/gc/tests/support.rs

Comment on lines +253 to +277
// ONE default for every build, tests included.
//
// The test build used to default to `Full` on the grounds that unit tests
// root GC-managed pointers as raw locals on the *native* (Rust) stack
// rather than the shadow stack, so the conservative scan was what kept them
// alive. That is a description of how some tests were written, not a
// requirement of testing, and it had a cost that outweighed the
// convenience: it made the test build a DIFFERENT GC configuration from
// production. A missing precise root — the #7055 shape — was rescued by the
// native scan under test and was a live wrong answer in production, so the
// suite filtered out precisely the failure class it exists to catch. It
// also made the copying minor ineligible under test
// (`CopiedMinorFallbackReason::ConservativeStack`), so relocation was
// largely unexercised.
//
// The correct mechanism already existed and the tests already used it:
// `RuntimeHandleScope` / `gc_temp_root_*`, plus the isolation guards, which
// pin `Auto` themselves — 351 of the 531 gc tests were already running this
// way. Flipping the default cost exactly one test conversion
// (`test_minor_preserves_old_to_young_edge_across_minors`, which read a
// relocated child through a stale pre-collection local).
//
// A test that needs the scan *provably* off — not merely resolved off by
// today's `Auto` policy — should pin `ConservativeScanDisabledGuard`.
ConservativeStackScanMode::Auto

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

Update the related test-helper comment.

ScopedRootScannerRegistryGuard::new still says that its Auto override opts out of a test-only Full default. This change removes that default. Keep the override if it establishes test isolation, but describe its current purpose.

🤖 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 `@crates/perry-runtime/src/gc/roots.rs` around lines 253 - 277, Update the
comment for ScopedRootScannerRegistryGuard::new to remove the claim that its
Auto override opts out of a test-only Full default. Keep the override if it
preserves test isolation, and describe that current isolation purpose instead.

Comment on lines +562 to +567
eprintln!(
"[perry-gc] shadow-stack pop past end: frame handle {frame_handle:#x} is out of \
range; the pop was SKIPPED (memory-safe, over-approximates the root set). This \
usually means a caller threaded a NaN-boxed value into js_shadow_frame_pop \
instead of the index js_shadow_frame_push returned. Reported once per process."
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Rust's eprintln! macro panic if writing to stderr fails? Cite Rust standard-library documentation or source.

💡 Result:

Yes, Rust's eprintln! macro panics if writing to standard error (stderr) fails [1][2]. According to the official Rust standard library documentation, this behavior occurs because the macro is designed for reporting error and progress messages, where it is generally considered appropriate to crash if the output mechanism itself fails [1][3]. Specifically, writing to non-blocking stderr can result in an error that leads the macro to panic [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## locate files"
fd -a 'shadow_stack\.rs|temp_roots\.rs' . || true

echo
echo "## relevant sections"
for f in crates/perry-runtime/src/gc/roots/shadow_stack.rs crates/perry-runtime/src/gc/roots/temp_roots.rs; do
  if [ -f "$f" ]; then
    echo
    echo "--- $f ---"
    nl -ba "$f" | sed -n '1,120p'
    echo
    nl -ba "$f" | sed -n '530,590p'
  fi
done

echo
echo "## extern C / c_unwind / panic settings references"
rg -n 'extern\s+"C"|c_unwind|cfg_attr\s*\(|catch_unwind|should_panic|test_no_safety' crates/perry-runtime/src/gc crates/perry-runtime/src -g '*.rs' | head -200

echo
echo "## module/crate panic behavior"
rg -n 'panic!|expect\(|unwrap\(|assert!|catch_unwind|unwind|panic=' Cargo.toml crates/perry-runtime -g 'Cargo.toml' -g '*.rs' | head -250

Repository: PerryTS/perry

Length of output: 492


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "## relevant sections with cat -n"
for f in crates/perry-runtime/src/gc/roots/shadow_stack.rs crates/perry-runtime/src/gc/roots/temp_roots.rs; do
  if [ -f "$f" ]; then
    echo
    echo "--- $f ---"
    cat -n "$f" | sed -n '1,120p'
    cat -n "$f" | sed -n '530,590p'
  fi
done

echo
echo "## extern C / ABI panic references"
rg -n 'extern\s+"C"|extern "C"|c_unwind|cfg_attr\s*\(|catch_unwind|should_panic|test_no_safety' crates/perry-runtime/src/gc crates/perry-runtime/src -g '*.rs' | head -250 || true

echo
echo "## panic-related references"
rg -n 'panic!|expect\(|unwrap \(\)|assert!|catch_unwind|unwind|panic=' Cargo.toml crates/perry-runtime -g 'Cargo.toml' -g '*.rs' | head -300 || true

echo
echo "## locate C function exports"
rg -n '#\[no_mangle\]|\bjs_.*\(.*\)\s*{|pub extern "C"\s*fn|pub unsafe extern "C"\s*fn' crates/perry-runtime/src/gc crates/perry-runtime/src -g '*.rs' | head -300 || true

echo
echo "## deterministic read: source lines for print macros in target files"
python3 - <<'PY'
from pathlib import Path
for name in ["crates/perry-runtime/src/gc/roots/shadow_stack.rs", "crates/perry-runtime/src/gc/roots/temp_roots.rs"]:
    p = Path(name)
    print(f"\n--- {name} ---")
    for i,line in enumerate(p.read_text(errors="replace").splitlines(), start=1):
        if "eprintln!" in line or "extern" in line.lower() or "no_mangle" in line:
            print(f"{i}: {line}")
PY

Repository: PerryTS/perry

Length of output: 50371


🌐 Web query:

Rust std macro eprintln stderr write failure panic source

💡 Result:

The Rust eprintln! macro is explicitly designed to panic if it encounters an error while writing to io::stderr [1][2][3]. The official documentation for eprintln! explicitly states this behavior under its "Panics" section [1][2]: "Panics if writing to io::stderr fails." This behavior is particularly relevant when writing to non-blocking stderr streams, where an error can occur if the write operation cannot complete immediately, leading the macro to trigger a panic [1][4]. From an implementation perspective, the eprintln! macro is defined in the Rust standard library (library/std/src/macros.rs) as follows [5][6]: macro_rules! eprintln { => { $crate::eprint!("\n") }; ($($arg:tt)) => {{ $crate::io::_eprint($crate::format_args_nl!($($arg))); }}; } It delegates to io::_eprint, which internally handles the writing process to the standard error stream [5][7][8]. The design choice to panic ensures that error messages intended for stderr are treated as critical, reflecting the assumption that failures in reporting errors should not be silently ignored [1][2].

Citations:


Make both C-ABI diagnostics non-panicking.

eprintln! panics when writing to io::stderr fails. These helpers are reachable from extern "C" functions, where a panic cannot unwind normally and can abort the process. Use writeln! on a locked stderr handle and discard any write error.

  • crates/perry-runtime/src/gc/roots/shadow_stack.rs:562-567: replace eprintln! with a fallible stderr write that ignores write errors.
  • crates/perry-runtime/src/gc/roots/temp_roots.rs:77-81: replace eprintln! with the same non-panicking write pattern.
📍 Affects 2 files
  • crates/perry-runtime/src/gc/roots/shadow_stack.rs#L562-L567 (this comment)
  • crates/perry-runtime/src/gc/roots/temp_roots.rs#L77-L81
🤖 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 `@crates/perry-runtime/src/gc/roots/shadow_stack.rs` around lines 562 - 567,
Make both C-ABI diagnostics non-panicking by replacing the eprintln! calls in
crates/perry-runtime/src/gc/roots/shadow_stack.rs:562-567 and
crates/perry-runtime/src/gc/roots/temp_roots.rs:77-81 with writeln! to a locked
stderr handle, explicitly discarding any write error. Preserve the existing
diagnostic messages and behavior otherwise.

Comment on lines +754 to +757
// This test verifies objects it holds only as native-stack locals are
// COLLECTED, so the native scan must not rescue them. `Disabled` says that
// unconditionally; the default `Auto` merely resolves that way today.
let _scan = ConservativeScanDisabledGuard::new();

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Reload child_header after the minor collection.

This guard enables relocation. The test dereferences child_header after collect_minor_trace, but it derived that pointer before collection. TemporaryCopyOnlyRootScanner::rust_bits(&[ptr_bits(child)]) can keep the child live, but it cannot rewrite the local child_header.

Reload the child through a rewritten root or the relocated parent slot before the mark assertion. Otherwise the test can inspect reclaimed from-space.

🤖 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 `@crates/perry-runtime/src/gc/tests/oldgen.rs` around lines 754 - 757, Update
the test around ConservativeScanDisabledGuard and collect_minor_trace to reload
child_header after collection from a rewritten root or the relocated parent slot
before performing the mark assertion. Do not dereference the pre-collection
child_header after relocation; preserve the existing root-scanning setup and
assertion behavior.

Source: Learnings

@proggeramlug

Copy link
Copy Markdown
Contributor Author

#7145 acceptance criterion verified end-to-end (Mac mini, darwin-arm64, --test-threads=1):

tree profile result
pristine main shadow_stack.rs dev thread caused non-unwinding panic. aborting.SIGABRT (signal 6), harness exit 101
this PR dev 1574 passed / 0 failed, harness exit 0
this PR release 1574 passed / 0 failed

That is the whole point of the issue: a profile of the runtime suite that could not complete, invisible to a release-only CI gate.

Production diff is provably empty for the scan-mode change. In a non-test build the old code already selected Auto:

-    #[cfg(test)]
-    { ConservativeStackScanMode::Full }
-    #[cfg(not(test))]
-    { ConservativeStackScanMode::Auto }
+    ConservativeStackScanMode::Auto

so the shipped runtime is unchanged by it. The only production-reachable change in this PR is the two #[cold] diagnostic fns in never-taken error branches — which is the expectation for gc-ratchet's gated columns (the job's relevance filter matches crates/, so it does measure rather than skip).

@proggeramlug
proggeramlug merged commit 2003a89 into main Aug 1, 2026
30 of 37 checks passed
@proggeramlug
proggeramlug deleted the fix/7146-gc-test-precise-rooting branch August 1, 2026 04:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

gc: debug_assert!(false) inside extern "C" js_shadow_frame_pop SIGABRTs the entire dev-profile runtime test suite (invisible to release-only CI)

1 participant