diff --git a/src/react_compiler/hir/cfg_utils.rs b/src/react_compiler/hir/cfg_utils.rs index ae7306280630..99127968cd59 100644 --- a/src/react_compiler/hir/cfg_utils.rs +++ b/src/react_compiler/hir/cfg_utils.rs @@ -221,12 +221,18 @@ pub fn remove_unnecessary_try_catch(hir: &mut HIR) { } // Clean up fallthrough predecessor info - if let Some(fallthrough) = hir.blocks.get_mut(&fallthrough_id) { + if let Some(fallthrough) = hir.blocks.get(&fallthrough_id) { if fallthrough.preds.len() == 1 && fallthrough.preds.contains(&handler_id) { // The handler was the only predecessor: remove the fallthrough block hir.blocks.shift_remove(&fallthrough_id); } else { - fallthrough.preds.shift_remove(&handler_id); + // Upstream removes only `handler_id`, but a longer handler ends in another block. + let is_gone = |pred: &&BlockId| !hir.blocks.contains_key(*pred); + let gone: Blocks = fallthrough.preds.iter().filter(is_gone).copied().collect(); + let fallthrough = hir.blocks.get_mut(&fallthrough_id).unwrap(); + for pred in &gone { + fallthrough.preds.shift_remove(pred); + } } } } diff --git a/test/bundler/transpiler/react-compiler.test.ts b/test/bundler/transpiler/react-compiler.test.ts index 3eaa8fe94a1d..1e9156776a96 100644 --- a/test/bundler/transpiler/react-compiler.test.ts +++ b/test/bundler/transpiler/react-compiler.test.ts @@ -1931,6 +1931,191 @@ describe("bundler", () => { }, }); + // PruneMaybeThrows removes a catch handler when nothing in the try body can + // throw: here the body is a constant, or a check that `process.env.NODE_ENV` + // folds away. The `try` terminal then becomes a goto, and the block after the + // try statement survives only if it is merged into the try block. A catch + // body of more than one block (`if`, `?:`, `??`, `?.`) left a stale + // predecessor that blocked the merge: `function IfInCatch(props) { ; }`. In a + // loop body or an outer try body only the rest of that body was lost. + // babel-plugin-react-compiler 1.0.0 prints the same. + // + // The fake `c` records the size of every memo cache, so the second element of + // each pair is the `_c(n)` of the function, or `[]` for one that the compiler + // leaves as written. `SingleBlockCatch` and `CanThrow` are the controls. + // `AssignedInBranchingCatch` is skipped (its phi has an operand of a removed + // block). `DeadTryInDoWhile` loses the test of its do-while with the handler + // and is skipped too: a merge into that `break` would move `p.f()` into the + // `if`. + itBundled("react-compiler/TryBodyThatCannotThrowWithBranchingCatchKeepsTheRestOfTheFunction", { + files: { + "/entry.jsx": /* jsx */ ` + import { sizes } from "react/compiler-runtime"; + + const validate = () => {}; + + function DevOnlyCheck(props) { + try { + const isDev = process.env.NODE_ENV !== "production"; + if (isDev) validate(props); + } catch (e) { + props.onError?.(e); + } + return
{props.a}
; + } + function IfInCatch(props) { + try { + const limit = 3; + } catch (e) { + if (props.debug) console.warn(e); + } + return
{props.a}
; + } + function TernaryInCatch(props) { + try { + const limit = 3; + } catch { + console.log(props.c ? 1 : 2); + } + return
{props.a}
; + } + function NullishInCatch(p) { + try { + const limit = 3; + } catch { + const u0 = p.o ?? 1; + } + return
{p.a}
; + } + function useOptionalInCatch(p) { + "use memo"; + try { + const limit = 3; + } catch { + console.log(p.o?.v); + } + return [p.a]; + } + function RestOfLoopBody(p) { + const out = []; + for (const it of p.list) { + try { + const limit = 3; + } catch { + if (p.c) console.log(1); + } + out.push(it); + } + return
{out}
; + } + function RestOfOuterTryBody(p) { + try { + try { + const limit = 3; + } catch { + if (p.c) console.log(1); + } + p.f(); + } catch {} + return
{p.a}
; + } + function SingleBlockCatch(props) { + try { + const limit = 3; + } catch (e) { + console.warn(e); + } + return
{props.a}
; + } + function CanThrow(props) { + try { + props.load(); + } catch (e) { + if (props.debug) console.warn(e); + } + return
{props.a}
; + } + function AssignedInBranchingCatch(props) { + let ok = true; + try { + const limit = 3; + } catch { + if (props.strict) ok = false; + } + return
{ok ? "on" : "off"}
; + } + function DeadTryInDoWhile(p) { + try { + do { + if (p.a) break; + try { + const limit = 3; + return null; + } catch {} + break; + } while (p.c); + p.f(); + } catch {} + return
{p.z}
; + } + + function render(fn, props) { + const before = sizes().length; + const result = fn(props); + return [result?.props?.children ?? result ?? "undefined", sizes().slice(before)]; + } + let fCalls = 0; + const f = () => fCalls++; + console.log(JSON.stringify({ + DevOnlyCheck: render(DevOnlyCheck, { a: 1 }), + IfInCatch: render(IfInCatch, { a: 2 }), + TernaryInCatch: render(TernaryInCatch, { a: 3 }), + NullishInCatch: render(NullishInCatch, { a: 4 }), + useOptionalInCatch: render(useOptionalInCatch, { a: 5 }), + RestOfLoopBody: render(RestOfLoopBody, { list: [1, 2] }), + RestOfOuterTryBody: render(RestOfOuterTryBody, { a: 6, f }), + SingleBlockCatch: render(SingleBlockCatch, { a: 7 }), + CanThrow: render(CanThrow, { a: 8, load: f }), + AssignedInBranchingCatch: render(AssignedInBranchingCatch, { strict: true }), + DeadTryInDoWhile: render(DeadTryInDoWhile, { a: true, z: 9, f }), + fCalls, + })); + `, + "/node_modules/react/package.json": `{"name":"react","main":"./index.js"}`, + "/node_modules/react/index.js": `exports.createElement = () => null;`, + "/node_modules/react/jsx-runtime.js": `exports.jsx = exports.jsxs = (type, props) => ({ type, props });`, + "/node_modules/react/jsx-dev-runtime.js": `exports.jsxDEV = (type, props) => ({ type, props });`, + "/node_modules/react/compiler-runtime.js": /* js */ ` + const sizes = []; + exports.c = size => { + sizes.push(size); + return new Array(size).fill(Symbol.for("react.memo_cache_sentinel")); + }; + exports.sizes = () => sizes; + `, + }, + reactCompiler: true, + target: "browser", + backend: "cli", + define: { "process.env.NODE_ENV": '"production"' }, + run: { + stdout: JSON.stringify({ + DevOnlyCheck: [1, [2]], + IfInCatch: [2, [2]], + TernaryInCatch: [3, [2]], + NullishInCatch: [4, [2]], + useOptionalInCatch: [[5], [2]], + RestOfLoopBody: [[1, 2], [2]], + RestOfOuterTryBody: [6, [2]], + SingleBlockCatch: [7, [2]], + CanThrow: [8, [2]], + AssignedInBranchingCatch: ["on", []], + DeadTryInDoWhile: [9, []], + fCalls: 3, + }), + }, + }); + // A 0-arg call to an unknown import is non-reactive in InferReactivePlaces // (no operand is reactive, callee isn't a hook), so its scope's deps prune // to empty and it becomes a sentinel-only block. Babel does the same; this