Skip to content

typed_feedback integration suite is order-dependent: 5 tests fail in-suite, pass alone #7490

Closed

Description

@proggeramlug

Symptom

cargo test -p perry-codegen --test typed_feedback fails 5 of 15 when the suite runs as one process, and every one of the 5 passes when run alone:

  • typed_feedback_guards_direct_class_field_specialization ("class-field raw fallback must be coerced before the numeric merge", typed_feedback.rs:369)
  • typed_feedback_guards_direct_class_method_specialization
  • typed_feedback_instruments_property_and_method_boundaries
  • typed_feedback_marks_numeric_array_literals
  • typed_feedback_trace_dump_runs_before_entry_return

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-codegen without --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.

Activity

  1. added a commit that references this issue on Aug 6, 2026
  2. proggeramlug commented on Aug 6, 2026

    @proggeramlug
    ContributorAuthor

    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 holding ENV_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).

  3. added a commit that references this issue on Aug 6, 2026
  4. proggeramlug commented on Aug 6, 2026

    @proggeramlug
    ContributorAuthor

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugConfirmed defect or regression

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions