Skip to content

fix(test): isolate typed_feedback failures — a poisoned ENV_LOCK turned one failure into five (#7490) - #7492

Merged
proggeramlug merged 4 commits into
mainfrom
fix/7490-typed-feedback-test-isolation
Aug 6, 2026
Merged

fix(test): isolate typed_feedback failures — a poisoned ENV_LOCK turned one failure into five (#7490)#7492
proggeramlug merged 4 commits into
mainfrom
fix/7490-typed-feedback-test-isolation

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Fixes #7490.

What the order dependence actually was

The issue's premise — process-global codegen state leaking between in-process compiles — does not hold. There is no leaking codegen state, and two of the five reported failures also fail when run alone, contrary to the issue text:

test verdict
typed_feedback_guards_direct_class_field_specialization genuine, fails alone (stale assertion)
typed_feedback_trace_dump_runs_before_entry_return genuine, fails alone (stale assertion)
typed_feedback_guards_direct_class_method_specialization PoisonError cascade only
typed_feedback_instruments_property_and_method_boundaries PoisonError cascade only
typed_feedback_marks_numeric_array_literals PoisonError cascade only

The three cascade victims pass as a group when the poisoner is not in the run.

The two genuine assertion drifts

typed_feedback_guards_direct_class_field_specialization asserted that the numeric coercion appeared in the textual window between the class_field_get_number.fallback and .merge labels. #7430 split that arm — .fallback now carries only the nullish-receiver check and branches to .throw_nullish / .fallback_lookup, and the by-name load plus coercion moved into .fallback_lookup, which is rendered after .merge:

class_field_get_number.fallback.7:            ; nullish check only
  br i1 %r115, label %...throw_nullish.11, label %...fallback_lookup.12

class_field_get_number.merge.8:
  %r119 = phi double [ %r112, %...fast.6 ], [ %r118, %...fallback_lookup.12 ]
  ...
class_field_get_number.fallback_lookup.12:    ; the coercion lives here, AFTER merge
  %r117 = call double @js_object_get_field_by_name_f64(i64 %r62, i64 %r66)
  %r118 = call double @js_number_coerce(double %r117)

The window could never match again. The codegen is correct; the assertion was stale.

typed_feedback_trace_dump_runs_before_entry_return cut main's body at the literal header define i32 @main() {. Since #7370 made native roots the default, every emitted function carries "frame-pointer"="non-leaf" (and a stack-map-requesting function adds gc "statepoint-example"), so the header never matches and the .expect fires.

How two failures became five, wobbling

typed_feedback_guards_direct_class_field_specialization panics while holding ENV_LOCK. The unwind poisons the mutex for the rest of the process, so every later ENV_LOCK.lock().unwrap() dies with PoisonError regardless of whether its own subject is healthy. Under --test-threads=1 the alphabetically-early poisoner deterministically takes three healthy tests down; under default parallelism the victim set shifts with the scheduler — which is exactly the reported "the failing set wobbles, and full_outline_ic_* join and leave it".

The fix

Three parts, none of which loosens an assertion.

  1. Isolation. env_lock() recovers a poisoned guard (unwrap_or_else(PoisonError::into_inner)). This is sound rather than a shrug: each test declares its EnvVarGuard after the lock guard, so Rust's reverse drop order restores the env var during unwind before the mutex is released — the protected state is already consistent at poison time. One test's failure must fail that test alone.

  2. Re-point the two drifted assertions — stronger, not looser. The class-field test now proves the data flow that the positional window only stood in for, end to end: .fallback_lookup records the fallback call, loads by name and coerces; its terminator branches to the numeric merge; and the merge phi's fallback incoming is the coerced register. A new block_body() helper reads a named block by stable label prefix, so per-function numeric label suffixes and block ordering stop being load-bearing. entry_fn_body matches the exact signature and cuts at that line's opening brace, so unrelated attribute changes can no longer fail the test.

  3. A sabotage test for the isolation itself. env_lock_is_poison_tolerant_so_one_failure_cannot_cascade plants the exact typed_feedback integration suite is order-dependent: 5 tests fail in-suite, pass alone #7490 shape — an unwind out of a lock-holding test — asserts it really did poison the mutex (the gate asserts its subject was live), then demands the accessor still hands out a usable guard. Against the pre-fix .lock().unwrap() it fails and reproduces the cascade: 9 of 16 red. Because it sorts first, every other test in the binary then runs under a genuinely poisoned lock, so the tolerance is exercised suite-wide rather than in one isolated case.

Is this a production bug too?

No. Both process-global gates these tests touch are already right for the real compiler:

  • PERRY_TYPED_FEEDBACK is read live at each call site (crates/perry-codegen/src/expr/typed_feedback.rs:252), never cached.
  • PERRY_FULL_OUTLINE_IC's decision is a thread_local! set once per compile_module (crates/perry-codegen/src/codegen/helpers.rs:371), deliberately not a process-global OnceLock, precisely so a multi-module build cannot pin the first module's decision.

The defect was confined to the test binary's failure isolation.

Validation

  • cargo test -p perry-codegen --test typed_feedback16 passed, 0 failed, three consecutive runs at default parallelism and three at --test-threads=1.
  • Every one of the 16 tests also passes run alone (scripted over the full --list), so no test depends on another having run.
  • cargo fmt --all -- --check clean.
  • Sabotage check: reverting only env_lock() to .lock().unwrap() turns the suite red at 9/16 and restores the cascade — so the isolation is doing the work.

Other suites checked (different root cause — not fixed here)

The sweep in the issue also named several suites. Every one of their failures reproduces when the test is run alone, so none is order-dependent, and this fix heals none of them. Full --no-fail-fast -- --test-threads=1 sweeps of -p perry-codegen --tests, default vs. the lowering override:

suite                                   default        PERRY_RS4GC=0
shadow_slot_hygiene                    0 / 12 pass     11 / 12 pass
scalar_replaced_slot_roots             2 / 11 pass      5 / 11 pass
temp_root_operand_temporaries         12 / 19 pass     13 / 19 pass
temp_root_argument_temporaries         3 /  7 pass      3 /  7 pass
native_proof_regressions             249 /253 pass    236 /253 pass   ← INVERSE
native_proof_buffer_views             28 / 30 pass     27 / 30 pass   ← inverse
typed_shape_descriptors               17 / 18 pass     17 / 18 pass
typed_feedback (this PR)              16 / 16 pass     16 / 16 pass

They split into two causes, filed granularly rather than folded in:

CI

cargo-test and api-docs-drift pass. The red jobs are pre-existing and reproduce identically on unrelated PR #7468: gc-ratchet, gc-root-dominance, security-audit, Warnings. lint fails only on its "Public benchmark evidence freshness" step (stale public baseline — run_public_baseline.sh needs a regen); its fingerprints cover Cargo.toml and benchmarks/** only, and this PR touches neither.

Summary by CodeRabbit

  • Bug Fixes

    • Improved isolation for environment-sensitive tests so one failing test no longer causes cascading failures.
    • Updated validation for generated entry-function attributes, class-field specialization, and fallback lookup behavior.
    • Replaced fragile output-based checks with more reliable generated-behavior validation.
  • Documentation

    • Added a changelog entry describing the test-isolation fixes, updated assertions, and validation results.
  • Chores

    • Updated the workspace version to 0.5.1285.

…ed one failure into five (#7490)

`cargo test -p perry-codegen --test typed_feedback` reported five failures as
a suite and a wobbling set under default parallelism. The suspected cause was
process-global codegen state leaking between in-process compiles. It was not.

Two assertions had genuinely drifted from intentional codegen changes, and
both fail when run ALONE (contrary to the issue's premise):

* `typed_feedback_guards_direct_class_field_specialization` matched the
  numeric coercion in the textual window between the
  `class_field_get_number.fallback` and `.merge` labels. #7430 split that arm:
  `.fallback` now holds only the nullish-receiver check, and the by-name load
  plus coercion moved to `.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() {`. Since #7370 made native roots the
  default every emitted function carries `"frame-pointer"="non-leaf"`, so that
  header never matches.

The first of those panics while holding `ENV_LOCK`, which poisons the mutex
for the rest of the process; every later `ENV_LOCK.lock().unwrap()` then dies
with `PoisonError` regardless of its own subject. That is the whole of the
"order dependence": under `--test-threads=1` the alphabetically-early poisoner
takes three healthy tests down with it, and under default parallelism the
victim set shifts with the scheduler.

Fix, in three parts:

* `env_lock()` recovers a poisoned guard. Sound because each test declares its
  `EnvVarGuard` after the lock guard, so the env var is restored during unwind
  before the mutex is released — the protected state is already consistent at
  poison time. One test's failure must fail that test alone.
* Both drifted assertions are re-pointed at the current IR and made STRONGER,
  not looser. The class-field one now proves the data flow the positional
  window stood in for, end to end: `.fallback_lookup` records the fallback,
  loads by name and coerces; its terminator branches to the numeric merge; and
  the merge phi's fallback incoming IS the coerced register. `entry_fn_body`
  matches the exact signature and cuts at that line's opening brace, so
  unrelated attribute changes can no longer fail the test.
* A sabotage test plants the exact #7490 shape — an unwind out of a
  lock-holding test — asserts it really poisoned the mutex, and demands the
  accessor still hands out a guard. It fails against the pre-fix
  `.lock().unwrap()` (9 of 16 red), so a green run is evidence, not decoration.

No production codegen state leaks between compiles: `PERRY_TYPED_FEEDBACK` is
read live at each call site and `PERRY_FULL_OUTLINE_IC`'s decision is a
thread-local set once per `compile_module` — both already correct.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c1987b8d-f894-4d62-99c1-bb00844cedb3

📥 Commits

Reviewing files that changed from the base of the PR and between a6ed0cf and 61f9c95.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • CLAUDE.md
  • Cargo.toml

📝 Walkthrough

Walkthrough

The typed-feedback integration tests now recover from poisoned environment locks, parse generated entry functions with attributes, and validate class-field fallback IR through block-level assertions. The changelog and workspace version documentation are updated.

Changes

Typed-feedback test robustness

Layer / File(s) Summary
Poison-tolerant environment locking
crates/perry-codegen/tests/typed_feedback.rs
env_lock recovers poisoned mutex guards. Environment-sensitive tests use this accessor. A sabotage test verifies later acquisition.
Generated IR parsing and validation
crates/perry-codegen/tests/typed_feedback.rs
Entry-function parsing accepts optional attributes. Block-level helpers and assertions verify fallback lookup, numeric coercion, branching, and merge-phi inputs.
Release documentation
changelog.d/7492-typed-feedback-test-isolation.md, Cargo.toml, CLAUDE.md
The changelog records the test-isolation changes. The workspace and documented versions advance to 0.5.1285.

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

Possibly related PRs

  • PerryTS/perry#6840 — Both changes replace fragile IR assertions with function- or block-level validation.
  • PerryTS/perry#6885 — Both changes update shared test environment-lock handling.
  • PerryTS/perry#6963 — Both changes address poison-tolerant locking in typed-feedback tests.

Suggested labels: bug, tooling

Suggested reviewers: jdalton

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the test failure-isolation fix and the poisoned ENV_LOCK cascade described by the changes.
Description check ✅ Passed The description explains the cause, changes, linked issue, validation commands, test results, and CI status in sufficient detail.
Linked Issues check ✅ Passed The PR addresses issue #7490 by fixing stale assertions, recovering poisoned ENV_LOCK guards, and verifying reliable suite execution.
Out of Scope Changes check ✅ Passed The changes remain within the issue scope and include only typed_feedback test fixes, isolation coverage, and related changelog documentation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/7490-typed-feedback-test-isolation

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.

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.

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

1 participant