Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ast/e.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 5 additions & 2 deletions src/react_compiler/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
))
Expand Down
5 changes: 5 additions & 0 deletions src/react_compiler/hir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,11 @@ pub enum InstructionValue {
UnaryExpression {
operator: UnaryOperator,
value: Place,
/// Bun's printer re-wraps `typeof <unbound-identifier>` as `typeof (0, x)`
/// unless `WAS_ORIGINALLY_TYPEOF_IDENTIFIER` is set, so codegen must
/// restore the parse-time flag. Upstream has no equivalent because
/// Babel's generator never inserts the `(0, …)` guard.
Comment thread
robobun marked this conversation as resolved.
Outdated
flags: bun_ast::e::UnaryFlags,
loc: Option<SourceLocation>,
},
TypeCastExpression {
Expand Down
1 change: 1 addition & 0 deletions src/react_compiler/lowering/build_hir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1021,6 +1021,7 @@ fn lower_unary(
Ok(InstructionValue::UnaryExpression {
operator,
value,
flags: unary.flags,
loc,
})
}
Expand Down
1 change: 1 addition & 0 deletions src/react_compiler/optimization/constant_propagation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,7 @@ fn evaluate_instruction(
operator,
value,
loc,
..
} => match operator {
UnaryOperator::Not => {
let operand = read(constants, value);
Expand Down
67 changes: 67 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 <identifier>`; 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 <identifier>` 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/);
},
});
});
Loading