Skip to content

fix(react_compiler): propagate codegen invariants instead of emitting empty bodies#24128

Merged
Boshen merged 1 commit into
mainfrom
fix/rc-invariant-miscompiles
Jul 4, 2026
Merged

fix(react_compiler): propagate codegen invariants instead of emitting empty bodies#24128
Boshen merged 1 commit into
mainfrom
fix/rc-invariant-miscompiles

Conversation

@Boshen

@Boshen Boshen commented Jul 3, 2026

Copy link
Copy Markdown
Member

Summary

This batch targets 5 fixtures that compiled without error in oxc but throw an internal Invariant upstream — cases where oxc's output was silently wrong (worse than upstream's loud failure).

Root cause (shared by 4 of the 5)

codegen_function wrapped ox_codegen_reactive_function in a scaffolding shim that fell back to an empty function body on ANY codegen error. That turned genuine "this input cannot be compiled" invariants into silent miscompiles: the whole body was replaced with {} and applied to the program with no diagnostic.

The shim still had a legitimate job, though — a couple of codegen sub-emitters aren't ported yet (Destructuring reassignment targets, hook-guard wrapping), and upstream does compile those. Blindly propagating every error would regress ~26 fixtures that currently emit an (imperfect) empty body but would now spuriously error.

Fix: split the two error classes. A new CompilerError::unimplemented flag (set via unimplemented_err) marks the genuinely-unported sub-emitters; the shim keeps falling back to an empty body only for those. Every other codegen error now propagates as a diagnostic — matching upstream's Invariant — instead of emitting wrong code. Net diff: only the 5 genuine-invariant fixtures change; the ~26 not-yet-ported fixtures keep their prior output (no regressions).

Per-fixture status

Fixture Status Notes
error.bug-invariant-unnamed-temporary fixed → now-errors matches upstream
error.bug-invariant-codegen-methodcall fixed → now-errors matches upstream
error.call-args-destructuring-asignment-complex fixed → now-errors matches upstream
error.todo-nested-method-calls-lower-property-load-into-temporary fixed → now-errors matches upstream
error.bug-invariant-local-or-context-references fixed → correct output oxc handles a case upstream crashes on

Bonus: error.todo-kitchensink also flipped from a silent empty body (masked behind 8 accumulated warnings) to the single upstream invariant Expected a variable declaration.

Details

error.bug-invariant-unnamed-temporaryrenderer={(...props) => <span {...props}>{displayValue}</span>}
Was silently compiled with the renderer arrow dropped to function _temp() {} (empty body). The codegen path hit Expected temporaries to be promoted to named identifiers in an earlier pass (an unnamed identifier reaching convert_identifier), which the shim swallowed. Now the error propagates and the function is left unchanged — identical to upstream's Invariant.

error.bug-invariant-codegen-methodcallMath.floor(diff.bar())
Was silently compiled to const YearsAndMonthsSince = () => {}; (entire body dropped). diff.bar's property had been promoted/lowered into an Identifier so codegen's MethodCall::property must be an unpromoted + unmemoized MemberExpression invariant fired (Got: 'Identifier') and was swallowed. Now propagates — matches upstream.

error.todo-nested-method-calls-lower-property-load-into-temporaryMath.max(2, items.push(5), ...other)
Same MethodCall::property invariant as above (items.push property lowered into a temporary), same silent empty-body drop. Now propagates — matches upstream.

error.call-args-destructuring-asignment-complexx.foo(([[x]] = makeObject()))
Was silently compiled to function Component() {} (body dropped). The complex destructuring assignment produced a const-declaration instruction that is also referenced as an expression, tripping codegen's Const declaration cannot be referenced as an expression invariant (this is Const), which was swallowed. Now propagates — matches upstream.

error.bug-invariant-local-or-context-referencescatch (err) { setState(_prev => ({..., error: err})); }
Upstream throws Expected all references to a variable to be consistently local or context references (it mis-classifies a catch binding captured by a lambda as a context variable even though err is never reassigned). oxc already lowers this correctly: it renames the catch param and aliases it — catch (t0) { const err = t0; setState(...) } — so err stays a plain read-only local and no inconsistency arises. Verified semantically equivalent to the source on both the try-success and catch paths (identical setState call sequences, including error propagation). No code change needed — oxc legitimately handles a case upstream crashes on.

Notes / still-open

  • The 4 now-erroring fixtures are cases upstream also fails on (its own passes emit these invariants). Producing correct memoized output for them would require fixing the earlier promotion/SSA passes so temporaries are named/unpromoted before codegen — upstream doesn't do this either, so erroring cleanly is the correct, achievable resolution here.
  • todo-kitchensink's oxc message is Invariant: Expected a variable declaration (upstream adds a Got ExpressionStatement. description); the reason matches, the description is a cosmetic gap.

@codspeed-hq

codspeed-hq Bot commented Jul 3, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

✅ 10 untouched benchmarks
⏩ 61 skipped benchmarks1


Comparing fix/rc-invariant-miscompiles (162be90) with main (e0e652c)

Open in CodSpeed

Footnotes

  1. 61 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports.

… empty bodies

`codegen_function` wrapped `ox_codegen_reactive_function` in a scaffolding shim
that fell back to an empty function body on ANY codegen error. This turned
genuine "this input cannot be compiled" invariants — the ones upstream throws
loudly — into SILENT miscompiles: the entire function body was replaced with
`{}` and applied to the program with no diagnostic.

