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
{{ message }}
Repository navigation
typed_feedback integration suite is order-dependent: 5 tests fail in-suite, pass alone #7490
Deterministic under --test-threads=1 (so it is intra-process order dependence, not thread races); under default parallelism the failing set additionally wobbles (other tests like full_outline_ic_* join/leave the set run to run).
Verified identical on main @ c9e11ea, at c9e11ea~1, and on an unrelated runtime-only branch — long-standing, not introduced by any of this week's merges. Host: arm64 macOS, debug profile, coherent workspace target dir.
Why nobody sees it
Integration suites under crates/*/tests/*.rs do not run per-PR (nightly/tag only — the CLAUDE.md pitfall). And a plain cargo test -p perry-runtime -p perry-codegenwithout --no-fail-fast stops at the first failing target, so a log tail looks like a complete run with one unrelated failure.
Likely shape
Process-global codegen state (typed-feedback site registry / IC epoch / class-keys tables) leaks between in-process compiles, shifting emitted IR for whichever test compiles later. The fix is per-test isolation of that state (reset hook or unique-per-test module keying), not loosening the asserts.
Acceptance
cargo test -p perry-codegen --test typed_feedback green as a full suite, both default and --test-threads=1, ×3 consecutive runs.
Diagnosed and fixed in #7492 — but the cause is not what the issue supposed, and one of its premises is wrong, so recording that here.
There is no leaking codegen state. Both process-global gates these tests touch are already correct for the real compiler: PERRY_TYPED_FEEDBACK is read live at each call site (never cached), and PERRY_FULL_OUTLINE_IC's decision is a thread_local! set once per compile_module — deliberately not a process-global OnceLock, precisely so a multi-module build cannot pin the first module's decision.
Two of the five also fail when run alone, contrary to "every one of the 5 passes when run alone":
typed_feedback_guards_direct_class_field_specialization — asserted the numeric coercion appeared in the textual window between the class_field_get_number.fallback and .merge labels. fix(codegen): class-typed nullish field read throws TypeError (#7153) #7430 split that arm: .fallback now holds only the nullish-receiver check, and the by-name load plus coercion moved into .fallback_lookup, a block rendered after.merge. The window could never match again.
typed_feedback_trace_dump_runs_before_entry_return — cut main's body at the literal header define i32 @main() {, which stopped matching when feat(gc): make native roots (statepoints) the default #7370 made native roots the default and every emitted function gained "frame-pointer"="non-leaf".
The other three are collateral. The first of those two panics while holdingENV_LOCK; the unwind poisons the mutex, and every later ENV_LOCK.lock().unwrap() in the binary then dies with PoisonError whatever its own subject is doing. That is the entire "order dependence" — and it explains the reported wobble exactly, since which tests reach the lock after the poisoning is a scheduling fact. Run the three victims as a group without the poisoner and they pass.
Fix in #7492: env_lock() recovers a poisoned guard (sound — each test declares its EnvVarGuard after the lock guard, so reverse drop order restores the env var during unwind before the mutex is released); both drifted assertions are re-pointed at the current IR and made stronger, not looser; and a sabotage test plants the exact #7490 shape, asserts it really poisoned the mutex, and demands the accessor still hands out a guard — it goes 9-of-16 red against the pre-fix .lock().unwrap(), so a green run is evidence rather than decoration.
Acceptance met: 16/16 green, three consecutive runs at default parallelism and three at --test-threads=1, and every test also passes run alone.
The other suites named in the sweep are not this bug — every one of their failures reproduces run alone. Split by actual cause into #7493 (the #7370 native-roots default; NativeRootsPin::shadow() is #[cfg(test)] and unreachable from tests/*.rs, and no single global setting satisfies the set because some suites assert the other lowering) and #7494 (three lowering-independent failures needing a call on whether a proof regressed).
Correction to the sweep numbers in my comment above: my first full-suite sweep was piped through head, which silently truncated the target list and hid two suites (temp_root_operand_temporaries, typed_shape_descriptors). Re-measured untruncated, both lowerings — #7493 and #7494 now carry the corrected table, and the conclusion is unchanged: none of those failures is order-dependent, every one reproduces run alone, and this issue's cause remains the poisoned ENV_LOCK.
Activity
docs(changelog): add fragment for #7490 typed_feedback test isolation
proggeramlug commentedon Aug 6, 2026
Diagnosed and fixed in #7492 — but the cause is not what the issue supposed, and one of its premises is wrong, so recording that here.
There is no leaking codegen state. Both process-global gates these tests touch are already correct for the real compiler:
PERRY_TYPED_FEEDBACKis read live at each call site (never cached), andPERRY_FULL_OUTLINE_IC's decision is athread_local!set once percompile_module— deliberately not a process-globalOnceLock, precisely so a multi-module build cannot pin the first module's decision.Two of the five also fail when run alone, contrary to "every one of the 5 passes when run alone":
typed_feedback_guards_direct_class_field_specialization— asserted the numeric coercion appeared in the textual window between theclass_field_get_number.fallbackand.mergelabels. fix(codegen): class-typed nullish field read throws TypeError (#7153) #7430 split that arm:.fallbacknow holds only the nullish-receiver check, and the by-name load plus coercion moved into.fallback_lookup, a block rendered after.merge. The window could never match again.typed_feedback_trace_dump_runs_before_entry_return— cutmain's body at the literal headerdefine i32 @main() {, which stopped matching when feat(gc): make native roots (statepoints) the default #7370 made native roots the default and every emitted function gained"frame-pointer"="non-leaf".The other three are collateral. The first of those two panics while holding
ENV_LOCK; the unwind poisons the mutex, and every laterENV_LOCK.lock().unwrap()in the binary then dies withPoisonErrorwhatever its own subject is doing. That is the entire "order dependence" — and it explains the reported wobble exactly, since which tests reach the lock after the poisoning is a scheduling fact. Run the three victims as a group without the poisoner and they pass.Fix in #7492:
env_lock()recovers a poisoned guard (sound — each test declares itsEnvVarGuardafter the lock guard, so reverse drop order restores the env var during unwind before the mutex is released); both drifted assertions are re-pointed at the current IR and made stronger, not looser; and a sabotage test plants the exact #7490 shape, asserts it really poisoned the mutex, and demands the accessor still hands out a guard — it goes 9-of-16 red against the pre-fix.lock().unwrap(), so a green run is evidence rather than decoration.Acceptance met: 16/16 green, three consecutive runs at default parallelism and three at
--test-threads=1, and every test also passes run alone.The other suites named in the sweep are not this bug — every one of their failures reproduces run alone. Split by actual cause into #7493 (the #7370 native-roots default;
NativeRootsPin::shadow()is#[cfg(test)]and unreachable fromtests/*.rs, and no single global setting satisfies the set because some suites assert the other lowering) and #7494 (three lowering-independent failures needing a call on whether a proof regressed).docs(changelog): point the #7490 fragment at the split follow-ups wit…
proggeramlug commentedon Aug 6, 2026
Correction to the sweep numbers in my comment above: my first full-suite sweep was piped through
head, which silently truncated the target list and hid two suites (temp_root_operand_temporaries,typed_shape_descriptors). Re-measured untruncated, both lowerings — #7493 and #7494 now carry the corrected table, and the conclusion is unchanged: none of those failures is order-dependent, every one reproduces run alone, and this issue's cause remains the poisonedENV_LOCK.fix(test): isolate typed_feedback failures — a poisoned ENV_LOCK turn…
fix(test): make the root-lowering pin reachable from integration suit…
fix(test): make the root-lowering pin reachable from integration suit…