mirror of
https://github.com/oxc-project/oxc.git
synced 2026-11-04 10:29:58 +00:00
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:
3 files changed
+223
-94
No files matched your search
@@ -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) {
|
||||
|
||||
+155
-85
@@ -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 }]
|
||||
|
||||
Reference in new issue
Block a user