diff --git a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts index 2e7613f0a2adf..b6d4d92fda136 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -96,6 +96,7 @@ import { validatePreservedManualMemoization, validateUseMemo, } from "../Validation"; +import { propagateScopeDependenciesHIR } from "../HIR/PropagateScopeDependenciesHIR"; export type CompilerPipelineValue = | { kind: "ast"; name: string; value: CodegenFunction } @@ -306,6 +307,13 @@ function* runWithEnvironment( }); assertTerminalSuccessorsExist(hir); assertTerminalPredsExist(hir); + + propagateScopeDependenciesHIR(hir); + yield log({ + kind: "hir", + name: "PropagateScopeDependenciesHIR", + value: hir, + }); } const reactiveFunction = buildReactiveFunction(hir); @@ -359,16 +367,15 @@ function* runWithEnvironment( name: "FlattenScopesWithHooks", value: reactiveFunction, }); - } + assertScopeInstructionsWithinScopes(reactiveFunction); - assertScopeInstructionsWithinScopes(reactiveFunction); - - propagateScopeDependencies(reactiveFunction); - yield log({ - kind: "reactive", - name: "PropagateScopeDependencies", - value: reactiveFunction, - }); + propagateScopeDependencies(reactiveFunction); + yield log({ + kind: "reactive", + name: "PropagateScopeDependencies", + value: reactiveFunction, + }); + } pruneNonEscapingScopes(reactiveFunction); yield log({ diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts new file mode 100644 index 0000000000000..40e272c3d7900 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -0,0 +1,436 @@ +import { CompilerError } from "../CompilerError"; +import { isMutable } from "../ReactiveScopes/InferReactiveScopeVariables"; +import { Set_intersect, Set_union, getOrInsertDefault } from "../Utils/utils"; +import { + BlockId, + GeneratedSource, + HIRFunction, + Identifier, + IdentifierId, + InstructionId, + Place, + ReactiveScopeDependency, + ScopeId, +} from "./HIR"; + +type CollectHoistablePropertyLoadsResult = { + nodes: Map; + temporaries: Map; + properties: Map; +}; + +export function collectHoistablePropertyLoads( + fn: HIRFunction, + usedOutsideDeclaringScope: Set +): CollectHoistablePropertyLoadsResult { + const result = new TemporariesSideMap(); + + const functionExprRvals = fn.env.config.enableTreatFunctionDepsAsConditional + ? collectFunctionExpressionRValues(fn) + : new Set(); + const nodes = collectNodes( + fn, + functionExprRvals, + usedOutsideDeclaringScope, + result + ); + deriveNonNull(fn, nodes); + + return { + nodes, + temporaries: result.temporaries, + properties: result.properties, + }; +} + +export type BlockInfo = { + blockId: BlockId; + scope: ScopeId | null; + preds: Set; + assumedNonNullObjects: Set; +}; + +/** + * Tree data structure to dedupe property loads (e.g. a.b.c) + * and make computing sets and intersections simpler. + */ +type RootNode = { + properties: Map; + parent: null; + // Recorded to make later computations simpler + fullPath: ReactiveScopeDependency; + root: Identifier; +}; + +type PropertyLoadNode = + | { + properties: Map; + parent: PropertyLoadNode; + fullPath: ReactiveScopeDependency; + } + | RootNode; + +class Tree { + roots: Map = new Map(); + + #getOrCreateRoot(identifier: Identifier): PropertyLoadNode { + // roots can always be accessed unconditionally in JS + let rootNode = this.roots.get(identifier); + + if (rootNode === undefined) { + rootNode = { + root: identifier, + properties: new Map(), + fullPath: { + identifier, + path: [], + }, + parent: null, + }; + this.roots.set(identifier, rootNode); + } + return rootNode; + } + + static #getOrMakeProperty( + node: PropertyLoadNode, + property: string + ): PropertyLoadNode { + let child = node.properties.get(property); + if (child == null) { + child = { + properties: new Map(), + parent: node, + fullPath: { + identifier: node.fullPath.identifier, + path: node.fullPath.path.concat([property]), + }, + }; + node.properties.set(property, child); + } + return child; + } + + add(n: ReactiveScopeDependency): PropertyLoadNode { + let currNode = this.#getOrCreateRoot(n.identifier); + // We add ReactiveScopeDependencies sequentially (e.g. a.b before a.b.c), + // so subpaths should already exist. + for (let i = 0; i < n.path.length - 1; i++) { + currNode = assertNonNull(currNode.properties.get(n.path[i])); + } + + currNode = Tree.#getOrMakeProperty(currNode, n.path.at(-1)!); + return currNode; + } +} + +/** + * We currently lower function expression dependencies inline before the + * function expression instruction. This causes our HIR to deviate from + * JS specs. + * + * For example, note that instructions 0-2 in the below HIR are incorrectly + * hoisted. + * ```js + * // Input + * function Component(props) { + * const fn = () => cond && read(props.a.b); + * // ... + * } + * + * // HIR: + * [0] $0 = LoadLocal "props" + * [1] $1 = PropertyLoad $0, "a" + * [2] $2 = PropertyLoad $1, "b" + * [3] $3 = FunctionExpression deps=[$2] context=[$0] { + * ... + * } + * + * TODO: rewrite function expression deps + */ +function collectFunctionExpressionRValues(fn: HIRFunction): Set { + const result = new Set(); + const loads = new Map(); + + for (const [_, block] of fn.body.blocks) { + for (const instr of block.instructions) { + if (instr.value.kind === "LoadLocal") { + loads.set(instr.lvalue.identifier.id, instr.value.place.identifier.id); + } else if (instr.value.kind === "PropertyLoad") { + loads.set(instr.lvalue.identifier.id, instr.value.object.identifier.id); + } else if (instr.value.kind === "FunctionExpression") { + for (const dep of instr.value.loweredFunc.dependencies) { + result.add(dep.identifier.id); + } + } + } + } + + // don't iterate newly added objects as optimization + for (const res of result) { + let curr = loads.get(res); + while (curr != null) { + result.add(curr); + curr = loads.get(curr); + } + } + return result; +} + +class TemporariesSideMap { + temporaries: Map = new Map(); + properties: Map = new Map(); + tree: Tree = new Tree(); + + declareTemporary(from: Identifier, to: Identifier): void { + this.temporaries.set(from, to); + } + + declareProperty( + lvalue: Place, + object: Place, + propertyName: string, + shouldDeclare: boolean + ): PropertyLoadNode { + // temporaries contains object if this is a property load chain from a named variable + // Otherwise, there is a non-trivial expression + const resolvedObject = + this.temporaries.get(object.identifier) ?? object.identifier; + + const resolvedDependency = this.properties.get(resolvedObject); + let property: ReactiveScopeDependency; + if (resolvedDependency == null) { + property = { + identifier: resolvedObject, + path: [propertyName], + }; + } else { + property = { + identifier: resolvedDependency.identifier, + path: [...resolvedDependency.path, propertyName], + }; + } + + if (shouldDeclare) { + this.properties.set(lvalue.identifier, property); + } + return this.tree.add(property); + } +} + +function collectNodes( + fn: HIRFunction, + functionExprRvals: Set, + usedOutsideDeclaringScope: Set, + c: TemporariesSideMap +): Map { + const nodes = new Map(); + const scopeStartBlocks = new Map(); + for (const [blockId, block] of fn.body.blocks) { + const assumedNonNullObjects = new Set(); + for (const instr of block.instructions) { + const { value, lvalue } = instr; + const usedOutside = usedOutsideDeclaringScope.has(lvalue.identifier.id); + if (value.kind === "PropertyLoad") { + const propertyNode = c.declareProperty( + lvalue, + value.object, + value.property, + !usedOutside + ); + if ( + !functionExprRvals.has(lvalue.identifier.id) && + !isMutable(instr, value.object) + ) { + let curr = propertyNode.parent; + while (curr != null) { + assumedNonNullObjects.add(curr); + curr = curr.parent; + } + } + } else if (value.kind === "LoadLocal") { + if ( + lvalue.identifier.name == null && + value.place.identifier.name !== null && + !usedOutside + ) { + c.declareTemporary(lvalue.identifier, value.place.identifier); + } + } + /** + * Note that we do not record StoreLocals as this runs after ExitSSA. + * As a result, an expression like `(a ?? b).c` is represented as two + * StoreLocals to the same identifier id. + */ + } + + if ( + block.terminal.kind === "scope" || + block.terminal.kind === "pruned-scope" + ) { + scopeStartBlocks.set(block.terminal.block, block.terminal.scope.id); + } + + nodes.set(blockId, { + blockId, + scope: scopeStartBlocks.get(blockId) ?? null, + preds: block.preds, + assumedNonNullObjects, + }); + } + return nodes; +} + +function deriveNonNull(fn: HIRFunction, nodes: Map): void { + // block -> successors sidemap + const succ = new Map>(); + const terminalPreds = new Set(); + + for (const [blockId, block] of fn.body.blocks) { + for (const pred of block.preds) { + const predVal = getOrInsertDefault(succ, pred, new Set()); + predVal.add(blockId); + } + if (block.terminal.kind === "throw" || block.terminal.kind === "return") { + terminalPreds.add(blockId); + } + } + + function recursivelyDeriveNonNull( + nodeId: BlockId, + kind: "succ" | "pred", + traversalState: Map, + result: Map> + ): boolean { + if (traversalState.has(nodeId)) { + return false; + } + traversalState.set(nodeId, "active"); + + const node = nodes.get(nodeId); + if (node == null) { + CompilerError.invariant(false, { + reason: `Bad node ${nodeId}, kind: ${kind}`, + loc: GeneratedSource, + }); + } + const neighbors = Array.from( + kind === "succ" ? succ.get(nodeId) ?? [] : node.preds + ); + + let changed = false; + for (const pred of neighbors) { + if (!traversalState.has(pred)) { + const neighborChanged = recursivelyDeriveNonNull( + pred, + kind, + traversalState, + result + ); + changed ||= neighborChanged; + } + } + // active neighbors can be filtered out as we're solving for the following + // relation. + // X = Intersect(X_neighbors, X) + // non-active neighbors with no recorded results can occur due to backedges. + // it's not safe to assume they can be filtered out (e.g. not intersected) + const neighborAccesses = Set_intersect([ + ...(Array.from(neighbors) + .filter((n) => traversalState.get(n) !== "active") + .map((n) => result.get(n) ?? new Set()) as Array< + Set + >), + ]); + + const prevSize = result.get(nodeId)?.size; + // const prevPrinted = [...(result.get(nodeId) ?? [])].map( + // printDependencyNode + // ); + result.set(nodeId, Set_union(node.assumedNonNullObjects, neighborAccesses)); + traversalState.set(nodeId, "done"); + + // const newPrinted = [...(result.get(nodeId) ?? [])].map(printDependencyNode); + // llog(" - ", nodeId, prevPrinted, newPrinted); + + changed ||= prevSize !== result.get(nodeId)!.size; + CompilerError.invariant( + prevSize == null || prevSize <= result.get(nodeId)!.size, + { + reason: "[CollectHoistablePropertyLoads] Nodes shrank!", + description: `${nodeId} ${kind} ${prevSize} ${ + result.get(nodeId)!.size + }`, + loc: GeneratedSource, + } + ); + return changed; + } + const fromEntry = new Map>(); + const fromExit = new Map>(); + let changed = true; + const traversalState = new Map(); + const reversedBlocks = [...fn.body.blocks]; + reversedBlocks.reverse(); + let i = 0; + + while (changed) { + i++; + changed = false; + for (const [blockId] of fn.body.blocks) { + const changed_ = recursivelyDeriveNonNull( + blockId, + "pred", + traversalState, + fromEntry + ); + changed ||= changed_; + } + traversalState.clear(); + for (const [blockId] of reversedBlocks) { + const changed_ = recursivelyDeriveNonNull( + blockId, + "succ", + traversalState, + fromExit + ); + changed ||= changed_; + } + traversalState.clear(); + } + + // TODO: I can't come up with a case that requires fixed-point iteration + CompilerError.invariant(i === 2, { + reason: "require fixed-point iteration", + loc: GeneratedSource, + }); + + CompilerError.invariant( + fromEntry.size === fromExit.size && fromEntry.size === nodes.size, + { + reason: + "bad sizes after calculating fromEntry + fromExit " + + `${fromEntry.size} ${fromExit.size} ${nodes.size}`, + loc: GeneratedSource, + } + ); + + for (const [id, node] of nodes) { + node.assumedNonNullObjects = Set_union( + assertNonNull(fromEntry.get(id)), + assertNonNull(fromExit.get(id)) + ); + } +} + +export function assertNonNull, U>( + value: T | null | undefined, + source?: string +): T { + CompilerError.invariant(value != null, { + reason: "Unexpected null", + description: source != null ? `(from ${source})` : null, + loc: GeneratedSource, + }); + return value; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts new file mode 100644 index 0000000000000..0f37d91b64c6f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts @@ -0,0 +1,352 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import { CompilerError } from "../CompilerError"; +import { GeneratedSource, Identifier, ReactiveScopeDependency } from "../HIR"; +import { printIdentifier } from "../HIR/PrintHIR"; +import { ReactiveScopePropertyDependency } from "../ReactiveScopes/DeriveMinimalDependencies"; + +const ENABLE_DEBUG_INVARIANTS = true; + +/* + * Finalizes a set of ReactiveScopeDependencies to produce a set of minimal unconditional + * dependencies, preserving granular accesses when possible. + * + * Correctness properties: + * - All dependencies to a ReactiveBlock must be tracked. + * We can always truncate a dependency's path to a subpath, due to Forget assuming + * deep immutability. If the value produced by a subpath has not changed, then + * dependency must have not changed. + * i.e. props.a === $[..] implies props.a.b === $[..] + * + * Note the inverse is not true, but this only means a false positive (we run the + * reactive block more than needed). + * i.e. props.a !== $[..] does not imply props.a.b !== $[..] + * + * - The dependencies of a finalized ReactiveBlock must be all safe to access + * unconditionally (i.e. preserve program semantics with respect to nullthrows). + * If a dependency is only accessed within a conditional, we must track the nearest + * unconditionally accessed subpath instead. + * @param initialDeps + * @returns + */ +export class ReactiveScopeDependencyTreeHIR { + #roots: Map = new Map(); + + #getOrCreateRoot(identifier: Identifier, isNonNull: boolean): DependencyNode { + // roots can always be accessed unconditionally in JS + let rootNode = this.#roots.get(identifier); + + if (rootNode === undefined) { + rootNode = { + properties: new Map(), + accessType: isNonNull + ? PropertyAccessType.NonNullAccess + : PropertyAccessType.MaybeNullAccess, + }; + this.#roots.set(identifier, rootNode); + } + return rootNode; + } + + addDependency(dep: ReactiveScopePropertyDependency): void { + const { path, optionalPath } = dep; + let currNode = this.#getOrCreateRoot(dep.identifier, false); + + const accessType = PropertyAccessType.MaybeNullAccess; + + currNode.accessType = merge(currNode.accessType, accessType); + + for (const property of path) { + // all properties read 'on the way' to a dependency are marked as 'access' + let currChild = getOrMakeProperty(currNode, property); + currChild.accessType = merge(currChild.accessType, accessType); + currNode = currChild; + } + + if (optionalPath.length === 0) { + /* + * If this property does not have a conditional path (i.e. a.b.c), the + * final property node should be marked as an conditional/unconditional + * `dependency` as based on control flow. + */ + currNode.accessType = merge( + currNode.accessType, + PropertyAccessType.MaybeNullDependency + ); + } else { + /* + * Technically, we only depend on whether unconditional path `dep.path` + * is nullish (not its actual value). As long as we preserve the nullthrows + * behavior of `dep.path`, we can keep it as an access (and not promote + * to a dependency). + * See test `reduce-reactive-cond-memberexpr-join` for example. + */ + + /* + * If this property has an optional path (i.e. a?.b.c), all optional + * nodes should be marked accordingly. + */ + for (const property of optionalPath) { + let currChild = getOrMakeProperty(currNode, property); + currChild.accessType = merge( + currChild.accessType, + PropertyAccessType.MaybeNullAccess + ); + currNode = currChild; + } + + // The final node should be marked as a conditional dependency. + currNode.accessType = merge( + currNode.accessType, + PropertyAccessType.MaybeNullDependency + ); + } + } + + markNodesNonNull(dep: ReactiveScopePropertyDependency): void { + const accessType = PropertyAccessType.NonNullAccess; + let currNode = this.#roots.get(dep.identifier); + + let cursor = 0; + while (currNode != null && cursor < dep.path.length) { + currNode.accessType = merge(currNode.accessType, accessType); + currNode = currNode.properties.get(dep.path[cursor++]); + } + if (currNode != null) { + currNode.accessType = merge(currNode.accessType, accessType); + } + } + /** + * Derive a set of minimal dependencies that are safe to + * access unconditionally (with respect to nullthrows behavior) + */ + deriveMinimalDependencies(): Set { + const results = new Set(); + for (const [rootId, rootNode] of this.#roots.entries()) { + if (ENABLE_DEBUG_INVARIANTS) { + assertWellFormedTree(rootNode); + } + const deps = deriveMinimalDependenciesInSubtree(rootNode, []); + + for (const dep of deps) { + results.add({ + identifier: rootId, + path: dep.path, + }); + } + } + + return results; + } + + /* + * Prints dependency tree to string for debugging. + * @param includeAccesses + * @returns string representation of DependencyTree + */ + printDeps(includeAccesses: boolean): string { + let res: Array> = []; + + for (const [rootId, rootNode] of this.#roots.entries()) { + const rootResults = printSubtree(rootNode, includeAccesses).map( + (result) => `${printIdentifier(rootId)}.${result}` + ); + res.push(rootResults); + } + return res.flat().join("\n"); + } +} + +enum PropertyAccessType { + MaybeNullAccess = "MaybeNullAccess", + NonNullAccess = "NonNullAccess", + MaybeNullDependency = "MaybeNullDependency", + NonNullDependency = "NonNullDependency", +} + +const MIN_ACCESS_TYPE = PropertyAccessType.MaybeNullAccess; +function isNonNull(access: PropertyAccessType): boolean { + return ( + access === PropertyAccessType.NonNullAccess || + access === PropertyAccessType.NonNullDependency + ); +} +function isDependency(access: PropertyAccessType): boolean { + return ( + access === PropertyAccessType.MaybeNullDependency || + access === PropertyAccessType.NonNullDependency + ); +} + +function merge( + access1: PropertyAccessType, + access2: PropertyAccessType +): PropertyAccessType { + const resultIsNonNull = isNonNull(access1) || isNonNull(access2); + const resultIsDependency = isDependency(access1) || isDependency(access2); + + /* + * Straightforward merge. + * This can be represented as bitwise OR, but is written out for readability + * + * Observe that `NonNullAccess | MaybeNullDependency` produces an + * unconditionally accessed conditional dependency. We currently use these + * as we use unconditional dependencies. (i.e. to codegen change variables) + */ + if (resultIsNonNull) { + if (resultIsDependency) { + return PropertyAccessType.NonNullDependency; + } else { + return PropertyAccessType.NonNullAccess; + } + } else { + if (resultIsDependency) { + return PropertyAccessType.MaybeNullDependency; + } else { + return PropertyAccessType.MaybeNullAccess; + } + } +} + +type DependencyNode = { + properties: Map; + accessType: PropertyAccessType; +}; + +type ReduceResultNode = { + path: Array; +}; +function assertWellFormedTree(node: DependencyNode): void { + let nonNullInChildren = false; + for (const childNode of node.properties.values()) { + assertWellFormedTree(childNode); + nonNullInChildren ||= isNonNull(childNode.accessType); + } + if (nonNullInChildren) { + CompilerError.invariant(isNonNull(node.accessType), { + reason: + "[DeriveMinimialDependencies] Not well formed tree, unexpected nonnull node", + description: node.accessType, + loc: GeneratedSource, + }); + } +} +function deriveMinimalDependenciesInSubtree( + node: DependencyNode, + path: Array +): Array { + if (isDependency(node.accessType)) { + /** + * If this node is a dependency, we truncate the subtree + * and return this node. e.g. deps=[`obj.a`, `obj.a.b`] + * reduces to deps=[`obj.a`] + */ + return [{ path }]; + } else { + if (isNonNull(node.accessType)) { + /* + * Only recurse into subtree dependencies if this node + * is known to be non-null. + */ + const result: Array = []; + for (const [childName, childNode] of node.properties) { + result.push( + ...deriveMinimalDependenciesInSubtree(childNode, [...path, childName]) + ); + } + return result; + } else { + /* + * This only occurs when this subtree contains a dependency, + * but this node is potentially nullish. As we currently + * don't record optional property paths as scope dependencies, + * we truncate and record this node as a dependency. + */ + return [{ path }]; + } + } +} + +/* + * Demote all unconditional accesses + dependencies in subtree to the + * conditional equivalent, mutating subtree in place. + * @param subtree unconditional node representing a subtree of dependencies + */ +function _demoteSubtreeToConditional(subtree: DependencyNode): void { + const stack: Array = [subtree]; + + let node; + while ((node = stack.pop()) !== undefined) { + const { accessType, properties } = node; + if (!isNonNull(accessType)) { + // A conditionally accessed node should not have unconditional children + continue; + } + node.accessType = isDependency(accessType) + ? PropertyAccessType.MaybeNullDependency + : PropertyAccessType.MaybeNullAccess; + + for (const childNode of properties.values()) { + if (isNonNull(accessType)) { + /* + * No conditional node can have an unconditional node as a child, so + * we only process childNode if it is unconditional + */ + stack.push(childNode); + } + } + } +} + +function printSubtree( + node: DependencyNode, + includeAccesses: boolean +): Array { + const results: Array = []; + for (const [propertyName, propertyNode] of node.properties) { + if (includeAccesses || isDependency(propertyNode.accessType)) { + results.push(`${propertyName} (${propertyNode.accessType})`); + } + const propertyResults = printSubtree(propertyNode, includeAccesses); + results.push( + ...propertyResults.map((result) => `${propertyName}.${result}`) + ); + } + return results; +} + +function getOrMakeProperty( + node: DependencyNode, + property: string +): DependencyNode { + let child = node.properties.get(property); + if (child == null) { + child = { + properties: new Map(), + accessType: MIN_ACCESS_TYPE, + }; + node.properties.set(property, child); + } + return child; +} + +function mapNonNull, V, U>( + arr: Array, + fn: (arg0: U) => T | undefined | null +): Array | null { + const result = []; + for (let i = 0; i < arr.length; i++) { + const element = fn(arr[i]); + if (element) { + result.push(element); + } else { + return null; + } + } + return result; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts new file mode 100644 index 0000000000000..b6ede41fac03a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -0,0 +1,578 @@ +import { + IdentifierId, + ScopeId, + HIRFunction, + Place, + Instruction, + ReactiveScopeDependency, + BlockId, + Identifier, + ReactiveScope, + isObjectMethodType, + isRefValueType, + isUseRefType, + makeInstructionId, + InstructionId, + InstructionKind, + GeneratedSource, +} from "./HIR"; +import { + BlockInfo, + collectHoistablePropertyLoads, +} from "./CollectHoistablePropertyLoads"; +import { + NO_OP, + ScopeBlockTraversal, + eachInstructionOperand, + eachInstructionValueOperand, + eachPatternOperand, + eachTerminalOperand, +} from "./visitors"; +import { ReactiveScopeDependencyTreeHIR } from "./DeriveMinimalDependenciesHIR"; +import { Stack, empty } from "../Utils/Stack"; +import { CompilerError } from "../CompilerError"; + +export function llog(..._args: any): void { + console.log(..._args); +} +type TemporariesUsedOutsideDefiningScope = { + /* + * tracks all relevant temporary declarations (currently LoadLocal and PropertyLoad) + * and the scope where they are defined + */ + declarations: Map; + // temporaries used outside of their defining scope + usedOutsideDeclaringScope: Set; +}; + +function findPromotedTemporaries( + fn: HIRFunction, + { + declarations, + usedOutsideDeclaringScope, + }: TemporariesUsedOutsideDefiningScope +): void { + const prunedScopes = new Set(); + const scopeTraversal = new ScopeBlockTraversal( + null, + (scope, pruned) => { + if (pruned) { + prunedScopes.add(scope.id); + } + return null; + }, + NO_OP + ); + + function handlePlace(place: Place): void { + const declaringScope = declarations.get(place.identifier.id); + if ( + declaringScope != null && + scopeTraversal.activeScopes.indexOf(declaringScope) === -1 && + !prunedScopes.has(declaringScope) + ) { + // Declaring scope is not active === used outside declaring scope + usedOutsideDeclaringScope.add(place.identifier.id); + } + } + + function handleInstruction(instr: Instruction): void { + const scope = scopeTraversal.activeScopes.at(-1); + if (scope === undefined || prunedScopes.has(scope)) { + return; + } + switch (instr.value.kind) { + case "LoadLocal": + case "LoadContext": + case "PropertyLoad": { + declarations.set(instr.lvalue.identifier.id, scope); + break; + } + default: { + break; + } + } + } + + for (const [_, block] of fn.body.blocks) { + scopeTraversal.handleBlock(block); + for (const instr of block.instructions) { + for (const place of eachInstructionOperand(instr)) { + handlePlace(place); + } + handleInstruction(instr); + } + + for (const place of eachTerminalOperand(block.terminal)) { + handlePlace(place); + } + } +} + +type Decl = { + id: InstructionId; + scope: Stack; +}; + +class Context { + #temporariesUsedOutsideScope: Set; + #declarations: Map = new Map(); + #reassignments: Map = new Map(); + // Reactive dependencies used in the current reactive scope. + #dependencies: Array = []; + /* + * We keep a sidemap for temporaries created by PropertyLoads, and do + * not store any control flow (i.e. #inConditionalWithinScope) here. + * - a ReactiveScope (A) containing a PropertyLoad may differ from the + * ReactiveScope (B) that uses the produced temporary. + * - codegen will inline these PropertyLoads back into scope (B) + */ + #properties: Map; + #temporaries: Map; + #scopes: Stack = empty(); + deps: Map> = new Map(); + + get properties(): Map { + return this.#properties; + } + constructor( + temporariesUsedOutsideScope: Set, + temporaries: Map, + properties: Map + ) { + this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope; + this.#temporaries = temporaries; + this.#properties = properties; + } + + static enterScope( + scope: ReactiveScope, + _pruned: boolean, + context: Context + ): { previousDeps: Array } { + // Save context of previous scope + const previousDeps = context.#dependencies; + + /* + * Set context for new scope + */ + context.#dependencies = []; + context.#scopes = context.#scopes.push(scope); + return { previousDeps }; + } + + static exitScope( + scope: ReactiveScope, + pruned: boolean, + context: Context, + state: { previousDeps: Array } + ): void { + const scopedDependencies = context.#dependencies; + + // Restore context of previous scope + context.#scopes = context.#scopes.pop(); + context.#dependencies = state.previousDeps; + + /* + * Collect dependencies we recorded for the exiting scope and propagate + * them upward using the same rules as normal dependency collection. + * Child scopes may have dependencies on values created within the outer + * scope, which necessarily cannot be dependencies of the outer scope. + */ + for (const dep of scopedDependencies) { + if (context.#checkValidDependency(dep)) { + context.#dependencies.push(dep); + } + } + + if (!pruned) { + context.deps.set(scope, scopedDependencies); + } + } + + isUsedOutsideDeclaringScope(place: Place): boolean { + return this.#temporariesUsedOutsideScope.has(place.identifier.id); + } + + /* + * Records where a value was declared, and optionally, the scope where the value originated from. + * This is later used to determine if a dependency should be added to a scope; if the current + * scope we are visiting is the same scope where the value originates, it can't be a dependency + * on itself. + */ + declare(identifier: Identifier, decl: Decl): void { + if (!this.#declarations.has(identifier.id)) { + this.#declarations.set(identifier.id, decl); + } + this.#reassignments.set(identifier, decl); + } + + resolveTemporary(place: Place): Identifier { + return this.#temporaries.get(place.identifier) ?? place.identifier; + } + + getProperty(object: Place, property: string): ReactiveScopeDependency { + return this.#getProperty(object, property, false); + } + + #getProperty( + object: Place, + property: string, + _isConditional: boolean + ): ReactiveScopeDependency { + /** + Example 1: + $0 = LoadLocal x + $1 = PropertyLoad $0.y + resolvedObject = x, resolvedDependency = null + + Example 2: + $0 = LoadLocal x + $1 = PropertyLoad $0.y + $2 = PropertyLoad $1.z + resolvedObject = null, resolvedDependency = x.y + + Example 3: + $0 = Call(...) + $1 = PropertyLoad $0.y + resolvedObject = null, resolvedDependency = null + */ + const resolvedObject = this.resolveTemporary(object); + const resolvedDependency = this.#properties.get(resolvedObject); + let objectDependency: ReactiveScopeDependency; + /* + * (1) Create the base property dependency as either a LoadLocal (from a temporary) + * or a deep copy of an existing property dependency. + */ + if (resolvedDependency === undefined) { + objectDependency = { + identifier: resolvedObject, + path: [], + }; + } else { + objectDependency = { + identifier: resolvedDependency.identifier, + path: [...resolvedDependency.path], + }; + } + + objectDependency.path.push(property); + return objectDependency; + } + + // Checks if identifier is a valid dependency in the current scope + #checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean { + // ref.current access is not a valid dep + if ( + isUseRefType(maybeDependency.identifier) && + maybeDependency.path.at(0) === "current" + ) { + return false; + } + + // ref value is not a valid dep + if (isRefValueType(maybeDependency.identifier)) { + return false; + } + + /* + * object methods are not deps because they will be codegen'ed back in to + * the object literal. + */ + if (isObjectMethodType(maybeDependency.identifier)) { + return false; + } + + const identifier = maybeDependency.identifier; + /* + * If this operand is used in a scope, has a dynamic value, and was defined + * before this scope, then its a dependency of the scope. + */ + const currentDeclaration = + this.#reassignments.get(identifier) ?? + this.#declarations.get(identifier.id); + const currentScope = this.currentScope.value; + return ( + currentScope != null && + currentDeclaration !== undefined && + currentDeclaration.id < currentScope.range.start + ); + } + + #isScopeActive(scope: ReactiveScope): boolean { + if (this.#scopes === null) { + return false; + } + return this.#scopes.find((state) => state === scope); + } + + get currentScope(): Stack { + return this.#scopes; + } + + visitOperand(place: Place): void { + const resolved = this.resolveTemporary(place); + /* + * if this operand is a temporary created for a property load, try to resolve it to + * the expanded Place. Fall back to using the operand as-is. + */ + + let dependency: ReactiveScopeDependency | null = null; + if (resolved.name === null) { + const propertyDependency = this.#properties.get(resolved); + if (propertyDependency !== undefined) { + dependency = { ...propertyDependency }; + } + } + // console.log( + // `resolving ${place.identifier.id} -> ${dependency ? printDependency(dependency) : ""}` + // ); + this.visitDependency( + dependency ?? { + identifier: resolved, + path: [], + } + ); + } + + visitProperty(object: Place, property: string): void { + const nextDependency = this.#getProperty(object, property, false); + // if (object.identifier.id === 32) { + // console.log(printDependency(nextDependency)); + // } + this.visitDependency(nextDependency); + } + + visitDependency(maybeDependency: ReactiveScopeDependency): void { + /* + * Any value used after its originally defining scope has concluded must be added as an + * output of its defining scope. Regardless of whether its a const or not, + * some later code needs access to the value. If the current + * scope we are visiting is the same scope where the value originates, it can't be a dependency + * on itself. + */ + + /* + * if originalDeclaration is undefined here, then this is not a local var + * (all decls e.g. `let x;` should be initialized in BuildHIR) + */ + const originalDeclaration = this.#declarations.get( + maybeDependency.identifier.id + ); + if ( + originalDeclaration !== undefined && + originalDeclaration.scope.value !== null + ) { + originalDeclaration.scope.each((scope) => { + if (!this.#isScopeActive(scope)) { + scope.declarations.set(maybeDependency.identifier.id, { + identifier: maybeDependency.identifier, + scope: originalDeclaration.scope.value!, + }); + } + }); + } + + if (this.#checkValidDependency(maybeDependency)) { + this.#dependencies.push(maybeDependency); + } + } + + /* + * Record a variable that is declared in some other scope and that is being reassigned in the + * current one as a {@link ReactiveScope.reassignments} + */ + visitReassignment(place: Place): void { + const currentScope = this.currentScope.value; + if ( + currentScope != null && + !Array.from(currentScope.reassignments).some( + (identifier) => identifier.id === place.identifier.id + ) && + this.#checkValidDependency({ identifier: place.identifier, path: [] }) + ) { + currentScope.reassignments.add(place.identifier); + } + } +} + +function handleInstruction(instr: Instruction, context: Context) { + const { id, value, lvalue } = instr; + if (value.kind === "LoadLocal") { + if ( + value.place.identifier.name === null || + lvalue.identifier.name !== null || + context.isUsedOutsideDeclaringScope(lvalue) + ) { + context.visitOperand(value.place); + } + } else if (value.kind === "PropertyLoad") { + if (context.isUsedOutsideDeclaringScope(lvalue)) { + context.visitProperty(value.object, value.property); + } else { + const nextDependency = context.getProperty(value.object, value.property); + context.properties.set(lvalue.identifier, nextDependency); + } + } else if (value.kind === "StoreLocal") { + context.visitOperand(value.value); + if (value.lvalue.kind === InstructionKind.Reassign) { + context.visitReassignment(value.lvalue.place); + } + context.declare(value.lvalue.place.identifier, { + id, + scope: context.currentScope, + }); + } else if (value.kind === "DeclareLocal" || value.kind === "DeclareContext") { + /* + * Some variables may be declared and never initialized. We need + * to retain (and hoist) these declarations if they are included + * in a reactive scope. One approach is to simply add all `DeclareLocal`s + * as scope declarations. + */ + + /* + * We add context variable declarations here, not at `StoreContext`, since + * context Store / Loads are modeled as reads and mutates to the underlying + * variable reference (instead of through intermediate / inlined temporaries) + */ + context.declare(value.lvalue.place.identifier, { + id, + scope: context.currentScope, + }); + } else if (value.kind === "Destructure") { + context.visitOperand(value.value); + for (const place of eachPatternOperand(value.lvalue.pattern)) { + if (value.lvalue.kind === InstructionKind.Reassign) { + context.visitReassignment(place); + } + context.declare(place.identifier, { + id, + scope: context.currentScope, + }); + } + } else { + for (const operand of eachInstructionValueOperand(value)) { + context.visitOperand(operand); + } + } + + context.declare(lvalue.identifier, { + id, + scope: context.currentScope, + }); +} + +function collectDependencies( + fn: HIRFunction, + usedOutsideDeclaringScope: Set, + temporaries: Map, + properties: Map +) { + const context = new Context( + usedOutsideDeclaringScope, + temporaries, + properties + ); + + for (const param of fn.params) { + if (param.kind === "Identifier") { + context.declare(param.identifier, { + id: makeInstructionId(0), + scope: empty(), + }); + } else { + context.declare(param.place.identifier, { + id: makeInstructionId(0), + scope: empty(), + }); + } + } + + type ScopeTraversalContext = { previousDeps: Array }; + const scopeTraversal = new ScopeBlockTraversal< + Context, + ScopeTraversalContext + >(context, Context.enterScope, Context.exitScope); + + // TODO don't count optional load rvals as dep (e.g. collectOptionalLoadRValues(...)) + for (const [_, block] of fn.body.blocks) { + // Handle scopes that begin or end at this block + scopeTraversal.handleBlock(block); + + for (const instr of block.instructions) { + handleInstruction(instr, context); + } + for (const place of eachTerminalOperand(block.terminal)) { + context.visitOperand(place); + } + } + return context.deps; +} + +/** + * Compute the set of hoistable property reads. + */ +function recordHoistablePropertyReads( + nodes: Map, + scopeId: ScopeId, + tree: ReactiveScopeDependencyTreeHIR +): void { + let nonNullObjects: Array | null = null; + for (const [_blockId, node] of nodes) { + if (node.scope === scopeId) { + nonNullObjects = [...node.assumedNonNullObjects].map((n) => n.fullPath); + break; + } + } + CompilerError.invariant(nonNullObjects != null, { + reason: "[PropagateScopeDependencies] Scope not found in tracked blocks", + loc: GeneratedSource, + }); + + for (const node of nonNullObjects) { + tree.markNodesNonNull({ + ...node, + optionalPath: [], + }); + } +} + +export function propagateScopeDependenciesHIR(fn: HIRFunction): void { + const escapingTemporaries: TemporariesUsedOutsideDefiningScope = { + declarations: new Map(), + usedOutsideDeclaringScope: new Set(), + }; + findPromotedTemporaries(fn, escapingTemporaries); + const { nodes, temporaries, properties } = collectHoistablePropertyLoads( + fn, + escapingTemporaries.usedOutsideDeclaringScope + ); + + const scopeDeps = collectDependencies( + fn, + escapingTemporaries.usedOutsideDeclaringScope, + temporaries, + properties + ); + + /** + * Derive the minimal set of hoistable dependencies for each scope. + */ + for (const [scope, deps] of scopeDeps) { + const tree = new ReactiveScopeDependencyTreeHIR(); + + /** + * Step 1: Add every dependency used by this scope (e.g. `a.b.c`) + */ + for (const dep of deps) { + tree.addDependency({ ...dep, optionalPath: [] }); + } + /** + * Step 2: Mark "hoistable" property reads, i.e. ones that will + * unconditionally run, given the basic block in which the scope + * begins. + */ + recordHoistablePropertyReads(nodes, scope.id, tree); + scope.dependencies = tree.deriveMinimalDependencies(); + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts index beda6e4a2054b..3c4903cbf23e3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -5,8 +5,10 @@ * LICENSE file in the root directory of this source tree. */ +import { CompilerError } from ".."; import { assertExhaustive } from "../Utils/utils"; import { + BasicBlock, BlockId, Instruction, InstructionValue, @@ -14,7 +16,9 @@ import { Pattern, Place, ReactiveInstruction, + ReactiveScope, ReactiveValue, + ScopeId, SpreadPattern, Terminal, } from "./HIR"; @@ -1147,3 +1151,112 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable { } } } + + +export const NO_OP = () => null; + +/** + * Helper class for traversing scope blocks in HIR-form. + */ +export class ScopeBlockTraversal { + #blockInfos: Map< + BlockId, + | { + kind: "end"; + scope: ReactiveScope; + pruned: boolean; + state: TState; + } + | { + kind: "begin"; + scope: ReactiveScope; + pruned: boolean; + fallthrough: BlockId; + } + > = new Map(); + #context: TContext; + #enterCallback: ( + scope: ReactiveScope, + pruned: boolean, + context: TContext + ) => TState; + #exitCallback: ( + scope: ReactiveScope, + pruned: boolean, + context: TContext, + state: TState + ) => void; + activeScopes: Array = []; + + constructor( + context: TContext, + enter: (scope: ReactiveScope, pruned: boolean, context: TContext) => TState, + exit: ( + scope: ReactiveScope, + pruned: boolean, + context: TContext, + state: TState + ) => void + ) { + this.#context = context; + this.#enterCallback = enter ?? null; + this.#exitCallback = exit ?? null; + } + + // Handle scopes that begin or end at the start of the given block, + // invoking scope enter/exit callbacks as applicable + handleBlock(block: BasicBlock): void { + const blockInfo = this.#blockInfos.get(block.id); + if (blockInfo?.kind === "begin") { + this.#blockInfos.delete(block.id); + this.activeScopes.push(blockInfo.scope.id); + const state = this.#enterCallback( + blockInfo.scope, + blockInfo.pruned, + this.#context + ); + + CompilerError.invariant(!this.#blockInfos.has(blockInfo.fallthrough), { + reason: "Expected unique scope blocks and fallthroughs", + loc: blockInfo.scope.loc, + }); + this.#blockInfos.set(blockInfo.fallthrough, { + kind: "end", + scope: blockInfo.scope, + pruned: blockInfo.pruned, + state, + }); + } else if (blockInfo?.kind === "end") { + this.#blockInfos.delete(block.id); + const top = this.activeScopes.at(-1); + CompilerError.invariant(blockInfo.scope.id === top, { + reason: + "Expected traversed block fallthrough to match top-most active scope", + loc: block.instructions[0]?.loc ?? block.terminal.id, + }); + this.activeScopes.pop(); + this.#exitCallback?.( + blockInfo.scope, + blockInfo.pruned, + this.#context, + blockInfo.state + ); + } + + if ( + block.terminal.kind === "scope" || + block.terminal.kind === "pruned-scope" + ) { + CompilerError.invariant(!this.#blockInfos.has(block.terminal.block), { + reason: "Expected unique scope blocks and fallthroughs", + loc: block.terminal.loc, + }); + this.#blockInfos.set(block.terminal.block, { + kind: "begin", + scope: block.terminal.scope, + pruned: block.terminal.kind === "pruned-scope", + fallthrough: block.terminal.fallthrough, + }); + } + } +} \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts index 0bff894cea1b5..968317330addd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/DeriveMinimalDependencies.ts @@ -24,6 +24,9 @@ import { assertExhaustive } from "../Utils/utils"; * path: ['b', 'c'], * optionalPath: ['d', 'e', 'f']. * } + * TODO remove optionalPath or rewrite our PropagateScopeDeps logic to understand + * optional load expressions + * */ export type ReactiveScopePropertyDependency = ReactiveScopeDependency & { optionalPath: Array; 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 921a6bf2097d0..c0d4acdee8740 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -84,15 +84,32 @@ export function getOrInsertDefault( } export function Set_union(a: Set, b: Set): Set { - const union = new Set(); - for (const item of a) { - if (b.has(item)) { - union.add(item); - } + const union = new Set(a); + for (const item of b) { + union.add(item); } return union; } +export function Set_intersect(sets: Array>): Set { + if (sets.length === 0 || sets.some((s) => s.size === 0)) { + return new Set(); + } else if (sets.length === 1) { + return new Set(sets[0]); + } + const result: Set = new Set(); + const first = sets[0]; + outer: for (const e of first) { + for (let i = 1; i < sets.length; i++) { + if (!sets[i].has(e)) { + continue outer; + } + } + result.add(e); + } + return result; +} + export function nonNull, U>( value: T | null | undefined ): value is T { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md index 2c6b373f0c45c..18a4418f205b3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md @@ -33,9 +33,14 @@ import { c as _c } from "react/compiler-runtime"; /** * props.b *does* influence `a` */ function Component(props) { - const $ = _c(2); + const $ = _c(5); let a; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { a = []; a.push(props.a); bb0: { @@ -47,10 +52,13 @@ function Component(props) { } a.push(props.d); - $[0] = props; - $[1] = a; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; } else { - a = $[1]; + a = $[4]; } return a; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md index 04012db573309..8e681e862c125 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md @@ -70,10 +70,10 @@ import { c as _c } from "react/compiler-runtime"; /** * props.b does *not* influence `a` */ function ComponentA(props) { - const $ = _c(3); + const $ = _c(5); let a_DEBUG; let t0; - if ($[0] !== props) { + if ($[0] !== props.a || $[1] !== props.b || $[2] !== props.d) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { a_DEBUG = []; @@ -85,12 +85,14 @@ function ComponentA(props) { a_DEBUG.push(props.d); } - $[0] = props; - $[1] = a_DEBUG; - $[2] = t0; + $[0] = props.a; + $[1] = props.b; + $[2] = props.d; + $[3] = a_DEBUG; + $[4] = t0; } else { - a_DEBUG = $[1]; - t0 = $[2]; + a_DEBUG = $[3]; + t0 = $[4]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; @@ -102,9 +104,14 @@ function ComponentA(props) { * props.b *does* influence `a` */ function ComponentB(props) { - const $ = _c(2); + const $ = _c(5); let a; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { a = []; a.push(props.a); if (props.b) { @@ -112,10 +119,13 @@ function ComponentB(props) { } a.push(props.d); - $[0] = props; - $[1] = a; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; } else { - a = $[1]; + a = $[4]; } return a; } @@ -124,10 +134,15 @@ function ComponentB(props) { * props.b *does* influence `a`, but only in a way that is never observable */ function ComponentC(props) { - const $ = _c(3); + const $ = _c(6); let a; let t0; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { a = []; @@ -140,12 +155,15 @@ function ComponentC(props) { a.push(props.d); } - $[0] = props; - $[1] = a; - $[2] = t0; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; + $[5] = t0; } else { - a = $[1]; - t0 = $[2]; + a = $[4]; + t0 = $[5]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; @@ -157,10 +175,15 @@ function ComponentC(props) { * props.b *does* influence `a` */ function ComponentD(props) { - const $ = _c(3); + const $ = _c(6); let a; let t0; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { a = []; @@ -173,12 +196,15 @@ function ComponentD(props) { a.push(props.d); } - $[0] = props; - $[1] = a; - $[2] = t0; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; + $[5] = t0; } else { - a = $[1]; - t0 = $[2]; + a = $[4]; + t0 = $[5]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md index ad638cf28d871..fa8348c200972 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md @@ -36,9 +36,9 @@ function mayMutate() {} ```javascript import { c as _c } from "react/compiler-runtime"; function ComponentA(props) { - const $ = _c(2); + const $ = _c(4); let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p1 || $[2] !== props.p2) { const a = []; const b = []; if (b) { @@ -49,18 +49,20 @@ function ComponentA(props) { } t0 = ; - $[0] = props; - $[1] = t0; + $[0] = props.p0; + $[1] = props.p1; + $[2] = props.p2; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } return t0; } function ComponentB(props) { - const $ = _c(2); + const $ = _c(4); let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p1 || $[2] !== props.p2) { const a = []; const b = []; if (mayMutate(b)) { @@ -71,10 +73,12 @@ function ComponentB(props) { } t0 = ; - $[0] = props; - $[1] = t0; + $[0] = props.p0; + $[1] = props.p1; + $[2] = props.p2; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-assignment-to-context-var.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-assignment-to-context-var.expect.md index 937bb7c2b623e..3cb54c8bcb19b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-assignment-to-context-var.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-assignment-to-context-var.expect.md @@ -30,7 +30,7 @@ import { identity } from "shared-runtime"; function Component(props) { const $ = _c(4); let x; - if ($[0] !== props.value) { + if ($[0] !== props) { const [t0] = props.value; x = t0; const foo = () => { @@ -38,7 +38,7 @@ function Component(props) { }; foo(); - $[0] = props.value; + $[0] = props; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md index e79bf7f17145a..e7e1078c93af7 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.expect.md @@ -29,7 +29,7 @@ import { identity } from "shared-runtime"; function Component(props) { const $ = _c(4); let x; - if ($[0] !== props.value) { + if ($[0] !== props) { const [t0] = props.value; x = t0; const foo = () => { @@ -37,7 +37,7 @@ function Component(props) { }; foo(); - $[0] = props.value; + $[0] = props; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md index 328f6d2ef3d03..235bf1f61cd85 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md @@ -31,9 +31,9 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(5); + const $ = _c(7); let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const x = []; @@ -41,12 +41,12 @@ function Component(props) { x.push(props.a); if (props.b) { let t1; - if ($[2] !== props.b) { + if ($[4] !== props.b) { t1 = [props.b]; - $[2] = props.b; - $[3] = t1; + $[4] = props.b; + $[5] = t1; } else { - t1 = $[3]; + t1 = $[5]; } const y = t1; x.push(y); @@ -58,20 +58,22 @@ function Component(props) { break bb0; } else { let t1; - if ($[4] === Symbol.for("react.memo_cache_sentinel")) { + if ($[6] === Symbol.for("react.memo_cache_sentinel")) { t1 = foo(); - $[4] = t1; + $[6] = t1; } else { - t1 = $[4]; + t1 = $[6]; } t0 = t1; break bb0; } } - $[0] = props; - $[1] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.b; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md index 584cbacb42bbf..133f2afb48472 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md @@ -45,9 +45,9 @@ import { c as _c } from "react/compiler-runtime"; import { makeArray } from "shared-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(6); let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const x = []; @@ -57,21 +57,23 @@ function Component(props) { break bb0; } else { let t1; - if ($[2] !== props.b) { + if ($[4] !== props.b) { t1 = makeArray(props.b); - $[2] = props.b; - $[3] = t1; + $[4] = props.b; + $[5] = t1; } else { - t1 = $[3]; + t1 = $[5]; } t0 = t1; break bb0; } } - $[0] = props; - $[1] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.b; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.expect.md new file mode 100644 index 0000000000000..99a3be2b62f19 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.expect.md @@ -0,0 +1,58 @@ + +## Input + +```javascript +// @enableTreatFunctionDepsAsConditional +import { Stringify } from "shared-runtime"; + +function Component({ props }) { + const f = () => props.a.b; + + return {} : f} />; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ props: null }], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(5); + const { props } = t0; + let t1; + if ($[0] !== props) { + t1 = () => props.a.b; + $[0] = props; + $[1] = t1; + } else { + t1 = $[1]; + } + const f = t1; + let t2; + if ($[2] !== props || $[3] !== f) { + t2 = {} : f} />; + $[2] = props; + $[3] = f; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ props: null }], +}; + +``` + +### Eval output +(kind: ok)
{"f":"[[ function params=0 ]]"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.tsx new file mode 100644 index 0000000000000..383180d14ff37 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.tsx @@ -0,0 +1,12 @@ +// @enableTreatFunctionDepsAsConditional +import { Stringify } from "shared-runtime"; + +function Component({ props }) { + const f = () => props.a.b; + + return {} : f} />; +} +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ props: null }], +}; diff --git "a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.expect.md" "b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.expect.md" index 293ad3d1d3c66..db78c744b93ec 100644 --- "a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.expect.md" +++ "b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.expect.md" @@ -25,11 +25,11 @@ import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsC function Component(props) { const $ = _c(5); let t0; - if ($[0] !== props) { + if ($[0] !== props.bar) { t0 = function getLength() { return props.bar.length; }; - $[0] = props; + $[0] = props.bar; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md index 6fcb1dc93e270..492c1a76eccda 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md @@ -26,9 +26,9 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(3); let items; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a) { let t0; if (props.cond) { t0 = []; @@ -38,10 +38,11 @@ function Component(props) { items = t0; items?.push(props.a); - $[0] = props; - $[1] = items; + $[0] = props.cond; + $[1] = props.a; + $[2] = items; } else { - items = $[1]; + items = $[2]; } return items; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md index bfe8339fcf88a..836ece6189ca8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md @@ -29,9 +29,9 @@ import { c as _c } from "react/compiler-runtime"; import { makeObject_Primitives } from "shared-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(3); let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.value) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const object = makeObject_Primitives(); @@ -45,10 +45,11 @@ function Component(props) { break bb0; } } - $[0] = props; - $[1] = t0; + $[0] = props.cond; + $[1] = props.value; + $[2] = t0; } else { - t0 = $[1]; + t0 = $[2]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md index 9e0050255b026..c5174acb8eeb6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md @@ -30,10 +30,10 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(6); let y; let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const x = []; @@ -43,11 +43,11 @@ function Component(props) { break bb0; } else { let t1; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + if ($[5] === Symbol.for("react.memo_cache_sentinel")) { t1 = foo(); - $[3] = t1; + $[5] = t1; } else { - t1 = $[3]; + t1 = $[5]; } y = t1; if (props.b) { @@ -56,12 +56,14 @@ function Component(props) { } } } - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.b; + $[3] = y; + $[4] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[3]; + t0 = $[4]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md index ee7aa62495664..1ee90d043ccac 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md @@ -36,9 +36,9 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(3); let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.value) { const x = {}; let y; if (props.cond) { @@ -50,10 +50,11 @@ function Component(props) { y.push(x); t0 = [x, y]; - $[0] = props; - $[1] = t0; + $[0] = props.cond; + $[1] = props.value; + $[2] = t0; } else { - t0 = $[1]; + t0 = $[2]; } return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md index b4343d3fbb485..e8afadf5a069e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md @@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @debug function Component(props) { - const $ = _c(5); + const $ = _c(6); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = {}; @@ -42,7 +42,7 @@ function Component(props) { } const x = t0; let y; - if ($[1] !== props) { + if ($[1] !== props.cond || $[2] !== props.a) { if (props.cond) { y = {}; } else { @@ -50,18 +50,19 @@ function Component(props) { } y.x = x; - $[1] = props; - $[2] = y; + $[1] = props.cond; + $[2] = props.a; + $[3] = y; } else { - y = $[2]; + y = $[3]; } let t1; - if ($[3] !== y) { + if ($[4] !== y) { t1 = [x, y]; - $[3] = y; - $[4] = t1; + $[4] = y; + $[5] = t1; } else { - t1 = $[4]; + t1 = $[5]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md index 9657c47fc98b4..7a03e9d7ff205 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md @@ -61,20 +61,13 @@ import { c as _c } from "react/compiler-runtime"; // This tests an optimization, import { CONST_TRUE, setProperty } from "shared-runtime"; function useJoinCondDepsInUncondScopes(props) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== props.a.b) { const y = {}; - let x; - if ($[2] !== props) { - x = {}; - if (CONST_TRUE) { - setProperty(x, props.a.b); - } - $[2] = props; - $[3] = x; - } else { - x = $[3]; + const x = {}; + if (CONST_TRUE) { + setProperty(x, props.a.b); } setProperty(y, props.a.b); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md index e7103b68f0d0c..696473384e128 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md @@ -34,19 +34,20 @@ import { identity } from "shared-runtime"; // and promote it to an unconditional dependency. function usePromoteUnconditionalAccessToDependency(props, other) { - const $ = _c(3); + const $ = _c(4); let x; - if ($[0] !== props.a || $[1] !== other) { + if ($[0] !== props.a.a.a || $[1] !== props.a.b || $[2] !== other) { x = {}; x.a = props.a.a.a; if (identity(other)) { x.c = props.a.b.c; } - $[0] = props.a; - $[1] = other; - $[2] = x; + $[0] = props.a.a.a; + $[1] = props.a.b; + $[2] = other; + $[3] = x; } else { - x = $[2]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-diff-ssa-instance.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-diff-ssa-instance.expect.md new file mode 100644 index 0000000000000..d3160e4ff9bf6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-diff-ssa-instance.expect.md @@ -0,0 +1,74 @@ + +## Input + +```javascript +function useFoo(a, b, c) { + let x = {}; + write(x, a); + + const y = []; + if (x.a != null) { + y.push(x.a.b); + } + y.push(b); + + x = makeThing(); + write(x.a.b); + + return [y, x.a.b]; +} + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +function useFoo(a, b, c) { + const $ = _c(9); + let x; + if ($[0] !== a) { + x = {}; + write(x, a); + $[0] = a; + $[1] = x; + } else { + x = $[1]; + } + let y; + if ($[2] !== x.a || $[3] !== b) { + y = []; + if (x.a != null) { + y.push(x.a.b); + } + + y.push(b); + $[2] = x.a; + $[3] = b; + $[4] = y; + } else { + y = $[4]; + } + if ($[5] === Symbol.for("react.memo_cache_sentinel")) { + x = makeThing(); + write(x.a.b); + $[5] = x; + } else { + x = $[5]; + } + let t0; + if ($[6] !== y || $[7] !== x.a.b) { + t0 = [y, x.a.b]; + $[6] = y; + $[7] = x.a.b; + $[8] = t0; + } else { + t0 = $[8]; + } + return t0; +} + +``` + +### Eval output +(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-diff-ssa-instance.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-diff-ssa-instance.js new file mode 100644 index 0000000000000..bf799429f0963 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-diff-ssa-instance.js @@ -0,0 +1,15 @@ +function useFoo(a, b, c) { + let x = {}; + write(x, a); + + const y = []; + if (x.a != null) { + y.push(x.a.b); + } + y.push(b); + + x = makeThing(); + write(x.a.b); + + return [y, x.a.b]; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.expect.md new file mode 100644 index 0000000000000..14a024740844c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.expect.md @@ -0,0 +1,105 @@ + +## Input + +```javascript +// x.a.b was accessed unconditionally within the mutable range of x. +// As a result, we cannot infer anything about whether `x` or `x.a` +// may be null. This means that it's not safe to hoist reads from x +// (e.g. take `x.a` or `x.a.b` as a dependency). + +import { identity, makeObject_Primitives, setProperty } from "shared-runtime"; + +function Component({ cond, other }) { + const x = makeObject_Primitives(); + setProperty(x, { b: 3, other }, "a"); + identity(x.a.b); + if (!cond) { + x.a = null; + } + + const y = [identity(cond) && x.a.b]; + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false }], + sequentialRenders: [ + { cond: false }, + { cond: false }, + { cond: false, other: 8 }, + { cond: true }, + { cond: true }, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // x.a.b was accessed unconditionally within the mutable range of x. +// As a result, we cannot infer anything about whether `x` or `x.a` +// may be null. This means that it's not safe to hoist reads from x +// (e.g. take `x.a` or `x.a.b` as a dependency). + +import { identity, makeObject_Primitives, setProperty } from "shared-runtime"; + +function Component(t0) { + const $ = _c(8); + const { cond, other } = t0; + let x; + if ($[0] !== other || $[1] !== cond) { + x = makeObject_Primitives(); + setProperty(x, { b: 3, other }, "a"); + identity(x.a.b); + if (!cond) { + x.a = null; + } + $[0] = other; + $[1] = cond; + $[2] = x; + } else { + x = $[2]; + } + let t1; + if ($[3] !== cond || $[4] !== x) { + t1 = identity(cond) && x.a.b; + $[3] = cond; + $[4] = x; + $[5] = t1; + } else { + t1 = $[5]; + } + let t2; + if ($[6] !== t1) { + t2 = [t1]; + $[6] = t1; + $[7] = t2; + } else { + t2 = $[7]; + } + const y = t2; + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false }], + sequentialRenders: [ + { cond: false }, + { cond: false }, + { cond: false, other: 8 }, + { cond: true }, + { cond: true }, + ], +}; + +``` + +### Eval output +(kind: ok) [false] +[false] +[false] +[3] +[3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.js new file mode 100644 index 0000000000000..db90a232a1747 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/uncond-access-in-mutable-range.js @@ -0,0 +1,30 @@ +// x.a.b was accessed unconditionally within the mutable range of x. +// As a result, we cannot infer anything about whether `x` or `x.a` +// may be null. This means that it's not safe to hoist reads from x +// (e.g. take `x.a` or `x.a.b` as a dependency). + +import { identity, makeObject_Primitives, setProperty } from "shared-runtime"; + +function Component({ cond, other }) { + const x = makeObject_Primitives(); + setProperty(x, { b: 3, other }, "a"); + identity(x.a.b); + if (!cond) { + x.a = null; + } + + const y = [identity(cond) && x.a.b]; + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ cond: false }], + sequentialRenders: [ + { cond: false }, + { cond: false }, + { cond: false, other: 8 }, + { cond: true }, + { cond: true }, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md index 061aa03c0856d..08ed68e9c57d2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md @@ -31,10 +31,16 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(7); let x = 0; let values; - if ($[0] !== props || $[1] !== x) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d || + $[4] !== x + ) { values = []; const y = props.a || props.b; values.push(y); @@ -48,13 +54,16 @@ function Component(props) { } values.push(x); - $[0] = props; - $[1] = x; - $[2] = values; - $[3] = x; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = x; + $[5] = values; + $[6] = x; } else { - values = $[2]; - x = $[3]; + values = $[5]; + x = $[6]; } return values; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md index a7389041bb12d..851e45da94089 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md @@ -24,9 +24,9 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(3); let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p1) { const x = []; let y; if (props.p0) { @@ -40,10 +40,11 @@ function Component(props) { {y} ); - $[0] = props; - $[1] = t0; + $[0] = props.p0; + $[1] = props.p1; + $[2] = t0; } else { - t0 = $[1]; + t0 = $[2]; } return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md index 11d328201eac7..0d3dfb9373ce2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md @@ -17,17 +17,19 @@ function foo(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(2); + const $ = _c(4); let x; - if ($[0] !== props) { + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) { x = []; x.push(props.bar); props.cond ? (([x] = [[]]), x.push(props.foo)) : null; mut(x); - $[0] = props; - $[1] = x; + $[0] = props.bar; + $[1] = props.cond; + $[2] = props.foo; + $[3] = x; } else { - x = $[1]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md index 8ecd18dea93f6..32b1d8d059389 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md @@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(4); + const $ = _c(5); let x; if ($[0] !== props.bar) { x = []; @@ -32,12 +32,13 @@ function foo(props) { } else { x = $[1]; } - if ($[2] !== props) { + if ($[2] !== props.cond || $[3] !== props.foo) { props.cond ? (([x] = [[]]), x.push(props.foo)) : null; - $[2] = props; - $[3] = x; + $[2] = props.cond; + $[3] = props.foo; + $[4] = x; } else { - x = $[3]; + x = $[4]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-with-mutation.expect.md index 3ec90ca8879bc..06c398c3130cc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-with-mutation.expect.md @@ -17,17 +17,19 @@ function foo(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(2); + const $ = _c(4); let x; - if ($[0] !== props) { + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) { x = []; x.push(props.bar); props.cond ? ((x = []), x.push(props.foo)) : null; mut(x); - $[0] = props; - $[1] = x; + $[0] = props.bar; + $[1] = props.cond; + $[2] = props.foo; + $[3] = x; } else { - x = $[1]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md index 83678c96d8043..7e9c41e8a609a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md @@ -22,7 +22,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(4); + const $ = _c(5); let x; if ($[0] !== props.bar) { x = []; @@ -32,12 +32,13 @@ function foo(props) { } else { x = $[1]; } - if ($[2] !== props) { + if ($[2] !== props.cond || $[3] !== props.foo) { props.cond ? ((x = []), x.push(props.foo)) : null; - $[2] = props; - $[3] = x; + $[2] = props.cond; + $[3] = props.foo; + $[4] = x; } else { - x = $[3]; + x = $[4]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md index 598d9677aae1d..bdc931c702afc 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md @@ -31,17 +31,19 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; import { arrayPush } from "shared-runtime"; function foo(props) { - const $ = _c(2); + const $ = _c(4); let x; - if ($[0] !== props) { + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) { x = []; x.push(props.bar); props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar)); arrayPush(x, 4); - $[0] = props; - $[1] = x; + $[0] = props.bar; + $[1] = props.cond; + $[2] = props.foo; + $[3] = x; } else { - x = $[1]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md index d0b8c933018ee..e548ff29e4aef 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md @@ -24,7 +24,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(4); + const $ = _c(6); let x; if ($[0] !== props.bar) { x = []; @@ -34,12 +34,14 @@ function foo(props) { } else { x = $[1]; } - if ($[2] !== props) { + if ($[2] !== props.cond || $[3] !== props.foo || $[4] !== props.bar) { props.cond ? ((x = []), x.push(props.foo)) : ((x = []), x.push(props.bar)); - $[2] = props; - $[3] = x; + $[2] = props.cond; + $[3] = props.foo; + $[4] = props.bar; + $[5] = x; } else { - x = $[3]; + x = $[5]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-with-mutation.expect.md index e27e00db70f05..3911d2f0f8b4b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-with-mutation.expect.md @@ -25,9 +25,9 @@ function foo(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(2); + const $ = _c(4); let x; - if ($[0] !== props) { + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) { x = []; x.push(props.bar); if (props.cond) { @@ -39,10 +39,12 @@ function foo(props) { } mut(x); - $[0] = props; - $[1] = x; + $[0] = props.bar; + $[1] = props.cond; + $[2] = props.foo; + $[3] = x; } else { - x = $[1]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-via-destructuring-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-via-destructuring-with-mutation.expect.md index b202302d65aba..253307cfb0b32 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-via-destructuring-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-via-destructuring-with-mutation.expect.md @@ -21,9 +21,9 @@ function foo(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(2); + const $ = _c(4); let x; - if ($[0] !== props) { + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) { ({ x } = { x: [] }); x.push(props.bar); if (props.cond) { @@ -32,10 +32,12 @@ function foo(props) { } mut(x); - $[0] = props; - $[1] = x; + $[0] = props.bar; + $[1] = props.cond; + $[2] = props.foo; + $[3] = x; } else { - x = $[1]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-with-mutation.expect.md index 76ccbae06a937..12758a0f1c287 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-with-mutation.expect.md @@ -21,9 +21,9 @@ function foo(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function foo(props) { - const $ = _c(2); + const $ = _c(4); let x; - if ($[0] !== props) { + if ($[0] !== props.bar || $[1] !== props.cond || $[2] !== props.foo) { x = []; x.push(props.bar); if (props.cond) { @@ -32,10 +32,12 @@ function foo(props) { } mut(x); - $[0] = props; - $[1] = x; + $[0] = props.bar; + $[1] = props.cond; + $[2] = props.foo; + $[3] = x; } else { - x = $[1]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-non-final-default.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-non-final-default.expect.md index 915218fcfa467..0a5e7103c602e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-non-final-default.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-non-final-default.expect.md @@ -33,10 +33,10 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(7); + const $ = _c(8); let y; let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p2) { const x = []; bb0: switch (props.p0) { case 1: { @@ -45,11 +45,11 @@ function Component(props) { case true: { x.push(props.p2); let t1; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + if ($[4] === Symbol.for("react.memo_cache_sentinel")) { t1 = []; - $[3] = t1; + $[4] = t1; } else { - t1 = $[3]; + t1 = $[4]; } y = t1; } @@ -62,23 +62,24 @@ function Component(props) { } t0 = ; - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.p0; + $[1] = props.p2; + $[2] = y; + $[3] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[2]; + t0 = $[3]; } const child = t0; y.push(props.p4); let t1; - if ($[4] !== y || $[5] !== child) { + if ($[5] !== y || $[6] !== child) { t1 = {child}; - $[4] = y; - $[5] = child; - $[6] = t1; + $[5] = y; + $[6] = child; + $[7] = t1; } else { - t1 = $[6]; + t1 = $[7]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md index 0c5aea9c7da9c..b83c1fcb7b68f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md @@ -28,10 +28,10 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(6); + const $ = _c(8); let y; let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p2 || $[2] !== props.p3) { const x = []; switch (props.p0) { case true: { @@ -44,23 +44,25 @@ function Component(props) { } t0 = ; - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.p0; + $[1] = props.p2; + $[2] = props.p3; + $[3] = y; + $[4] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[3]; + t0 = $[4]; } const child = t0; y.push(props.p4); let t1; - if ($[3] !== y || $[4] !== child) { + if ($[5] !== y || $[6] !== child) { t1 = {child}; - $[3] = y; - $[4] = child; - $[5] = t1; + $[5] = y; + $[6] = child; + $[7] = t1; } else { - t1 = $[5]; + t1 = $[7]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.expect.md new file mode 100644 index 0000000000000..7766e82132df6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.expect.md @@ -0,0 +1,73 @@ + +## Input + +```javascript +import { Stringify } from "shared-runtime"; +import { makeArray } from "shared-runtime"; + +/** + * Here, we don't need to memoize Stringify as it is a read off of a global. + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals + * and avoid adding them to `temporariesUsedOutsideDefiningScope`. + */ +function Component({ num }: { num: number }) { + const arr = makeArray(num); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ num: 2 }], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify } from "shared-runtime"; +import { makeArray } from "shared-runtime"; + +/** + * Here, we don't need to memoize Stringify as it is a read off of a global. + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals + * and avoid adding them to `temporariesUsedOutsideDefiningScope`. + */ +function Component(t0) { + const $ = _c(6); + const { num } = t0; + let T0; + let t1; + if ($[0] !== num) { + const arr = makeArray(num); + T0 = Stringify; + t1 = arr.push(num); + $[0] = num; + $[1] = T0; + $[2] = t1; + } else { + T0 = $[1]; + t1 = $[2]; + } + let t2; + if ($[3] !== T0 || $[4] !== t1) { + t2 = ; + $[3] = T0; + $[4] = t1; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ num: 2 }], +}; + +``` + +### Eval output +(kind: ok)
{"value":2}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.tsx new file mode 100644 index 0000000000000..227588bd15f01 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-load-cached.tsx @@ -0,0 +1,17 @@ +import { Stringify } from "shared-runtime"; +import { makeArray } from "shared-runtime"; + +/** + * Here, we don't need to memoize Stringify as it is a read off of a global. + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals + * and avoid adding them to `temporariesUsedOutsideDefiningScope`. + */ +function Component({ num }: { num: number }) { + const arr = makeArray(num); + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ num: 2 }], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.expect.md new file mode 100644 index 0000000000000..bad5ca2572f31 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.expect.md @@ -0,0 +1,78 @@ + +## Input + +```javascript +import * as SharedRuntime from "shared-runtime"; +import { makeArray } from "shared-runtime"; + +/** + * Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad + * off of a global. + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals + * and avoid adding them to `temporariesUsedOutsideDefiningScope`. + */ +function Component({ num }: { num: number }) { + const arr = makeArray(num); + return ( + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ num: 2 }], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import * as SharedRuntime from "shared-runtime"; +import { makeArray } from "shared-runtime"; + +/** + * Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad + * off of a global. + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals + * and avoid adding them to `temporariesUsedOutsideDefiningScope`. + */ +function Component(t0) { + const $ = _c(6); + const { num } = t0; + let T0; + let t1; + if ($[0] !== num) { + const arr = makeArray(num); + + T0 = SharedRuntime.Stringify; + t1 = arr.push(num); + $[0] = num; + $[1] = T0; + $[2] = t1; + } else { + T0 = $[1]; + t1 = $[2]; + } + let t2; + if ($[3] !== T0 || $[4] !== t1) { + t2 = ; + $[3] = T0; + $[4] = t1; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ num: 2 }], +}; + +``` + +### Eval output +(kind: ok)
{"value":2}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.tsx new file mode 100644 index 0000000000000..a6ff9d2b6c199 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/todo-global-property-load-cached.tsx @@ -0,0 +1,20 @@ +import * as SharedRuntime from "shared-runtime"; +import { makeArray } from "shared-runtime"; + +/** + * Here, we don't need to memoize SharedRuntime.Stringify as it is a PropertyLoad + * off of a global. + * TODO: in PropagateScopeDeps (hir), we should produce a sidemap of global rvals + * and avoid adding them to `temporariesUsedOutsideDefiningScope`. + */ +function Component({ num }: { num: number }) { + const arr = makeArray(num); + return ( + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ num: 2 }], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md index fde19b0664a37..67f6d096d27fb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md @@ -28,9 +28,9 @@ import { c as _c } from "react/compiler-runtime"; const { shallowCopy, throwErrorWithMessage } = require("shared-runtime"); function Component(props) { - const $ = _c(3); + const $ = _c(5); let x; - if ($[0] !== props.a) { + if ($[0] !== props) { x = []; try { let t0; @@ -42,9 +42,17 @@ function Component(props) { } x.push(t0); } catch { - x.push(shallowCopy({ a: props.a })); + let t0; + if ($[3] !== props.a) { + t0 = shallowCopy({ a: props.a }); + $[3] = props.a; + $[4] = t0; + } else { + t0 = $[4]; + } + x.push(t0); } - $[0] = props.a; + $[0] = props; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch-escaping.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch-escaping.expect.md index 6976489a4b7b9..475fc74d2f85b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch-escaping.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch-escaping.expect.md @@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime"; const { throwInput } = require("shared-runtime"); function Component(props) { - const $ = _c(3); + const $ = _c(2); let x; - if ($[0] !== props.y || $[1] !== props.e) { + if ($[0] !== props) { try { const y = []; y.push(props.y); @@ -43,11 +43,10 @@ function Component(props) { e.push(props.e); x = e; } - $[0] = props.y; - $[1] = props.e; - $[2] = x; + $[0] = props; + $[1] = x; } else { - x = $[2]; + x = $[1]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch.expect.md index c5ad808d99ecd..f63764680dda1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-try-value-modified-in-catch.expect.md @@ -30,9 +30,9 @@ import { c as _c } from "react/compiler-runtime"; const { throwInput } = require("shared-runtime"); function Component(props) { - const $ = _c(3); + const $ = _c(2); let t0; - if ($[0] !== props.y || $[1] !== props.e) { + if ($[0] !== props) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { try { @@ -46,11 +46,10 @@ function Component(props) { break bb0; } } - $[0] = props.y; - $[1] = props.e; - $[2] = t0; + $[0] = props; + $[1] = t0; } else { - t0 = $[2]; + t0 = $[1]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md index 1141297f01073..c0685309ef34a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md @@ -33,11 +33,16 @@ import { c as _c } from "react/compiler-runtime"; import { useMemo } from "react"; function Component(props) { - const $ = _c(3); + const $ = _c(6); let t0; bb0: { let y; - if ($[0] !== props) { + if ( + $[0] !== props.cond || + $[1] !== props.a || + $[2] !== props.cond2 || + $[3] !== props.b + ) { y = []; if (props.cond) { y.push(props.a); @@ -48,12 +53,15 @@ function Component(props) { } y.push(props.b); - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.cond2; + $[3] = props.b; + $[4] = y; + $[5] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[4]; + t0 = $[5]; } t0 = y; } diff --git a/compiler/packages/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index a67ab298ada0c..8d2b2a2aa26cb 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -78,21 +78,29 @@ export function mutateAndReturnNewValue(arg: T): string { return "hello!"; } -export function setProperty(arg: any, property: any): void { +export function setProperty( + arg: any, + property: any, + propertyName?: string +): void { // don't mutate primitive if (arg == null || typeof arg !== "object") { return arg; } - let count: number = 0; - let key; - while (true) { - key = "wat" + count; - if (!Object.hasOwn(arg, key)) { - arg[key] = property; - return arg; + if (propertyName != null && typeof propertyName === "string") { + arg[propertyName] = property; + } else { + let count: number = 0; + let key; + while (true) { + key = "wat" + count; + if (!Object.hasOwn(arg, key)) { + arg[key] = property; + return arg; + } + count++; } - count++; } }