diff --git a/src/ast/e.rs b/src/ast/e.rs index 8a9a1b327072..e33ce74f8bb9 100644 --- a/src/ast/e.rs +++ b/src/ast/e.rs @@ -135,7 +135,7 @@ pub struct Unary { } bitflags::bitflags! { - #[derive(Clone, Copy, Default, PartialEq, Eq)] + #[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] #[repr(transparent)] pub struct UnaryFlags: u8 { /// The expression "typeof (0, x)" must not become "typeof x" if "x" diff --git a/src/react_compiler/codegen.rs b/src/react_compiler/codegen.rs index 4645cc51c5a1..6f9a76970052 100644 --- a/src/react_compiler/codegen.rs +++ b/src/react_compiler/codegen.rs @@ -1853,14 +1853,17 @@ fn codegen_base_instruction_value( )) } InstructionValue::UnaryExpression { - operator, value, .. + operator, + value, + flags, + .. } => { let arg = codegen_place_to_expression(cx, value)?; Ok(Expr::init( E::Unary { op: convert_unary_operator(*operator), value: arg, - flags: E::UnaryFlags::empty(), + flags: *flags, }, loc, )) diff --git a/src/react_compiler/hir/mod.rs b/src/react_compiler/hir/mod.rs index 9722be83b2ad..2fdcdbe213ed 100644 --- a/src/react_compiler/hir/mod.rs +++ b/src/react_compiler/hir/mod.rs @@ -752,6 +752,8 @@ pub enum InstructionValue { UnaryExpression { operator: UnaryOperator, value: Place, + /// Parse-time `E::UnaryFlags`, threaded back to codegen for the printer. + flags: bun_ast::e::UnaryFlags, loc: Option, }, TypeCastExpression { diff --git a/src/react_compiler/lowering/build_hir/expr.rs b/src/react_compiler/lowering/build_hir/expr.rs index 21912029a62e..d48122651b92 100644 --- a/src/react_compiler/lowering/build_hir/expr.rs +++ b/src/react_compiler/lowering/build_hir/expr.rs @@ -1021,6 +1021,7 @@ fn lower_unary( Ok(InstructionValue::UnaryExpression { operator, value, + flags: unary.flags, loc, }) } diff --git a/src/react_compiler/optimization/constant_propagation.rs b/src/react_compiler/optimization/constant_propagation.rs index 9800ebb06858..13ea86874172 100644 --- a/src/react_compiler/optimization/constant_propagation.rs +++ b/src/react_compiler/optimization/constant_propagation.rs @@ -449,6 +449,7 @@ fn evaluate_instruction( operator, value, loc, + .. } => match operator { UnaryOperator::Not => { let operand = read(constants, value); diff --git a/test/bundler/transpiler/react-compiler.test.ts b/test/bundler/transpiler/react-compiler.test.ts index 87de7f56028e..c539236a800f 100644 --- a/test/bundler/transpiler/react-compiler.test.ts +++ b/test/bundler/transpiler/react-compiler.test.ts @@ -891,4 +891,71 @@ describe("bundler", () => { expect(out).toMatch(/__MEMO_CACHE_SENTINEL\)\s*\{[^}]*globalFn\(\)/); }, }); + + // Regression: codegen.rs UnaryExpression emitted `E::Unary` with + // `UnaryFlags::empty()`. The parser sets `WAS_ORIGINALLY_TYPEOF_IDENTIFIER` + // for `typeof `; without that flag the printer re-wraps an + // unbound operand as `typeof (0, x)`, which evaluates `x` and throws a + // ReferenceError instead of returning "undefined". + itBundled("react-compiler/TypeofUnboundGlobalReturnsUndefined", { + files: { + "/entry.jsx": /* jsx */ ` + import { useMemo } from "react"; + export function useThing(a) { + return useMemo(() => [typeof SomeUnboundGlobal, a], [a]); + } + console.log(JSON.stringify(useThing(1))); + `, + "/node_modules/react/index.js": `exports.useMemo = (f) => f();`, + "/node_modules/react/compiler-runtime.js": `exports.c = n => new Array(n).fill(Symbol.for("react.memo_cache_sentinel"));`, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + }, + reactCompiler: true, + target: "browser", + backend: "cli", + run: { stdout: '["undefined",1]' }, + onAfterBundle(api) { + const out = api.readFile("/out.js"); + // The hook must be compiled (codegen, not a bailout, is on trial). + // With react bundled the `_c` import is renamed, so assert on the + // compiler-runtime body being linked in instead. + expect(out).toContain("react.memo_cache_sentinel"); + // `typeof (0, SomeUnboundGlobal)` evaluates the reference and throws. + expect(out).not.toMatch(/typeof\s*\(\s*0\s*,\s*SomeUnboundGlobal/); + expect(out).toMatch(/typeof\s+SomeUnboundGlobal\b/); + }, + }); + + // Control: the parse-time flag is NOT set for `typeof (0, x)`. Lowering + // discards the side-effect-free `0`, so codegen sees a bare identifier + // again; the printer must still re-wrap because the source did not have + // `typeof ` semantics. + itBundled("react-compiler/TypeofSequenceUnboundGlobalStillThrows", { + files: { + "/entry.jsx": /* jsx */ ` + import { useMemo } from "react"; + export function useThing(a) { + return useMemo(() => [typeof (0, SomeUnboundGlobal), a], [a]); + } + try { + useThing(1); + console.log("no throw"); + } catch (e) { + console.log(e instanceof ReferenceError ? "ReferenceError" : "other"); + } + `, + "/node_modules/react/index.js": `exports.useMemo = (f) => f();`, + "/node_modules/react/compiler-runtime.js": `exports.c = n => new Array(n).fill(Symbol.for("react.memo_cache_sentinel"));`, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + }, + reactCompiler: true, + target: "browser", + backend: "cli", + run: { stdout: "ReferenceError" }, + onAfterBundle(api) { + const out = api.readFile("/out.js"); + expect(out).toContain("react.memo_cache_sentinel"); + expect(out).toMatch(/typeof\s*\(\s*0\s*,\s*SomeUnboundGlobal/); + }, + }); });