You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PERRY_GC_FORCE_EVACUATE=1 has no effect on a test that drives collection with an explicit gc() call, and such a test additionally cannot expose raw-local rooting bugs. Both properties are structural, verified on origin/main. A test written that way looks like GC stress coverage and is not.
It runs a FULL mark-sweep, deliberately not the minor path (the in-code comment explains why: dead large/tenured objects must be reclaimed to match --expose-gc semantics). But PERRY_GC_FORCE_EVACUATE is read only inside the minor cycle (gc/mod.rs: let force_evacuation = gc_force_evacuate_enabled(); within GcCollectionKind::Minor, plus gc/policy.rs / gc/oldgen.rs evacuation paths). A full mark-sweep never evacuates, so nothing moves, so a stale pointer is trivially valid.
Independently measured in #6942: across 10 collections in a 12M-allocation program, PERRY_GC_DIAG=1 printed zero[gc-evac-policy] / [gc-copy-minor] lines.
Blast radius (measured, not assumed)
9 gap files call gc() — test_gc_array_layout_mask, test_gc_aggressive_forced, test_gc_class_object_forced, test_issue_1425_gc_unsafe_zones, test_issue_1830_gc_in_catch_after_deep_throw, test_issue_2656_weakref_finalization_gc, test_issue_4977_gc_toplevel_locals, test_issue_callable_with_properties, test_memory_class_object_churn. Any evacuation/rooting claim resting on these under FORCE_EVACUATE is unsupported.
The representation-selection corpus does NOT call gc() (verified: 0 occurrences across all test_gap_repsel_* / test_gap_specabi_* files). Those rely on allocation pressure, which can drive automatic minors — where FORCE_EVACUATE does apply and the conservative scan is not forced. Whether their churn actually crosses the threshold is unverified and is the open question; gc: no reachable configuration exercises an evacuating minor with unpinned runtime locals — the #6655/#6935 bug class is untestable #6942 notes a common "20000 short-lived objects" prelude triggers no GC at all because non-escaping allocations are optimized away.
Why it matters
"Byte-exact under PERRY_GC_FORCE_EVACUATE=1" has been used as the standard validation ritual across a long series of GC and representation PRs. For gc()-driven tests that sentence asserts nothing about evacuation or rooting.
Suggested fixes
Make the trap loud: if PERRY_GC_FORCE_EVACUATE=1 is set and a manual gc() runs, emit a diagnostic (or have PERRY_GC_DIAG state plainly that force-evacuate was ignored on this path).
Require liveness proof in GC tests: assert via PERRY_GC_DIAG that a collection (and, for evacuating arms, moved_objects > 0) actually occurred, rather than trusting the env var.
Related: #6942 (stress arms measured inert, with three proposed harness routes), #6910 (mark/rewrite parity; its tests correctly pin ConservativeStackScanMode::Disabled — the pattern to copy).
Summary
PERRY_GC_FORCE_EVACUATE=1has no effect on a test that drives collection with an explicitgc()call, and such a test additionally cannot expose raw-local rooting bugs. Both properties are structural, verified onorigin/main. A test written that way looks like GC stress coverage and is not.Mechanism (verified on
origin/main)js_gc_collect→manual_gc_collect_now(crates/perry-runtime/src/gc/policy.rs):force_full_scan()pins raw locals. The conservative native-stack scan is forced on, so a raw NaN-boxed word in a Rust local or an unrooted slot is treated as a root. That is exactly the class of bug the rooting work (runtime: audit dynamic_arith operand rooting — raw NaN-boxed operands held across GC-capable to_numeric coercions (pre-existing, file-wide) #6655, runtime: audit ToPropertyKey receiver/value rooting — raw receiver and stored value held across GC-capable key coercions #6935/fix(runtime): root receivers and stored values across GC-capable property-key coercions (#6935) #6941) targets — agc()-driven test cannot observe it. Note production resolvesAuto → SkipDisabled(gc/roots.rs), so this is a test-only safety net: a greengc()-driven test can be a production-mode lie.--expose-gcsemantics). ButPERRY_GC_FORCE_EVACUATEis read only inside the minor cycle (gc/mod.rs:let force_evacuation = gc_force_evacuate_enabled();withinGcCollectionKind::Minor, plusgc/policy.rs/gc/oldgen.rsevacuation paths). A full mark-sweep never evacuates, so nothing moves, so a stale pointer is trivially valid.Independently measured in #6942: across 10 collections in a 12M-allocation program,
PERRY_GC_DIAG=1printed zero[gc-evac-policy]/[gc-copy-minor]lines.Blast radius (measured, not assumed)
gc()—test_gc_array_layout_mask,test_gc_aggressive_forced,test_gc_class_object_forced,test_issue_1425_gc_unsafe_zones,test_issue_1830_gc_in_catch_after_deep_throw,test_issue_2656_weakref_finalization_gc,test_issue_4977_gc_toplevel_locals,test_issue_callable_with_properties,test_memory_class_object_churn. Any evacuation/rooting claim resting on these underFORCE_EVACUATEis unsupported.gc()(verified: 0 occurrences across alltest_gap_repsel_*/test_gap_specabi_*files). Those rely on allocation pressure, which can drive automatic minors — whereFORCE_EVACUATEdoes apply and the conservative scan is not forced. Whether their churn actually crosses the threshold is unverified and is the open question; gc: no reachable configuration exercises an evacuating minor with unpinned runtime locals — the #6655/#6935 bug class is untestable #6942 notes a common "20000 short-lived objects" prelude triggers no GC at all because non-escaping allocations are optimized away.Why it matters
"Byte-exact under
PERRY_GC_FORCE_EVACUATE=1" has been used as the standard validation ritual across a long series of GC and representation PRs. Forgc()-driven tests that sentence asserts nothing about evacuation or rooting.Suggested fixes
PERRY_GC_FORCE_EVACUATE=1is set and a manualgc()runs, emit a diagnostic (or havePERRY_GC_DIAGstate plainly that force-evacuate was ignored on this path).perry/gc'sminor()evacuates, but per gc: no reachable configuration exercises an evacuating minor with unpinned runtime locals — the #6655/#6935 bug class is untestable #6942 it engagesforce_full_scan()and so re-pins raw locals; a variant without that pinning is what rooting tests actually need).PERRY_GC_DIAGthat a collection (and, for evacuating arms,moved_objects > 0) actually occurred, rather than trusting the env var.Related: #6942 (stress arms measured inert, with three proposed harness routes), #6910 (mark/rewrite parity; its tests correctly pin
ConservativeStackScanMode::Disabled— the pattern to copy).