Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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(Clone, Copy, Default, PartialEq, Eq, Debug)]
#[repr(transparent)]
pub struct UnaryFlags: u8 {
/// The expression "typeof (0, x)" must not become "typeof x" if "x"
Expand Down
12 changes: 8 additions & 4 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,
bun_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: *bun_flags,
Comment thread
robobun marked this conversation as resolved.
},
loc,
))
Expand Down Expand Up @@ -1995,7 +1998,8 @@ fn codegen_base_instruction_value(
E::Unary {
op: OpCode::UnDelete,
value: property_access_expr(obj, property, loc, None),
flags: E::UnaryFlags::empty(),
// `lower_unary` only creates PropertyDelete when this flag was set.
flags: E::UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS,
},
loc,
))
Expand Down Expand Up @@ -2055,7 +2059,7 @@ fn codegen_base_instruction_value(
},
loc,
),
flags: E::UnaryFlags::empty(),
flags: E::UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS,
},
loc,
))
Expand Down
2 changes: 2 additions & 0 deletions src/react_compiler/hir/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -752,6 +752,8 @@ pub enum InstructionValue {
UnaryExpression {
operator: UnaryOperator,
value: Place,
/// Parse-time flags from the visited `E::Unary`, restored at codegen.
bun_flags: bun_ast::E::UnaryFlags,
loc: Option<SourceLocation>,
},
TypeCastExpression {
Expand Down
16 changes: 14 additions & 2 deletions src/react_compiler/lowering/build_hir/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -984,15 +984,26 @@ fn lower_unary(
let loc = convert_loc(bun_loc);
match unary.op {
UnDelete => match &unary.value.data {
Data::EDot(d) if d.optional_chain.is_none() => {
// A flagless EDot here is a visitor-folded no-op `delete`; bail like upstream.
Data::EDot(d)
if d.optional_chain.is_none()
&& unary.flags.contains(
E::UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS,
) =>
{
let object = lower_expression_to_temporary(builder, &d.target)?;
Ok(InstructionValue::PropertyDelete {
object,
property: PropertyLiteral::String(d.name),
loc,
})
}
Data::EIndex(i) if i.optional_chain.is_none() => {
Data::EIndex(i)
if i.optional_chain.is_none()
&& unary.flags.contains(
E::UnaryFlags::WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS,
) =>
{
let object = lower_expression_to_temporary(builder, &i.target)?;
let property = lower_expression_to_temporary(builder, &i.index)?;
Ok(InstructionValue::ComputedDelete {
Expand Down Expand Up @@ -1021,6 +1032,7 @@ fn lower_unary(
Ok(InstructionValue::UnaryExpression {
operator,
value,
bun_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
119 changes: 119 additions & 0 deletions test/bundler/transpiler/react-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -557,6 +557,125 @@ describe("bundler", () => {
},
});

// Regression: codegen.rs PropertyDelete/ComputedDelete/UnaryExpression emitted
// `E::Unary` with `UnaryFlags::empty()`. The parser sets
// `WAS_ORIGINALLY_DELETE_OF_IDENTIFIER_OR_PROPERTY_ACCESS` for `delete <dot|index>`;
// the printer re-wraps any `delete <dot|index>` lacking that flag as
// `delete (0, obj.prop)`, which evaluates the property to a value and returns
// `true` without deleting anything.
Comment thread
coderabbitai[bot] marked this conversation as resolved.
itBundled("react-compiler/PropertyDeletePreservesReferenceSemantics", {
files: {
"/entry.jsx": /* jsx */ `
import { useMemo } from "react";
export function useThing(a, b) {
return useMemo(() => {
const x = { a, b, c: 3 };
delete x.b;
const key = "c";
delete x[key];
return x;
}, [a, b]);
}
console.log(JSON.stringify(useThing(1, 2)));
`,
"/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: '{"a":1}' },
onAfterBundle(api) {
const out = api.readFile("/out.js");
// The hook must be compiled (sanity: 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");
// `delete (0, x.b)` / `delete (0, x[...])` evaluates to a value, not a
// Reference — must not appear for either the dot or index form.
expect(out).not.toMatch(/delete\s*\(\s*0\s*,/);
},
});

// Sibling of the above: `WAS_ORIGINALLY_TYPEOF_IDENTIFIER` was also dropped,
// so the printer wrapped `typeof undeclared` as `typeof (0, undeclared)`,
// which throws ReferenceError instead of returning "undefined" — breaking
// the common `typeof window !== "undefined"` SSR check.
itBundled("react-compiler/TypeofUnboundIdentifierPreservesFlag", {
files: {
"/entry.jsx": /* jsx */ `
import { useMemo } from "react";
export function useIsBrowser() {
return useMemo(() => typeof window !== "undefined", []);
}
// The folded-conditional form must keep throwing semantics: the visitor
// wraps it as a real (0, x) comma expression, and codegen must not set
// the flag just because the operand inlines to an identifier.
export function useTypeofFolded() {
return useMemo(() => {
try {
return typeof (true ? NotDeclaredAnywhere : Other);
} catch {
return "threw";
}
}, []);
}
console.log(useIsBrowser(), useTypeofFolded());
`,
"/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: "false threw" },
onAfterBundle(api) {
const out = api.readFile("/out.js");
// Both hooks must be compiled (RC drops the `useMemo` wrapper); these
// particular bodies need 0 memo slots so compiler-runtime is tree-shaken.
expect(out).not.toContain("useMemo(");
// `typeof window` must survive as-is; `typeof (true ? ...)` must stay wrapped.
expect(out).toMatch(/\btypeof window\b(?!\s*\))/);
expect(out).toMatch(/\btypeof\s*\(\s*0\s*,\s*NotDeclaredAnywhere\s*\)/);
},
});

// `delete (true ? o.a : o.b)` is a no-op per spec (operand is a value, not a
// Reference). The visitor folds the conditional to a bare EDot with the
// delete-flag unset; lowering must not turn that into a real PropertyDelete.
// Upstream's Babel plugin sees the unfolded ConditionalExpression and bails
// with "Only object properties can be deleted", so bailing out here matches.
itBundled("react-compiler/DeleteFoldedConditionalKeepsNoOpSemantics", {
files: {
"/entry.jsx": /* jsx */ `
export function Comp({ a, b }) {
const o = { a, b };
const r = delete (true ? o.a : o.b);
return <div>{r}{JSON.stringify(o)}</div>;
}
const el = Comp({ a: 1, b: 2 });
console.log(el.props.children.join(""));
`,
"/node_modules/react/index.js": `module.exports = {};`,
"/node_modules/react/jsx-runtime.js": `exports.jsx = exports.jsxs = (t, p) => ({ t, props: p });`,
"/node_modules/react/jsx-dev-runtime.js": `exports.jsxDEV = (t, p) => ({ t, props: p });`,
"/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: 'true{"a":1,"b":2}' },
onAfterBundle(api) {
const out = api.readFile("/out.js");
// The component bails out of compilation (Babel parity), so the delete
// stays in its post-visit `delete (0, o.a)` form.
expect(out).toMatch(/delete\s*\(\s*0\s*,\s*o\.a\s*\)/);
},
});

itBundled("react-compiler/NonComponentUntouched", {
files: {
"/entry.jsx": /* jsx */ `
Expand Down
Loading