From f707cb5ff321d1e5c66068274c224d5e7f3bb079 Mon Sep 17 00:00:00 2001 From: Joe Savona Date: Mon, 8 Apr 2024 17:09:07 -0700 Subject: [PATCH] HIR-based MergeOverlappingScopes ghstack-source-id: 93ac68114683044d7e6332fe7898b0dcc5dc6685 Pull Request resolved: https://github.com/facebook/react-forget/pull/2828 --- .../src/Entrypoint/Pipeline.ts | 14 +- .../AlignReactiveScopesToBlockScopesHIR.ts | 325 ++++++++++++++---- ...consequent-alternate-both-return.expect.md | 66 ++++ ...ted-in-consequent-alternate-both-return.js | 17 + ...ble-range-extending-into-ternary.expect.md | 106 ++++++ ...ro-mutable-range-extending-into-ternary.js | 44 +++ .../repro-separate-scopes-for-divs.expect.md | 115 +++++++ .../repro-separate-scopes-for-divs.js | 31 ++ ...erge-overlapping-reactive-scopes.expect.md | 71 ++++ ...-call-merge-overlapping-reactive-scopes.js | 23 ++ 10 files changed, 737 insertions(+), 75 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.js create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.js create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.js create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.expect.md create mode 100644 compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.js diff --git a/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts index d616150df6..939f341240 100644 --- a/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-forget/src/Entrypoint/Pipeline.ts @@ -264,14 +264,14 @@ function* runWithEnvironment( name: "AlignReactiveScopesToBlockScopes", value: reactiveFunction, }); - } - mergeOverlappingReactiveScopes(reactiveFunction); - yield log({ - kind: "reactive", - name: "MergeOverlappingReactiveScopes", - value: reactiveFunction, - }); + mergeOverlappingReactiveScopes(reactiveFunction); + yield log({ + kind: "reactive", + name: "MergeOverlappingReactiveScopes", + value: reactiveFunction, + }); + } buildReactiveBlocks(reactiveFunction); yield log({ diff --git a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts index faea4a14c4..59f9cda412 100644 --- a/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-forget/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts @@ -10,6 +10,7 @@ import { BlockId, HIRFunction, InstructionId, + MutableRange, Place, ReactiveScope, makeInstructionId, @@ -21,7 +22,9 @@ import { mapTerminalSuccessors, terminalFallthrough, } from "../HIR/visitors"; +import DisjointSet from "../Utils/DisjointSet"; import { retainWhere } from "../Utils/utils"; +import { getPlaceScope } from "./BuildReactiveBlocks"; /* * Note: this is the 2nd of 4 passes that determine how to break a function into discrete @@ -64,119 +67,149 @@ import { retainWhere } from "../Utils/utils"; * finds the first instruction after the scope's mutable range in that same block scope (which * will be the updated end for that scope). */ - export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { - type BlockContext = - | { kind: "block"; block: BlockId; scopes: Array } - | { - kind: "value"; - start: InstructionId; - end: InstructionId; - scopes: Array; - }; - const blockContexts = new Map(); + const blockNodes = new Map(); + const rootNode: BlockNode = { + kind: "node", + valueRange: null, + children: [], + id: makeInstructionId(0), + }; + blockNodes.set(fn.body.entry, rootNode); const seen = new Set(); + const placeScopes = new Map(); - function recordPlace(place: Place, context: BlockContext): void { - const scope = place.identifier.scope; + function recordPlace(id: InstructionId, place: Place, node: BlockNode): void { + if (place.identifier.scope !== null) { + placeScopes.set(place, place.identifier.scope); + } + + const scope = getPlaceScope(id, place); if (scope == null) { return; } + node.children.push({ kind: "scope", scope, id }); if (seen.has(scope)) { return; } - if (context.kind === "value") { + seen.add(scope); + if (node.valueRange !== null) { scope.range.start = makeInstructionId( - Math.min(context.start, scope.range.start) + Math.min(node.valueRange.start, scope.range.start) ); scope.range.end = makeInstructionId( - Math.max(context.end, scope.range.end) + Math.max(node.valueRange.end, scope.range.end) ); } - seen.add(scope); - context.scopes.push(scope); } for (const [, block] of fn.body.blocks) { const { instructions, terminal } = block; - let context = blockContexts.get(block.id); - if (context === undefined) { - if (block.kind === "block" || block.kind === "catch") { - context = { kind: "block", block: block.id, scopes: [] }; - } else { - CompilerError.invariant(false, { - reason: `Expected a context to be initialized for value block`, - loc: instructions[0]?.loc ?? terminal.loc, - description: `No value for block bb${block.id}`, - }); - } - } else if (block.kind === "block" && context.kind !== "block") { + const node = blockNodes.get(block.id); + if (node === undefined) { CompilerError.invariant(false, { - reason: `Expected a block context for block`, + reason: `Expected a node to be initialized for block`, loc: instructions[0]?.loc ?? terminal.loc, - description: `Got value block for bb${block.id}`, + description: `No node for block bb${block.id} (${block.kind})`, }); } - /* - * Any scopes that carried over across a terminal->fallback need their range extended - * to at least the first instruction of the fallback - */ - const startId = instructions.at(0)?.id ?? terminal.id; - for (const scope of context.scopes) { - scope.range.end = makeInstructionId(Math.max(scope.range.end, startId)); - } - - /* - * Visit instructions, pruning scopes that end and recording new scopes that appear - * on operands - */ for (const instr of instructions) { - retainWhere(context.scopes, (scope) => scope.range.end > instr.id); for (const lvalue of eachInstructionLValue(instr)) { - recordPlace(lvalue, context); + recordPlace(instr.id, lvalue, node); } for (const operand of eachInstructionValueOperand(instr.value)) { - recordPlace(operand, context); + recordPlace(instr.id, operand, node); } } - - // Close scopes that complete at the terminal, and visit scopes of operands - retainWhere(context.scopes, (scope) => scope.range.end > terminal.id); for (const operand of eachTerminalOperand(terminal)) { - recordPlace(operand, context); + recordPlace(terminal.id, operand, node); } - // Save the current context for the fallback block, where this block scope continues + // Save the current node for the fallback block, where this block scope continues const fallthrough = terminalFallthrough(terminal); - if (fallthrough !== null && !blockContexts.has(fallthrough)) { - blockContexts.set(fallthrough, context); + if (fallthrough !== null && !blockNodes.has(fallthrough)) { + /* + * Any scopes that carried over across a terminal->fallback need their range extended + * to at least the first instruction of the fallback + * + * Note that it's possible for a terminal such as an if or switch to have a null fallback, + * indicating that all control-flow paths diverge instead of reaching the fallthrough. + * In this case there isn't an instruction id in the program that we can point to for the + * updated range. Since the output is correct in this case we leave it, but it would be + * more correct to find the maximum instuction id in the whole program and set the range.end + * to one greater. Alternatively, we could leave in an unreachable fallthrough (with a new + * "unreachable" terminal variant, perhaps) and use that instruction id. + */ + const fallthroughBlock = fn.body.blocks.get(fallthrough)!; + const nextId = + fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id; + for (const child of node.children) { + if (child.kind !== "scope") { + continue; + } + const scope = child.scope; + if (scope.range.end > terminal.id) { + scope.range.end = makeInstructionId( + Math.max(scope.range.end, nextId) + ); + } + } + blockNodes.set(fallthrough, node); } /* * Visit all successors (not just direct successors for control-flow ordering) to - * set a value block context where necessary to align the value block start/end + * set a value block node where necessary to align the value block start/end * back to the outer block scope. * * TODO: add a variant of eachTerminalSuccessor() that visits _all_ successors, not * just those that are direct successors for normal control-flow ordering. */ mapTerminalSuccessors(terminal, (successor) => { + if (blockNodes.has(successor)) { + return successor; + } + const successorBlock = fn.body.blocks.get(successor)!; /* * we need the block kind check here because the do..while terminal's successor * is a block, and try's successor is a catch block */ - if ( - !blockContexts.has(successor) && - successorBlock.kind !== "block" && - successorBlock.kind !== "catch" + if (successorBlock.kind === "block" || successorBlock.kind === "catch") { + const childNode: BlockNode = { + kind: "node", + id: terminal.id, + children: [], + valueRange: null, + }; + node.children.push(childNode); + blockNodes.set(successor, childNode); + } else if ( + node.valueRange === null || + terminal.kind === "ternary" || + terminal.kind === "logical" || + terminal.kind === "optional" ) { - let valueContext: BlockContext; - if (context!.kind === "value") { - valueContext = context!; - } else { + /** + * Create a new scope node whenever we transition from block scope -> value scope. + * + * For compatibility with the previous ReactiveFunction-based scope merging logic, + * we also create new scope nodes for ternary, logical, and optional terminals. + * However, inside value blocks we always store a range (valueRange) that is the + * start/end instruction ids at the nearest parent block scope level, so that + * scopes inside the value blocks can be extended to align with block scope + * instructions. + */ + const childNode = { + kind: "node", + id: terminal.id, + children: [], + valueRange: null, + } as BlockNode; + if (node.valueRange === null) { + // Transition from block->value scope, derive the outer block scope range CompilerError.invariant(fallthrough !== null, { reason: `Expected a fallthrough for value block`, loc: terminal.loc, @@ -185,16 +218,172 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { const nextId = fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id; - valueContext = { - kind: "value", + childNode.valueRange = { start: terminal.id, end: nextId, - scopes: [], - } as BlockContext; + }; + } else { + // else value->value transition, reuse the range + childNode.valueRange = node.valueRange; } - blockContexts.set(successor, valueContext); + node.children.push(childNode); + blockNodes.set(successor, childNode); + } else { + // this is a value -> value block transition, reuse the node + blockNodes.set(successor, node); } return successor; }); } + + // console.log(_debug(rootNode)); + + const joinedScopes: DisjointSet = + mergeOverlappingScopes(rootNode); + + joinedScopes.forEach((scope, groupScope) => { + if (scope !== groupScope) { + groupScope.range.start = makeInstructionId( + Math.min(groupScope.range.start, scope.range.start) + ); + groupScope.range.end = makeInstructionId( + Math.max(groupScope.range.end, scope.range.end) + ); + } + }); + for (const [place, originalScope] of placeScopes) { + const nextScope = joinedScopes.find(originalScope); + if (nextScope !== null && nextScope !== originalScope) { + place.identifier.scope = nextScope; + } + } +} + +type BlockNode = { + kind: "node"; + id: InstructionId; + valueRange: MutableRange | null; + children: Array; +}; +type ReactiveScopeNode = { + kind: "scope"; + id: InstructionId; + scope: ReactiveScope; +}; + +function _debug(node: BlockNode): string { + const buf: Array = []; + _printNode(node, buf, 0); + return buf.join("\n"); +} +function _printNode( + node: BlockNode | ReactiveScopeNode, + out: Array, + depth: number = 0 +): void { + let prefix = " ".repeat(depth); + if (node.kind === "scope") { + out.push( + `${prefix}[${node.id}] @${node.scope.id} [${node.scope.range.start}:${node.scope.range.end}]` + ); + } else { + let range = + node.valueRange !== null + ? ` [${node.valueRange.start}:${node.valueRange.end}]` + : ""; + out.push(`${prefix}[${node.id}] node${range} [`); + for (const child of node.children) { + _printNode(child, out, depth + 1); + } + out.push(`${prefix}]`); + } +} + +type ScopeItem = { + scope: ReactiveScope; + shadowedBy: ReactiveScope | null; +}; +class BlockItem { + seen: Set = new Set(); + scopes: Array = []; +} + +function mergeOverlappingScopes(root: BlockNode): DisjointSet { + const seen = new Set(); + const joined = new DisjointSet(); + + function visit(node: BlockNode, stack: Array): void { + const currentBlock = stack.at(-1)!; + child: for (const child of node.children) { + retainWhere(currentBlock.scopes, (item) => { + if (item.scope.range.end > child.id) { + return true; + } else { + currentBlock.seen.delete(item.scope); + return false; + } + }); + if (child.kind === "node") { + visit(child, [...stack, new BlockItem()]); + } else { + const scope = child.scope; + if (!seen.has(scope)) { + seen.add(scope); + currentBlock.seen.add(scope); + currentBlock.scopes.push({ shadowedBy: null, scope }); + continue; + } + + let index = stack.length - 1; + let nextBlock = currentBlock; + while (!nextBlock.seen.has(scope)) { + joined.union([scope, ...nextBlock.scopes.map((s) => s.scope)]); + index--; + if (index < 0) { + currentBlock.seen.add(scope); + currentBlock.scopes.push({ shadowedBy: null, scope }); + continue child; + } + nextBlock = stack[index]!; + } + + // Handle interleaving within a given block scope + let found = false; + for (let i = 0; i < nextBlock.scopes.length; i++) { + const current = nextBlock.scopes[i]!; + if (current.scope.id === scope.id) { + found = true; + if (current.shadowedBy !== null) { + joined.union([current.shadowedBy, current.scope]); + } + } else if (found && current.shadowedBy === null) { + // `scope` is shadowing `current` and may interleave + current.shadowedBy = scope; + if (current.scope.range.end > scope.range.end) { + /* + * Current is shadowed by `scope`, and we know that `current` will mutate + * again (per its range), so the scopes are already known to interleave. + * + * Eagerly extend the ranges of the scopes so that we don't prematurely end + * a scope relative to its eventual post-merge mutable range + */ + const end = makeInstructionId( + Math.max(current.scope.range.end, scope.range.end) + ); + current.scope.range.end = end; + scope.range.end = end; + joined.union([current.scope, scope]); + } + } + } + if (!currentBlock.seen.has(scope)) { + currentBlock.seen.add(scope); + currentBlock.scopes.push({ shadowedBy: null, scope }); + } + } + } + } + + visit(root, [new BlockItem()]); + return joined; } diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md new file mode 100644 index 0000000000..47a68b2804 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md @@ -0,0 +1,66 @@ + +## Input + +```javascript +import { makeObject_Primitives } from "shared-runtime"; + +function Component(props) { + const object = makeObject_Primitives(); + if (props.cond) { + object.value = 1; + return object; + } else { + object.value = props.value; + return object; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, value: [0, 1, 2] }], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { makeObject_Primitives } from "shared-runtime"; + +function Component(props) { + const $ = useMemoCache(2); + let t0; + if ($[0] !== props) { + t0 = Symbol.for("react.early_return_sentinel"); + bb9: { + const object = makeObject_Primitives(); + if (props.cond) { + object.value = 1; + t0 = object; + break bb9; + } else { + object.value = props.value; + t0 = object; + break bb9; + } + } + $[0] = props; + $[1] = t0; + } else { + t0 = $[1]; + } + if (t0 !== Symbol.for("react.early_return_sentinel")) { + return t0; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, value: [0, 1, 2] }], +}; + +``` + +### Eval output +(kind: ok) {"a":0,"b":"value1","c":true,"value":[0,1,2]} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.js new file mode 100644 index 0000000000..513a752c07 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.js @@ -0,0 +1,17 @@ +import { makeObject_Primitives } from "shared-runtime"; + +function Component(props) { + const object = makeObject_Primitives(); + if (props.cond) { + object.value = 1; + return object; + } else { + object.value = props.value; + return object; + } +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, value: [0, 1, 2] }], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md new file mode 100644 index 0000000000..4b9822fdb8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.expect.md @@ -0,0 +1,106 @@ + +## Input + +```javascript +import { useState } from "react"; + +function Component(props) { + const items = props.items ? props.items.slice() : []; + const [state] = useState(""); + return props.cond ? ( +
{state}
+ ) : ( +
+ {items.map((item) => ( +
{item.name}
+ ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, items: [{ id: 0, name: "Alice" }] }], + sequentialRenders: [ + { cond: false, items: [{ id: 0, name: "Alice" }] }, + { + cond: false, + items: [ + { id: 0, name: "Alice" }, + { id: 1, name: "Bob" }, + ], + }, + { + cond: true, + items: [ + { id: 0, name: "Alice" }, + { id: 1, name: "Bob" }, + ], + }, + { + cond: false, + items: [ + { id: 1, name: "Bob" }, + { id: 2, name: "Claire" }, + ], + }, + ], +}; + +``` + +## Code + +```javascript +import { useState } from "react"; + +function Component(props) { + const items = props.items ? props.items.slice() : []; + const [state] = useState(""); + return props.cond ? ( +
{state}
+ ) : ( +
+ {items.map((item) => ( +
{item.name}
+ ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, items: [{ id: 0, name: "Alice" }] }], + sequentialRenders: [ + { cond: false, items: [{ id: 0, name: "Alice" }] }, + { + cond: false, + items: [ + { id: 0, name: "Alice" }, + { id: 1, name: "Bob" }, + ], + }, + { + cond: true, + items: [ + { id: 0, name: "Alice" }, + { id: 1, name: "Bob" }, + ], + }, + { + cond: false, + items: [ + { id: 1, name: "Bob" }, + { id: 2, name: "Claire" }, + ], + }, + ], +}; + +``` + +### Eval output +(kind: ok)
Alice
+
Alice
Bob
+
+
Bob
Claire
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.js new file mode 100644 index 0000000000..948483d182 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-mutable-range-extending-into-ternary.js @@ -0,0 +1,44 @@ +import { useState } from "react"; + +function Component(props) { + const items = props.items ? props.items.slice() : []; + const [state] = useState(""); + return props.cond ? ( +
{state}
+ ) : ( +
+ {items.map((item) => ( +
{item.name}
+ ))} +
+ ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, items: [{ id: 0, name: "Alice" }] }], + sequentialRenders: [ + { cond: false, items: [{ id: 0, name: "Alice" }] }, + { + cond: false, + items: [ + { id: 0, name: "Alice" }, + { id: 1, name: "Bob" }, + ], + }, + { + cond: true, + items: [ + { id: 0, name: "Alice" }, + { id: 1, name: "Bob" }, + ], + }, + { + cond: false, + items: [ + { id: 1, name: "Bob" }, + { id: 2, name: "Claire" }, + ], + }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.expect.md new file mode 100644 index 0000000000..4183ee1408 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.expect.md @@ -0,0 +1,115 @@ + +## Input + +```javascript +import { identity } from "shared-runtime"; + +const DISPLAY = true; +function Component({ cond = false, id }) { + return ( + <> +
+ + {cond === false && ( +
+ )} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, id: 42 }], + sequentialRenders: [ + { cond: false, id: 4 }, + { cond: true, id: 4 }, + { cond: true, id: 42 }, + ], +}; + +const styles = { + a: "a", + b: "b", + c: "c", + d: "d", +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import { identity } from "shared-runtime"; + +const DISPLAY = true; +function Component(t0) { + const $ = useMemoCache(9); + const { cond: t1, id } = t0; + const cond = t1 === undefined ? false : t1; + let t2; + if ($[0] !== id) { + t2 = identity(styles.a, id !== null ? styles.b : {}); + $[0] = id; + $[1] = t2; + } else { + t2 = $[1]; + } + let t3; + if ($[2] !== t2) { + t3 =
; + $[2] = t2; + $[3] = t3; + } else { + t3 = $[3]; + } + let t4; + if ($[4] !== cond) { + t4 = cond === false && ( +
+ ); + $[4] = cond; + $[5] = t4; + } else { + t4 = $[5]; + } + let t5; + if ($[6] !== t3 || $[7] !== t4) { + t5 = ( + <> + {t3} + {t4} + + ); + $[6] = t3; + $[7] = t4; + $[8] = t5; + } else { + t5 = $[8]; + } + return t5; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, id: 42 }], + sequentialRenders: [ + { cond: false, id: 4 }, + { cond: true, id: 4 }, + { cond: true, id: 42 }, + ], +}; + +const styles = { + a: "a", + b: "b", + c: "c", + d: "d", +}; + +``` + +### Eval output +(kind: ok)
+
+
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.js new file mode 100644 index 0000000000..2d564c174a --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-separate-scopes-for-divs.js @@ -0,0 +1,31 @@ +import { identity } from "shared-runtime"; + +const DISPLAY = true; +function Component({ cond = false, id }) { + return ( + <> +
+ + {cond === false && ( +
+ )} + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false, id: 42 }], + sequentialRenders: [ + { cond: false, id: 4 }, + { cond: true, id: 4 }, + { cond: true, id: 42 }, + ], +}; + +const styles = { + a: "a", + b: "b", + c: "c", + d: "d", +}; diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.expect.md b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.expect.md new file mode 100644 index 0000000000..3f39bfe134 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.expect.md @@ -0,0 +1,71 @@ + +## Input + +```javascript +import fbt from "fbt"; +import { Stringify } from "shared-runtime"; + +function Component(props) { + const label = fbt( + fbt.plural("bar", props.value.length, { + many: "bars", + showCount: "yes", + }), + "The label text" + ); + return props.cond ? ( + Text here} + label={label.toString()} + /> + ) : null; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: true, value: [0, 1, 2] }], +}; + +``` + +## Code + +```javascript +import { unstable_useMemoCache as useMemoCache } from "react"; +import fbt from "fbt"; +import { Stringify } from "shared-runtime"; + +function Component(props) { + const $ = useMemoCache(3); + let t0; + if ($[0] !== props.value.length || $[1] !== props.cond) { + const label = fbt._( + { "*": "{number} bars", _1: "1 bar" }, + [fbt._plural(props.value.length, "number")], + { hk: "4mUen7" } + ); + + t0 = props.cond ? ( + + ) : null; + $[0] = props.value.length; + $[1] = props.cond; + $[2] = t0; + } else { + t0 = $[2]; + } + return t0; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: true, value: [0, 1, 2] }], +}; + +``` + +### Eval output +(kind: ok)
{"description":"Text here","label":"3 bars"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.js b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.js new file mode 100644 index 0000000000..77453fa354 --- /dev/null +++ b/compiler/packages/babel-plugin-react-forget/src/__tests__/fixtures/compiler/repro-unmerged-fbt-call-merge-overlapping-reactive-scopes.js @@ -0,0 +1,23 @@ +import fbt from "fbt"; +import { Stringify } from "shared-runtime"; + +function Component(props) { + const label = fbt( + fbt.plural("bar", props.value.length, { + many: "bars", + showCount: "yes", + }), + "The label text" + ); + return props.cond ? ( + Text here} + label={label.toString()} + /> + ) : null; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: true, value: [0, 1, 2] }], +};