Distinguish the two error classes instead. Not-yet-ported sub-emitters
(destructuring reassignment targets, hook-guard wrapping) now raise an
`unimplemented`-marked error and keep falling back to an empty body, preserving
current behavior for the constructs upstream compiles and oxc does not yet
emit. Every other codegen error (unnamed temporary, MethodCall property must be
an unpromoted MemberExpression, const/let referenced as an expression, expected
a variable declaration, ...) now propagates as a diagnostic, matching upstream's
Invariant failure rather than emitting wrong code.
@Boshen
Boshen force-pushed the fix/rc-invariant-miscompiles branch from 5ee5953 to 162be90 Compare July 4, 2026 02:10
@Boshen
Boshen marked this pull request as ready for review July 4, 2026 02:31
@Boshen
Boshen merged commit b82c394 into main Jul 4, 2026
30 checks passed
@Boshen
Boshen deleted the fix/rc-invariant-miscompiles branch July 4, 2026 02:32
Boshen added a commit that referenced this pull request Jul 6, 2026
### 🚀 Features

- 260425f semantic/examples: Include unresolved references (#24214)
(camc314)
- 2d9b0b3 minifier: Fold boolean-literal ternary branches in value
contexts (#24110) (Dunqing)
- 61fbf10 ast: Implement `ReplaceWith` on all AST types (#24013)
(overlookmotel)
- 7db7a29 allocator: Add `ReplaceWith` trait (#24012) (overlookmotel)
- 4eb074e mangler: Add `reserved` option for names that must not be
mangled (#24041) (Dunqing)
- 2e62012 data_structures: Add `StringExt` trait (#24006)
(overlookmotel)
- 60e7160 minifier: Drop side-effect-free IIFEs whose result is unused
(#23967) (Dunqing)
- 26dd9e2 ast: Add method to widen inherited enum ref to parent ref
(#23961) (overlookmotel)

### 🐛 Bug Fixes

- e8b50ee transformer: Clean up semantics for stripped TypeScript syntax
(#24180) (camc314)
- d966d0b react_compiler: Remove clippy allows (#24168) (Boshen)
- 854ef8d react_compiler: Compile generic functions instead of
over-bailing on type-param hoisting (#24158) (Boshen)
- 093586c react_compiler: Align memoization cache-slot allocation with
Babel (#24157) (Boshen)
- 09c8f59 react_compiler: Normalize snapshot fixture paths (#24142)
(camc314)
- f13df97 react_compiler: Drop stray empty statement from catch bindings
(#24133) (Boshen)
- cb2a505 react_compiler: Codegen destructuring reassignment targets
(#24131) (Boshen)
- b82c394 react_compiler: Propagate codegen invariants instead of
emitting empty bodies (#24128) (Boshen)
- 5771982 react_compiler: Render unchanged programs as source in fixture
snapshots (#24129) (Boshen)
- 4b16e1a transformer/async-to-generator: Preserve direct eval scope
flags (#24136) (camc314)
- 4e9194f react_compiler: Lower `delete obj.prop` to
Property/ComputedDelete (#24123) (Boshen)
- 0b25582 ast: Type binding node `typeAnnotation` as `TSTypeAnnotation |
null` (#23113) (Boshen)
- 018c0e5 transformer: Hoist lowered async declarations (#22770)
(camc314)
- 652fbaf mangler: Keep names of destructured exported bindings (#24036)
(Dunqing)
- e274415 minifier: Don't drop global calls that throw despite pure
arguments (#23917) (Dunqing)
- 59abb30 minifier: Only merge string literals in `try_fold_add` when
the inner operator is `+` (#23622) (Jerry Zhao)

### ⚡ Performance

- c5ca77b transformer: Avoid cloning refresh options (#24191) (camc314)
- bf1a151 react_compiler: Compile out debug printers (#24184) (Boshen)
- abb44a0 transformer: Build fixed object-rest arguments (#24190)
(camc314)
- a4db731 isolated_declarations: Use `ReplaceWith` instead of `TakeIn`
(#24016) (overlookmotel)
- ff10855 transformer: Use `ReplaceWith` instead of `TakeIn` (#24015)
(overlookmotel)
- bd49aff ecmascript: Avoid heap-allocating Math.min/max/imul operands
(#23941) (Lawrence Lin)
- e4b708b react_compiler: Skip compiled files before prefilters (#24171)
(Boshen)
- c59f2fe rust: Return impl ExactSizeIterator from slice-backed
accessors (#24144) (Boshen)
- 5d6d04a codegen: SWAR-skip boring byte runs in sourcemap line/column
scan (#24023) (Boshen)
- a55e0be traverse: Reduce string operations in `get_var_name_from_node`
(#24007) (overlookmotel)
- e6d48e1 transformer/nullish_coalescing: Move cold path into separate
function (#23989) (overlookmotel)
- c4e35b5 transformer/object_rest_spread: Pre-allocate capacity in `Vec`
(#23988) (overlookmotel)
- 527b8e5 transformer/decorators: Narrow type earlier (#23987)
(overlookmotel)

### 📚 Documentation

- 30d17f5 allocator: Clarify docs for `TakeIn::take_in_box` (#24093)
(overlookmotel)
- 675e6a8 ast: Correct doc comment for `PrivateFieldExpression` (#24008)
(overlookmotel)
- e4c30e6 minifier: Explain what `dce` mode means (#23994) (Dunqing)
- 37cbf88 ast_macros: Document fields of `StructDetails` (#23959)
(overlookmotel)
- 4de3e54 ast: Correct doc comment (#23948) (overlookmotel)

Co-authored-by: Boshen <[email protected]>
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.

1 participant