feat(react_compiler): implement enableEmitHookGuards codegen (#24329)

Implements the last `unimplemented`-flagged codegen path, porting TS `createHookGuard`/`createCallExpression` exactly:

- **Whole-body wrap**: with `enableEmitHookGuards` in client mode, the compiled function body becomes `try { $dispatcherGuard(0); … } finally { $dispatcherGuard(1); }`, applied before the memo-cache preface is unshifted so `const $ = _c(n)` stays outside the guard.
- **Per-hook-call wrap**: hook calls (`getHookKind != null`, for both `CallExpression` and `MethodCall`) are wrapped in `(function () { try { $dispatcherGuard(2); return useHook(…); } finally { $dispatcherGuard(3); } })()`. Non-hook calls are untouched — the previous stub errored on *every* call when guards were enabled.
- The guard import was already resolved by the existing `hook_guard_name` plumbing; codegen just references it.

Since this was the only producer of `CompilerError::unimplemented`, the flag and the empty-body fallback in `codegen_function` are removed — codegen errors now always propagate as diagnostics.

Output verified against the upstream fixture: `flag-enable-emit-hook-guards` previously fell back to an **empty function body**; it now matches `flag-enable-emit-hook-guards.expect.md` node-for-node (same 4 memo slots, same guard kinds, hooks-only wrapping including the nested `useContext` and the `ObjectWithHooks.useIdentity` method call), differing only in oxc_codegen paren style for IIFEs in expression position. The remaining 1735 fixture snapshots are unchanged.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
This commit is contained in:
Boshen committed 2026-07-10 04:38:14 +00:00
1 parent 1f8997d76a
commit a3a39f98eb
3 files changed
+223 -94

No files matched your search

+1 -8
View File
@@ -120,18 +120,11 @@ pub struct CompilerError {
/// because errors created directly (e.g., via `?` from a pass) are
/// analogous to thrown errors in the TS code.
pub is_thrown: bool,
/// Set when the error originates from an oxc codegen sub-emitter that has not
/// yet been ported (e.g. destructuring reassignment targets, hook-guard
/// wrapping). `codegen_function` swallows these and falls back to an empty
/// body — preserving the pre-port behavior — instead of surfacing a spurious
/// diagnostic for a construct the upstream compiler handles. Genuine invariant
/// errors leave this `false` and propagate as diagnostics.
pub unimplemented: bool,
}
impl CompilerError {
pub fn new() -> Self {
Self { diagnostics: Vec::new(), is_thrown: true, unimplemented: false }
Self { diagnostics: Vec::new(), is_thrown: true }
}
pub fn push(&mut self, diagnostic: OxcDiagnostic) {
@@ -37,7 +37,7 @@ use crate::react_compiler_hir::PrimitiveValue;
use crate::react_compiler_hir::PropertyLiteral;
use crate::react_compiler_hir::ScopeId;
use crate::react_compiler_hir::SpreadPattern;
use crate::react_compiler_hir::environment::Environment;
use crate::react_compiler_hir::environment::{Environment, OutputMode};
use crate::react_compiler_hir::reactive::PrunedReactiveScopeBlock;
use crate::react_compiler_hir::reactive::ReactiveBlock;
use crate::react_compiler_hir::reactive::ReactiveFunction;
@@ -105,42 +105,25 @@ pub fn codegen_function<'a, 'h>(
fbt_operands,
);
// A few codegen sub-emitters (destructuring reassignment targets, hook-guard
// wrapping) are not yet ported and raise an `unimplemented` error. For those,
// fall back to an empty body so the function is emitted un-memoized instead of
// surfacing a spurious diagnostic for a construct the upstream compiler handles.
//
// Genuine invariant errors (unnamed temporaries, MethodCall property must be an
// unmemoized MemberExpression, const/let referenced as an expression, ...) are
// NOT `unimplemented` and propagate as diagnostics — matching the upstream
// compiler, which throws an Invariant on these inputs rather than silently
// emitting wrong code.
let mut compiled = match ox_codegen_reactive_function(&mut cx, func) {
Ok(compiled) => compiled,
Err(err) if err.unimplemented => OxcCompiledFunction {
params: oxc_ast::ast::FormalParameters::boxed(
SPAN,
oxc_ast::ast::FormalParameterKind::FormalParameter,
oxc_allocator::ArenaVec::new_in(ast),
None::<oxc_allocator::Box<oxc_ast::ast::FormalParameterRest>>,
ast,
),
body: oxc_ast::ast::FunctionBody::boxed(
SPAN,
oxc_allocator::ArenaVec::new_in(ast),
oxc_allocator::ArenaVec::new_in(ast),
ast,
),
generator: false,
is_async: false,
memo_slots_used: 0,
memo_blocks: 0,
memo_values: 0,
pruned_memo_blocks: 0,
pruned_memo_values: 0,
},
Err(err) => return Err(err),
};
let mut compiled = ox_codegen_reactive_function(&mut cx, func)?;
// enableEmitHookGuards: wrap the compiled body in push/pop dispatcher guards.
// Runs before the memo-cache preface is unshifted so `const $ = _c(n)` stays
// outside the guard, matching TS `codegenFunction`.
if cx.env.output_mode == OutputMode::Client
&& let Some(guard_name) = cx.env.hook_guard_name.as_deref()
{
let body_stmts =
std::mem::replace(&mut compiled.body.statements, oxc_allocator::ArenaVec::new_in(ast));
let guarded = ox_create_hook_guard(
&cx.ast,
guard_name,
body_stmts,
GuardKind::PushHookGuard,
GuardKind::PopHookGuard,
);
compiled.body.statements = oxc_allocator::ArenaVec::from_value_in(guarded, ast);
}
let cache_count = compiled.memo_slots_used;
if cache_count != 0 {
@@ -1386,11 +1369,6 @@ fn ox_codegen_for_init<'a, 'h>(
// The HIR-driven control flow is identical; only node construction differs. Since
// oxc tracks positions by `Span` (not Babel-style locs), the per-node span
// propagation (`apply_loc_to_value` / place-span overrides) collapses to `SPAN`.
//
// `FunctionExpression` / `ObjectExpression` / JSX / non-trivial `TypeCastExpression`
// emission are deferred to later batches and currently raise an invariant error
// (which fails compilation of that function and falls back to the original program,
// matching the current differential floor).
// =============================================================================
fn ox_convert_value_to_expression<'a>(
@@ -1918,16 +1896,8 @@ fn ox_codegen_base_instruction_value<'a>(
InstructionValue::CallExpression { callee, args, .. } => {
let callee_expr = ox_codegen_place_to_expression(cx, callee)?;
let arguments = ox_codegen_arguments(cx, args)?;
let call_expr = oxc_ast::ast::Expression::new_call_expression(
SPAN,
callee_expr,
None::<oxc_allocator::Box<oxc::TSTypeParameterInstantiation>>,
arguments,
false,
&cx.ast,
);
let result = ox_maybe_wrap_hook_call(cx, call_expr)?;
Ok(OxValue::Expression(result))
let call = ox_create_call_expression(cx, callee_expr, arguments, callee.identifier);
Ok(OxValue::Expression(call))
}
InstructionValue::MethodCall { property, args, .. } => {
let member_expr = ox_codegen_place_to_expression(cx, property)?;
@@ -1944,16 +1914,8 @@ fn ox_codegen_base_instruction_value<'a>(
return Err(err);
}
let arguments = ox_codegen_arguments(cx, args)?;
let call_expr = oxc_ast::ast::Expression::new_call_expression(
SPAN,
member_expr,
None::<oxc_allocator::Box<oxc::TSTypeParameterInstantiation>>,
arguments,
false,
&cx.ast,
);
let result = ox_maybe_wrap_hook_call(cx, call_expr)?;
Ok(OxValue::Expression(result))
let call = ox_create_call_expression(cx, member_expr, arguments, property.identifier);
Ok(OxValue::Expression(call))
}
InstructionValue::NewExpression { callee, args, .. } => {
let callee_expr = ox_codegen_place_to_expression(cx, callee)?;
@@ -3238,21 +3200,140 @@ fn ox_convert_member_expression_to_jsx<'a>(
Ok((object, property))
}
fn ox_maybe_wrap_hook_call<'a>(
/// `GuardKind` runtime constants, synced with `react-compiler-runtime`'s enum
/// (TS `Utils/RuntimeDiagnosticConstants.ts`).
#[derive(Clone, Copy)]
enum GuardKind {
PushHookGuard = 0,
PopHookGuard = 1,
AllowHook = 2,
DisallowHook = 3,
}
/// `$dispatcherGuard(<kind>);` — `guard_name` must already be arena-allocated.
fn ox_dispatcher_guard_stmt<'a>(
ast: &oxc_ast::builder::AstBuilder<'a>,
guard_name: &'a str,
kind: GuardKind,
) -> oxc::Statement<'a> {
let call = oxc_ast::ast::Expression::new_call_expression(
SPAN,
oxc_ast::ast::Expression::new_identifier(SPAN, guard_name, ast),
None::<oxc_allocator::Box<oxc::TSTypeParameterInstantiation>>,
oxc_allocator::ArenaVec::from_value_in(
oxc_ast::ast::Argument::from(ox_number(ast, kind as u8 as f64)),
ast,
),
false,
ast,
);
oxc_ast::ast::Statement::new_expression_statement(SPAN, call, ast)
}
/// `try { $dispatcherGuard(<before>); <stmts> } finally { $dispatcherGuard(<after>); }`
///
/// Matches TS `createHookGuard`.
fn ox_create_hook_guard<'a>(
ast: &oxc_ast::builder::AstBuilder<'a>,
guard_name: &str,
stmts: oxc_allocator::ArenaVec<'a, oxc::Statement<'a>>,
before: GuardKind,
after: GuardKind,
) -> oxc::Statement<'a> {
let guard_name = ox_str(ast, guard_name);
let mut try_stmts = oxc_allocator::ArenaVec::with_capacity_in(stmts.len() + 1, ast);
try_stmts.push(ox_dispatcher_guard_stmt(ast, guard_name, before));
try_stmts.extend(stmts);
let try_block = oxc_ast::ast::BlockStatement::new(SPAN, try_stmts, ast);
let finalizer = oxc_ast::ast::BlockStatement::new(
SPAN,
oxc_allocator::ArenaVec::from_value_in(
ox_dispatcher_guard_stmt(ast, guard_name, after),
ast,
),
ast,
);
oxc_ast::ast::Statement::new_try_statement(
SPAN,
try_block,
None::<oxc_ast::ast::CatchClause>,
Some(oxc_allocator::ArenaBox::new_in(finalizer, ast)),
ast,
)
}
/// Build a call expression for `CallExpression`/`MethodCall`, matching TS
/// `createCallExpression`: with `enableEmitHookGuards` in client mode, hook
/// calls are wrapped in a guarded IIFE:
/// `(function () { try { $dispatcherGuard(2); return <call>; } finally { $dispatcherGuard(3); } })()`
fn ox_create_call_expression<'a>(
cx: &OxcContext<'a, '_, '_>,
call_expr: oxc::Expression<'a>,
) -> Result<oxc::Expression<'a>, CompilerError> {
// enableEmitHookGuards wrapping is deferred to a later batch; the guard is
// off by default, so unwrapped calls match the differential floor.
if cx.env.hook_guard_name.is_some()
&& cx.env.output_mode == crate::react_compiler_hir::environment::OutputMode::Client
callee: oxc::Expression<'a>,
arguments: oxc_allocator::ArenaVec<'a, oxc::Argument<'a>>,
callee_id: IdentifierId,
) -> oxc::Expression<'a> {
let ast = &cx.ast;
let call_expr = oxc_ast::ast::Expression::new_call_expression(
SPAN,
callee,
None::<oxc_allocator::Box<oxc::TSTypeParameterInstantiation>>,
arguments,
false,
ast,
);
// The hook-kind lookup only runs with guards enabled, keeping the default
// codegen path free of per-call shape probing.
let Some(guard_name) = cx.env.hook_guard_name.as_deref() else {
return call_expr;
};
if cx.env.output_mode != OutputMode::Client
|| cx.env.get_hook_kind_for_id(callee_id).ok().flatten().is_none()
{
return Err(unimplemented_err(
"Hook guard wrapping in oxc codegen is not yet ported (deferred to a later batch)",
None,
));
return call_expr;
}
Ok(call_expr)
let return_stmt = oxc_ast::ast::Statement::new_return_statement(SPAN, Some(call_expr), ast);
let guarded = ox_create_hook_guard(
ast,
guard_name,
oxc_allocator::ArenaVec::from_value_in(return_stmt, ast),
GuardKind::AllowHook,
GuardKind::DisallowHook,
);
let body = oxc_ast::ast::FunctionBody::boxed(
SPAN,
oxc_allocator::ArenaVec::new_in(ast),
oxc_allocator::ArenaVec::from_value_in(guarded, ast),
ast,
);
let params = oxc_ast::ast::FormalParameters::boxed(
SPAN,
oxc_ast::ast::FormalParameterKind::FormalParameter,
oxc_allocator::ArenaVec::new_in(ast),
None::<oxc_allocator::Box<oxc_ast::ast::FormalParameterRest>>,
ast,
);
let iife = oxc_ast::ast::Function::new(
SPAN,
oxc::FunctionType::FunctionExpression,
None::<oxc_ast::ast::BindingIdentifier>,
false,
false,
false,
None::<oxc_allocator::Box<oxc::TSTypeParameterDeclaration>>,
None::<oxc_allocator::Box<oxc::TSThisParameter>>,
params,
None::<oxc_allocator::Box<oxc::TSTypeAnnotation>>,
Some(body),
ast,
);
oxc_ast::ast::Expression::new_call_expression(
SPAN,
oxc::Expression::FunctionExpression(oxc_allocator::ArenaBox::new_in(iife, ast)),
None::<oxc_allocator::Box<oxc::TSTypeParameterInstantiation>>,
oxc_allocator::ArenaVec::new_in(ast),
false,
ast,
)
}
// =============================================================================
@@ -3462,17 +3543,6 @@ fn invariant(condition: bool, reason: &str, span: Option<Span>) -> Result<(), Co
if !condition { Err(invariant_err(reason, span)) } else { Ok(()) }
}
/// Construct an error for a codegen sub-emitter that has not yet been ported. Unlike
/// [`invariant_err`], `codegen_function` catches these (via `CompilerError::unimplemented`)
/// and falls back to an empty body rather than surfacing a spurious diagnostic for a
/// construct the upstream compiler handles. Use this only for genuinely-unported paths,
/// never for real invariants (which must propagate and fail loudly like upstream).
fn unimplemented_err(reason: &str, span: Option<Span>) -> CompilerError {
let mut err = invariant_err(reason, span);
err.unimplemented = true;
err
}
fn invariant_err(reason: &str, span: Option<Span>) -> CompilerError {
invariant_err_with_detail_message(reason, reason, span)
}
@@ -3,11 +3,77 @@ source: crates/oxc_react_compiler/tests/snapshot.rs
input_file: crates/oxc_react_compiler/fixtures/flag-enable-emit-hook-guards.ts
---
import { $dispatcherGuard } from "react-compiler-runtime";
import { c as _c } from "react/compiler-runtime";
// @enableEmitHookGuards
import { createContext, useContext, useEffect, useState } from "react";
import { CONST_STRING0, ObjectWithHooks, getNumber, identity, print } from "shared-runtime";
const MyContext = createContext("my context value");
function Component() {}
function Component(t0) {
const $ = _c(4);
try {
$dispatcherGuard(0);
const { value } = t0;
print(identity(CONST_STRING0));
let t1;
if ($[0] === Symbol.for("react.memo_cache_sentinel")) {
t1 = getNumber();
$[0] = t1;
} else {
t1 = $[0];
}
const [state, setState] = function() {
try {
$dispatcherGuard(2);
return useState(t1);
} finally {
$dispatcherGuard(3);
}
}();
print(value, state);
let t2;
let t3;
if ($[1] !== state) {
t2 = () => {
if (state === 4) {
setState(5);
}
};
t3 = [state];
$[1] = state;
$[2] = t2;
$[3] = t3;
} else {
t2 = $[2];
t3 = $[3];
}
(function() {
try {
$dispatcherGuard(2);
return useEffect(t2, t3);
} finally {
$dispatcherGuard(3);
}
})();
print(identity(value + state));
return function() {
try {
$dispatcherGuard(2);
return ObjectWithHooks.useIdentity(function() {
try {
$dispatcherGuard(2);
return useContext(MyContext);
} finally {
$dispatcherGuard(3);
}
}());
} finally {
$dispatcherGuard(3);
}
}();
} finally {
$dispatcherGuard(1);
}
}
export const FIXTURE_ENTRYPOINT = {
fn: Component,
args: [{ value: 0 }]