Skip to content

[rust-compiler] Dedup async-reassignment immutability diagnostic#36496

Merged
poteto merged 2 commits into
facebook:pr-36173from
poteto:rust-compiler-async-immutability-dedup
May 20, 2026
Merged

[rust-compiler] Dedup async-reassignment immutability diagnostic#36496
poteto merged 2 commits into
facebook:pr-36173from
poteto:rust-compiler-async-immutability-dedup

Conversation

@poteto
Copy link
Copy Markdown
Collaborator

@poteto poteto commented May 20, 2026

Bug

validate_locals_not_reassigned_after_render emitted the same
Cannot reassign variable in async function diagnostic once per
async function body that reassigned the same outer let. The TS
reference emits it exactly once per offending variable.

Reproducer (both compilers, panicThreshold: 'none'):

function Component() {
	let value: number | undefined;
	const a = async () => { value = 1; };
	const b = async () => { value = 2; };
	return <div>{[a, b]}</div>;
}

Before: TS emits 1 error, Rust emits 2.
After: both emit 1.

Root cause

crates/react_compiler_validation/src/validate_locals_not_reassigned_after_render.rs is a faithful port of the TS pass, except the inner-function async-reassignment branch is missing the early return. The TS reference does env.recordError(...); return null;. The Rust code recorded the diagnostic then fell through into the next loop iteration, so the next async function body got the same emit path.

There was even a // Return null (don't propagate further) — matches TS behavior comment in place of the missing return.

Fix

Replace the comment with the actual return None;. One-line change.

How I found it

TS-vs-Rust differential sweep over a large real-world TSX corpus (~59k files), comparing the in-repo TS compiler vs the Rust port. Among 14k+ error events, this duplicate emission was the most concentrated divergence cluster.

Verification

  • New fixture error.invalid-reassign-local-variable-in-multiple-async-callbacks.tsx: yarn snap and yarn snap --rust both pass (1 error each).
  • The existing error.invalid-reassign-local-variable-in-async-callback.js fixture still passes.
  • Full yarn snap --rust: 1782/1794 (+1 vs main, no regression in the 12 known frontier failures).

Commit order

  1. Adds the fixture with the broken-Rust baseline (Found 2 errors). yarn snap --rust passes against it, yarn snap fails — captures the bug.
  2. Applies the one-line fix and flips the baseline to Found 1 error. Both yarn snap and yarn snap --rust pass.

When multiple async functions reassign the same outer `let`, the Rust
port emits the same `Cannot reassign variable in async function`
diagnostic once per reassigning function body. The reference TS
compiler emits it exactly once per offending variable, regardless of
how many async functions reassign it.

This fixture snapshots the broken behavior with two async callbacks:
the baseline records 2 errors. `yarn snap --rust` passes against
this broken baseline; `yarn snap` (TS) fails because TS only emits 1.
The next commit fixes the over-emission and flips the baseline.
@meta-cla meta-cla Bot added the CLA Signed label May 20, 2026
@github-actions github-actions Bot added the React Core Team Opened by a member of the React Core Team label May 20, 2026
@poteto poteto force-pushed the rust-compiler-async-immutability-dedup branch from e5b0564 to 86519cb Compare May 20, 2026 18:53
`validate_locals_not_reassigned_after_render` had a missing early
return in the inner-function async-reassignment branch. The Rust port
recorded the diagnostic, but where the TS reference does `return null;`
right after `env.recordError(...)`, the Rust code fell through into
the next loop iteration, re-emitting the same diagnostic for every
async function body that reassigned the same outer let.

Add the `return None;` the comment already promised. Found by a
TS-vs-Rust differential sweep over a large real-world TSX corpus
where Rust over-emitted this diagnostic in proportion to the number
of reassigning callback sites (e.g. 4 in usePlanEditorState; TS
emits 1).

The fixture baseline flips from 2 errors to 1, matching TS. Full
`yarn snap --rust` is unchanged otherwise (1782/1794 passing, +1
versus before).
@poteto poteto force-pushed the rust-compiler-async-immutability-dedup branch from 86519cb to 5f452f0 Compare May 20, 2026 19:00
@poteto poteto merged commit cfcbe9d into facebook:pr-36173 May 20, 2026
15 of 20 checks passed
@poteto poteto deleted the rust-compiler-async-immutability-dedup branch May 20, 2026 21:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed React Core Team Opened by a member of the React Core Team

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant