diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts index 3e451c23c7807..3d78d99cb10ed 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/AlignReactiveScopesToBlockScopesHIR.ts @@ -22,8 +22,10 @@ import { mapTerminalSuccessors, terminalFallthrough, } from "../HIR/visitors"; +import { retainWhere_Set } from "../Utils/utils"; import { getPlaceScope } from "./BuildReactiveBlocks"; +type InstructionRange = MutableRange; /* * Note: this is the 2nd of 4 passes that determine how to break a function into discrete * reactive scopes (independently memoizeable units of code): @@ -66,18 +68,20 @@ import { getPlaceScope } from "./BuildReactiveBlocks"; * will be the updated end for that scope). */ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { - const blockNodes = new Map(); - const rootNode: BlockNode = { - kind: "node", - valueRange: null, - children: [], - id: makeInstructionId(0), - }; - blockNodes.set(fn.body.entry, rootNode); + const activeBlockFallthroughRanges: Array<{ + range: InstructionRange; + fallthrough: BlockId; + }> = []; + const activeScopes = new Set(); const seen = new Set(); + const valueBlockNodes = new Map(); const placeScopes = new Map(); - function recordPlace(id: InstructionId, place: Place, node: BlockNode): void { + function recordPlace( + id: InstructionId, + place: Place, + node: ValueBlockNode | null + ): void { if (place.identifier.scope !== null) { placeScopes.set(place, place.identifier.scope); } @@ -86,13 +90,14 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { if (scope == null) { return; } - node.children.push({ kind: "scope", scope, id }); + activeScopes.add(scope); + node?.children.push({ kind: "scope", scope, id }); if (seen.has(scope)) { return; } seen.add(scope); - if (node.valueRange !== null) { + if (node != null && node.valueRange !== null) { scope.range.start = makeInstructionId( Math.min(node.valueRange.start, scope.range.start) ); @@ -103,16 +108,25 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { } for (const [, block] of fn.body.blocks) { - const { instructions, terminal } = block; - const node = blockNodes.get(block.id); - if (node === undefined) { - CompilerError.invariant(false, { - reason: `Expected a node to be initialized for block`, - loc: instructions[0]?.loc ?? terminal.loc, - description: `No node for block bb${block.id} (${block.kind})`, - }); + const startingId = block.instructions[0]?.id ?? block.terminal.id; + retainWhere_Set(activeScopes, (scope) => scope.range.end > startingId); + const top = activeBlockFallthroughRanges.at(-1); + if (top?.fallthrough === block.id) { + activeBlockFallthroughRanges.pop(); + /* + * All active scopes must have either started before or within the last + * block-fallthrough range. In either case, they overlap this block- + * fallthrough range and can have their ranges extended. + */ + for (const scope of activeScopes) { + scope.range.start = makeInstructionId( + Math.min(scope.range.start, top.range.start) + ); + } } + const { instructions, terminal } = block; + const node = valueBlockNodes.get(block.id) ?? null; for (const instr of instructions) { for (const lvalue of eachInstructionLValue(instr)) { recordPlace(instr.id, lvalue, node); @@ -125,36 +139,42 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { recordPlace(terminal.id, operand, node); } - // Save the current node for the fallback block, where this block scope continues const fallthrough = terminalFallthrough(terminal); - if (fallthrough !== null && !blockNodes.has(fallthrough)) { + if (fallthrough !== null) { /* - * 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. + * Any currently active scopes that overlaps the block-fallthrough range + * need their range extended to at least the first instruction of the + * fallthrough */ 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; + for (const scope of activeScopes) { if (scope.range.end > terminal.id) { scope.range.end = makeInstructionId( Math.max(scope.range.end, nextId) ); } } - blockNodes.set(fallthrough, node); + /** + * We also record the block-fallthrough range for future scopes that begin + * within the range (and overlap with the range end). + */ + activeBlockFallthroughRanges.push({ + fallthrough, + range: { + start: terminal.id, + end: nextId, + }, + }); + + CompilerError.invariant(!valueBlockNodes.has(fallthrough), { + reason: "Expect hir blocks to have unique fallthroughs", + loc: terminal.loc, + }); + if (node != null) { + valueBlockNodes.set(fallthrough, node); + } } /* @@ -166,48 +186,35 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { * just those that are direct successors for normal control-flow ordering. */ mapTerminalSuccessors(terminal, (successor) => { - if (blockNodes.has(successor)) { + if (valueBlockNodes.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 (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); + /* + * 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 + */ } else if ( - node.valueRange === null || + node == null || terminal.kind === "ternary" || terminal.kind === "logical" || terminal.kind === "optional" ) { /** - * Create a new scope node whenever we transition from block scope -> value scope. + * Create a new node whenever we transition from non-value -> value block. * * 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 + * 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 + let valueRange: MutableRange; + if (node == null) { + // Transition from block->value block, derive the outer block range CompilerError.invariant(fallthrough !== null, { reason: `Expected a fallthrough for value block`, loc: terminal.loc, @@ -216,32 +223,36 @@ export function alignReactiveScopesToBlockScopesHIR(fn: HIRFunction): void { const nextId = fallthroughBlock.instructions[0]?.id ?? fallthroughBlock.terminal.id; - childNode.valueRange = { + valueRange = { start: terminal.id, end: nextId, }; } else { // else value->value transition, reuse the range - childNode.valueRange = node.valueRange; + valueRange = node.valueRange; } - node.children.push(childNode); - blockNodes.set(successor, childNode); + const childNode: ValueBlockNode = { + kind: "node", + id: terminal.id, + children: [], + valueRange, + }; + node?.children.push(childNode); + valueBlockNodes.set(successor, childNode); } else { // this is a value -> value block transition, reuse the node - blockNodes.set(successor, node); + valueBlockNodes.set(successor, node); } return successor; }); } - - // console.log(_debug(rootNode)); } -type BlockNode = { +type ValueBlockNode = { kind: "node"; id: InstructionId; - valueRange: MutableRange | null; - children: Array; + valueRange: MutableRange; + children: Array; }; type ReactiveScopeNode = { kind: "scope"; @@ -249,13 +260,13 @@ type ReactiveScopeNode = { scope: ReactiveScope; }; -function _debug(node: BlockNode): string { +function _debug(node: ValueBlockNode): string { const buf: Array = []; _printNode(node, buf, 0); return buf.join("\n"); } function _printNode( - node: BlockNode | ReactiveScopeNode, + node: ValueBlockNode | ReactiveScopeNode, out: Array, depth: number = 0 ): void { @@ -265,10 +276,7 @@ function _printNode( `${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}]` - : ""; + let range = ` (range=[${node.valueRange.start}:${node.valueRange.end}])`; out.push(`${prefix}[${node.id}] node${range} [`); for (const child of node.children) { _printNode(child, out, depth + 1); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts index c02e813255676..921a6bf2097d0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -45,6 +45,17 @@ export function retainWhere( array.length = writeIndex; } +export function retainWhere_Set( + items: Set, + predicate: (item: T) => boolean +): void { + for (const item of items) { + if (!predicate(item)) { + items.delete(item); + } + } +} + export function getOrInsertWith( m: Map, key: U, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.expect.md new file mode 100644 index 0000000000000..d386efbc310a6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.expect.md @@ -0,0 +1,76 @@ + +## Input + +```javascript +import { mutate } from "shared-runtime"; + +/** + * Similar fixture to `align-scopes-nested-block-structure`, but + * a simpler case. + */ +function useFoo(cond) { + let s = null; + if (cond) { + s = {}; + } else { + return null; + } + mutate(s); + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { mutate } from "shared-runtime"; + +/** + * Similar fixture to `align-scopes-nested-block-structure`, but + * a simpler case. + */ +function useFoo(cond) { + const $ = _c(3); + let s; + let t0; + if ($[0] !== cond) { + t0 = Symbol.for("react.early_return_sentinel"); + bb0: { + if (cond) { + s = {}; + } else { + t0 = null; + break bb0; + } + + mutate(s); + } + $[0] = cond; + $[1] = t0; + $[2] = s; + } else { + t0 = $[1]; + s = $[2]; + } + if (t0 !== Symbol.for("react.early_return_sentinel")) { + return t0; + } + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; + +``` + +### Eval output +(kind: ok) {"wat0":"joe"} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.ts new file mode 100644 index 0000000000000..03b73fa179192 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scope-starts-within-cond.ts @@ -0,0 +1,21 @@ +import { mutate } from "shared-runtime"; + +/** + * Similar fixture to `align-scopes-nested-block-structure`, but + * a simpler case. + */ +function useFoo(cond) { + let s = null; + if (cond) { + s = {}; + } else { + return null; + } + mutate(s); + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md new file mode 100644 index 0000000000000..449beb18fd81c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.expect.md @@ -0,0 +1,55 @@ + +## Input + +```javascript +import { getNull } from "shared-runtime"; + +function Component(props) { + const items = (() => { + return getNull() ?? []; + })(); + items.push(props.a); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: {} }], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { getNull } from "shared-runtime"; + +function Component(props) { + const $ = _c(3); + let t0; + let items; + if ($[0] !== props.a) { + t0 = getNull() ?? []; + items = t0; + + items.push(props.a); + $[0] = props.a; + $[1] = items; + $[2] = t0; + } else { + items = $[1]; + t0 = $[2]; + } + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ a: {} }], +}; + +``` + +### Eval output +(kind: ok) [{}] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-iife-return-modified-later-logical.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.expect.md new file mode 100644 index 0000000000000..b06f495cd7272 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.expect.md @@ -0,0 +1,169 @@ + +## Input + +```javascript +import { mutate } from "shared-runtime"; +/** + * Fixture showing that it's not sufficient to only align direct scoped + * accesses of a block-fallthrough pair. + * Below is a simplified view of HIR blocks in this fixture. + * Note that here, s is mutated in both bb1 and bb4. However, neither + * bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves. + * + * This means that we need to recursively visit all scopes accessed between + * a block and its fallthrough and extend the range of those scopes which overlap + * with an active block/fallthrough pair, + * + * bb0 + * ┌──────────────┐ + * │let s = null │ + * │test cond1 │ + * │ │ + * └┬─────────────┘ + * │ bb1 + * ├─►┌───────┐ + * │ │s = {} ├────┐ + * │ └───────┘ │ + * │ bb2 │ + * └─►┌───────┐ │ + * │return;│ │ + * └───────┘ │ + * bb3 │ + * ┌──────────────┐◄┘ + * │test cond2 │ + * │ │ + * └┬─────────────┘ + * │ bb4 + * ├─►┌─────────┐ + * │ │mutate(s)├─┐ + * ▼ └─────────┘ │ + * bb5 │ + * ┌───────────┐ │ + * │return s; │◄──┘ + * └───────────┘ + */ +function useFoo({ cond1, cond2 }) { + let s = null; + if (cond1) { + s = {}; + } else { + return null; + } + + if (cond2) { + mutate(s); + } + + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond1: true, cond2: false }], + sequentialRenders: [ + { cond1: true, cond2: false }, + { cond1: true, cond2: false }, + { cond1: true, cond2: true }, + { cond1: true, cond2: true }, + { cond1: false, cond2: true }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { mutate } from "shared-runtime"; +/** + * Fixture showing that it's not sufficient to only align direct scoped + * accesses of a block-fallthrough pair. + * Below is a simplified view of HIR blocks in this fixture. + * Note that here, s is mutated in both bb1 and bb4. However, neither + * bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves. + * + * This means that we need to recursively visit all scopes accessed between + * a block and its fallthrough and extend the range of those scopes which overlap + * with an active block/fallthrough pair, + * + * bb0 + * ┌──────────────┐ + * │let s = null │ + * │test cond1 │ + * │ │ + * └┬─────────────┘ + * │ bb1 + * ├─►┌───────┐ + * │ │s = {} ├────┐ + * │ └───────┘ │ + * │ bb2 │ + * └─►┌───────┐ │ + * │return;│ │ + * └───────┘ │ + * bb3 │ + * ┌──────────────┐◄┘ + * │test cond2 │ + * │ │ + * └┬─────────────┘ + * │ bb4 + * ├─►┌─────────┐ + * │ │mutate(s)├─┐ + * ▼ └─────────┘ │ + * bb5 │ + * ┌───────────┐ │ + * │return s; │◄──┘ + * └───────────┘ + */ +function useFoo(t0) { + const $ = _c(4); + const { cond1, cond2 } = t0; + let s; + let t1; + if ($[0] !== cond1 || $[1] !== cond2) { + t1 = Symbol.for("react.early_return_sentinel"); + bb0: { + if (cond1) { + s = {}; + } else { + t1 = null; + break bb0; + } + if (cond2) { + mutate(s); + } + } + $[0] = cond1; + $[1] = cond2; + $[2] = t1; + $[3] = s; + } else { + t1 = $[2]; + s = $[3]; + } + if (t1 !== Symbol.for("react.early_return_sentinel")) { + return t1; + } + return s; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond1: true, cond2: false }], + sequentialRenders: [ + { cond1: true, cond2: false }, + { cond1: true, cond2: false }, + { cond1: true, cond2: true }, + { cond1: true, cond2: true }, + { cond1: false, cond2: true }, + ], +}; + +``` + +### Eval output +(kind: ok) {} +{} +{"wat0":"joe"} +{"wat0":"joe"} +null \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scopes-nested-block-structure.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.ts similarity index 81% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scopes-nested-block-structure.expect.md rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.ts index 9d19f7fa21986..3087f041a553e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scopes-nested-block-structure.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-nested-block-structure.ts @@ -1,7 +1,4 @@ - -## Input - -```javascript +import { mutate } from "shared-runtime"; /** * Fixture showing that it's not sufficient to only align direct scoped * accesses of a block-fallthrough pair. @@ -41,7 +38,7 @@ * │return s; │◄──┘ * └───────────┘ */ -function useFoo(cond1, cond2) { +function useFoo({ cond1, cond2 }) { let s = null; if (cond1) { s = {}; @@ -56,13 +53,14 @@ function useFoo(cond1, cond2) { return s; } -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:10(5:15) -``` - - \ No newline at end of file +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond1: true, cond2: false }], + sequentialRenders: [ + { cond1: true, cond2: false }, + { cond1: true, cond2: false }, + { cond1: true, cond2: true }, + { cond1: true, cond2: true }, + { cond1: false, cond2: true }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md new file mode 100644 index 0000000000000..906c15092e076 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.expect.md @@ -0,0 +1,84 @@ + +## Input + +```javascript +function useFoo({ cond }) { + let items: any = {}; + b0: { + if (cond) { + // Mutable range of `items` begins here, but its reactive scope block + // should be aligned to above the if-branch + items = []; + } else { + break b0; + } + items.push(2); + } + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond: true }], + sequentialRenders: [ + { cond: true }, + { cond: true }, + { cond: false }, + { cond: false }, + { cond: true }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function useFoo(t0) { + const $ = _c(3); + const { cond } = t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = {}; + $[0] = t1; + } else { + t1 = $[0]; + } + let items = t1; + bb0: if ($[1] !== cond) { + if (cond) { + items = []; + } else { + break bb0; + } + + items.push(2); + $[1] = cond; + $[2] = items; + } else { + items = $[2]; + } + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond: true }], + sequentialRenders: [ + { cond: true }, + { cond: true }, + { cond: false }, + { cond: false }, + { cond: true }, + ], +}; + +``` + +### Eval output +(kind: ok) [2] +[2] +{} +{} +[2] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-if.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-label.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-label.expect.md new file mode 100644 index 0000000000000..dd5a9a1926d8f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-label.expect.md @@ -0,0 +1,79 @@ + +## Input + +```javascript +import { arrayPush } from "shared-runtime"; + +function useFoo({ cond, value }) { + let items; + label: { + items = []; + // Mutable range of `items` begins here, but its reactive scope block + // should be aligned to above the label-block + if (cond) break label; + arrayPush(items, value); + } + arrayPush(items, value); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond: true, value: 2 }], + sequentialRenders: [ + { cond: true, value: 2 }, + { cond: true, value: 2 }, + { cond: true, value: 3 }, + { cond: false, value: 3 }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { arrayPush } from "shared-runtime"; + +function useFoo(t0) { + const $ = _c(3); + const { cond, value } = t0; + let items; + if ($[0] !== cond || $[1] !== value) { + bb0: { + items = []; + if (cond) { + break bb0; + } + arrayPush(items, value); + } + + arrayPush(items, value); + $[0] = cond; + $[1] = value; + $[2] = items; + } else { + items = $[2]; + } + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ cond: true, value: 2 }], + sequentialRenders: [ + { cond: true, value: 2 }, + { cond: true, value: 2 }, + { cond: true, value: 3 }, + { cond: false, value: 3 }, + ], +}; + +``` + +### Eval output +(kind: ok) [2] +[2] +[3] +[3,3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-label.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-label.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.expect.md new file mode 100644 index 0000000000000..80a0349b3e8c0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.expect.md @@ -0,0 +1,65 @@ + +## Input + +```javascript +import { arrayPush, mutate } from "shared-runtime"; + +function useFoo({ value }) { + let items = null; + try { + // Mutable range of `items` begins here, but its reactive scope block + // should be aligned to above the try-block + items = []; + arrayPush(items, value); + } catch { + // ignore + } + mutate(items); + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ value: 2 }], + sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { arrayPush, mutate } from "shared-runtime"; + +function useFoo(t0) { + const $ = _c(2); + const { value } = t0; + let items; + if ($[0] !== value) { + try { + items = []; + arrayPush(items, value); + } catch {} + + mutate(items); + $[0] = value; + $[1] = items; + } else { + items = $[1]; + } + return items; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ value: 2 }], + sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }], +}; + +``` + +### Eval output +(kind: ok) [2] +[2] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.ts similarity index 89% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.ts index 0a4a7eab6c73d..378cdff83a3ee 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-reactive-scope-overlaps-try.ts @@ -1,4 +1,4 @@ -import { arrayPush } from "shared-runtime"; +import { arrayPush, mutate } from "shared-runtime"; function useFoo({ value }) { let items = null; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.expect.md new file mode 100644 index 0000000000000..b8f525d6b98cf --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +import { CONST_TRUE, makeObject_Primitives } from "shared-runtime"; + +function Foo() { + try { + let thing = null; + if (cond) { + thing = makeObject_Primitives(); + } + if (CONST_TRUE) { + mutate(thing); + } + return thing; + } catch {} +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { CONST_TRUE, makeObject_Primitives } from "shared-runtime"; + +function Foo() { + const $ = _c(1); + try { + let thing; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + thing = null; + if (cond) { + thing = makeObject_Primitives(); + } + if (CONST_TRUE) { + mutate(thing); + } + $[0] = thing; + } else { + thing = $[0]; + } + return thing; + } catch {} +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; + +``` + +### Eval output +(kind: ok) \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.ts new file mode 100644 index 0000000000000..2cc042094463e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/align-scopes-trycatch-nested-overlapping-range.ts @@ -0,0 +1,19 @@ +import { CONST_TRUE, makeObject_Primitives } from "shared-runtime"; + +function Foo() { + try { + let thing = null; + if (cond) { + thing = makeObject_Primitives(); + } + if (CONST_TRUE) { + mutate(thing); + } + return thing; + } catch {} +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.expect.md deleted file mode 100644 index ca77829e2f7f4..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.expect.md +++ /dev/null @@ -1,26 +0,0 @@ - -## Input - -```javascript -function Foo() { - try { - let thing = null; - if (cond) { - thing = makeObject(); - } - if (otherCond) { - mutate(thing); - } - } catch {} -} - -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 2:24(18:26) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.js deleted file mode 100644 index 37be9363a5acf..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.bug-repro-trycatch-nested-overlapping-range.js +++ /dev/null @@ -1,11 +0,0 @@ -function Foo() { - try { - let thing = null; - if (cond) { - thing = makeObject(); - } - if (otherCond) { - mutate(thing); - } - } catch {} -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.expect.md deleted file mode 100644 index 7cd2acc9affab..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.expect.md +++ /dev/null @@ -1,27 +0,0 @@ - -## Input - -```javascript -function Foo(props, ref) { - const value = {}; - if (cond1) { - mutate(value); - return ; - } - mutate(value); - if (cond2) { - return ; - } - return value; -} - -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 1:21(16:23) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.js deleted file mode 100644 index 8837c348eeba7..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.repro-bug-ref-mutable-range.js +++ /dev/null @@ -1,12 +0,0 @@ -function Foo(props, ref) { - const value = {}; - if (cond1) { - mutate(value); - return ; - } - mutate(value); - if (cond2) { - return ; - } - return value; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.expect.md deleted file mode 100644 index a3b498b1c373a..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.expect.md +++ /dev/null @@ -1,29 +0,0 @@ - -## Input - -```javascript -/** - * Similar fixture to `error.todo-align-scopes-nested-block-structure`, but - * a simpler case. - */ -function useFoo(cond) { - let s = null; - if (cond) { - s = {}; - } else { - return null; - } - mutate(s); - return s; -} - -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:10(5:13) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.ts deleted file mode 100644 index 555bb713898a8..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scope-starts-within-cond.ts +++ /dev/null @@ -1,14 +0,0 @@ -/** - * Similar fixture to `error.todo-align-scopes-nested-block-structure`, but - * a simpler case. - */ -function useFoo(cond) { - let s = null; - if (cond) { - s = {}; - } else { - return null; - } - mutate(s); - return s; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scopes-nested-block-structure.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scopes-nested-block-structure.ts deleted file mode 100644 index 8e99f0435b562..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-align-scopes-nested-block-structure.ts +++ /dev/null @@ -1,53 +0,0 @@ -/** - * Fixture showing that it's not sufficient to only align direct scoped - * accesses of a block-fallthrough pair. - * Below is a simplified view of HIR blocks in this fixture. - * Note that here, s is mutated in both bb1 and bb4. However, neither - * bb1 nor bb4 have terminal fallthroughs or are fallthroughs themselves. - * - * This means that we need to recursively visit all scopes accessed between - * a block and its fallthrough and extend the range of those scopes which overlap - * with an active block/fallthrough pair, - * - * bb0 - * ┌──────────────┐ - * │let s = null │ - * │test cond1 │ - * │ │ - * └┬─────────────┘ - * │ bb1 - * ├─►┌───────┐ - * │ │s = {} ├────┐ - * │ └───────┘ │ - * │ bb2 │ - * └─►┌───────┐ │ - * │return;│ │ - * └───────┘ │ - * bb3 │ - * ┌──────────────┐◄┘ - * │test cond2 │ - * │ │ - * └┬─────────────┘ - * │ bb4 - * ├─►┌─────────┐ - * │ │mutate(s)├─┐ - * ▼ └─────────┘ │ - * bb5 │ - * ┌───────────┐ │ - * │return s; │◄──┘ - * └───────────┘ - */ -function useFoo(cond1, cond2) { - let s = null; - if (cond1) { - s = {}; - } else { - return null; - } - - if (cond2) { - mutate(s); - } - - return s; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.expect.md deleted file mode 100644 index c14ae737e544a..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-iife-return-modified-later-logical.expect.md +++ /dev/null @@ -1,29 +0,0 @@ - -## Input - -```javascript -import { getNull } from "shared-runtime"; - -function Component(props) { - const items = (() => { - return getNull() ?? []; - })(); - items.push(props.a); - return items; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ a: {} }], -}; - -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 2:15(3:21) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.expect.md deleted file mode 100644 index df0192db2a006..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-if.expect.md +++ /dev/null @@ -1,41 +0,0 @@ - -## Input - -```javascript -function useFoo({ cond }) { - let items: any = {}; - b0: { - if (cond) { - // Mutable range of `items` begins here, but its reactive scope block - // should be aligned to above the if-branch - items = []; - } else { - break b0; - } - items.push(2); - } - return items; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ cond: true }], - sequentialRenders: [ - { cond: true }, - { cond: true }, - { cond: false }, - { cond: false }, - { cond: true }, - ], -}; - -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 6:11(7:15) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.expect.md deleted file mode 100644 index 62295b9a0ca0a..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-label.expect.md +++ /dev/null @@ -1,40 +0,0 @@ - -## Input - -```javascript -import { arrayPush } from "shared-runtime"; - -function useFoo({ cond, value }) { - let items; - label: { - items = []; - // Mutable range of `items` begins here, but its reactive scope block - // should be aligned to above the label-block - if (cond) break label; - arrayPush(items, value); - } - arrayPush(items, value); - return items; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ cond: true, value: 2 }], - sequentialRenders: [ - { cond: true, value: 2 }, - { cond: true, value: 2 }, - { cond: true, value: 3 }, - { cond: false, value: 3 }, - ], -}; - -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 3:14(4:18) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.expect.md deleted file mode 100644 index 95cdbe5aeea76..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-reactive-scope-overlaps-try.expect.md +++ /dev/null @@ -1,36 +0,0 @@ - -## Input - -```javascript -import { arrayPush } from "shared-runtime"; - -function useFoo({ value }) { - let items = null; - try { - // Mutable range of `items` begins here, but its reactive scope block - // should be aligned to above the try-block - items = []; - arrayPush(items, value); - } catch { - // ignore - } - mutate(items); - return items; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ value: 2 }], - sequentialRenders: [{ value: 2 }, { value: 2 }, { value: 3 }], -}; - -``` - - -## Error - -``` -Invariant: Invalid nesting in program blocks or scopes. Items overlap but are not nested: 4:19(5:22) -``` - - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md new file mode 100644 index 0000000000000..69ae9aa3d6c2a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime"; + +function Foo(props, ref) { + const value = {}; + if (CONST_TRUE) { + mutate(value); + return ; + } + mutate(value); + if (CONST_TRUE) { + return ; + } + return value; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}, { current: "fake-ref-object" }], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime"; + +function Foo(props, ref) { + const $ = _c(5); + let value; + let t0; + if ($[0] !== ref) { + t0 = Symbol.for("react.early_return_sentinel"); + bb0: { + value = {}; + if (CONST_TRUE) { + mutate(value); + t0 = ; + break bb0; + } + + mutate(value); + if (CONST_TRUE) { + const t1 = identity(ref); + let t2; + if ($[3] !== t1) { + t2 = ; + $[3] = t1; + $[4] = t2; + } else { + t2 = $[4]; + } + t0 = t2; + break bb0; + } + } + $[0] = ref; + $[1] = value; + $[2] = t0; + } else { + value = $[1]; + t0 = $[2]; + } + if (t0 !== Symbol.for("react.early_return_sentinel")) { + return t0; + } + return value; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}, { current: "fake-ref-object" }], +}; + +``` + +### Eval output +(kind: ok)
{"ref":{"current":"fake-ref-object"}}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx new file mode 100644 index 0000000000000..106ebd772e611 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-ref-mutable-range.tsx @@ -0,0 +1,19 @@ +import { Stringify, identity, mutate, CONST_TRUE } from "shared-runtime"; + +function Foo(props, ref) { + const value = {}; + if (CONST_TRUE) { + mutate(value); + return ; + } + mutate(value); + if (CONST_TRUE) { + return ; + } + return value; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{}, { current: "fake-ref-object" }], +};