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 2f6d8a94021ab..aef18c90c2e5e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -101,6 +101,7 @@ import {propagatePhiTypes} from '../TypeInference/PropagatePhiTypes'; import {lowerContextAccess} from '../Optimization/LowerContextAccess'; import {validateNoSetStateInPassiveEffects} from '../Validation/ValidateNoSetStateInPassiveEffects'; import {validateNoJSXInTryStatement} from '../Validation/ValidateNoJSXInTryStatement'; +import {propagateScopeDependenciesHIR} from '../HIR/PropagateScopeDependenciesHIR'; export type CompilerPipelineValue = | {kind: 'ast'; name: string; value: CodegenFunction} @@ -341,6 +342,14 @@ function* runWithEnvironment( }); assertTerminalSuccessorsExist(hir); assertTerminalPredsExist(hir); + if (env.config.enablePropagateDepsInHIR) { + propagateScopeDependenciesHIR(hir); + yield log({ + kind: 'hir', + name: 'PropagateScopeDependenciesHIR', + value: hir, + }); + } const reactiveFunction = buildReactiveFunction(hir); yield log({ @@ -359,12 +368,14 @@ function* runWithEnvironment( }); assertScopeInstructionsWithinScopes(reactiveFunction); - propagateScopeDependencies(reactiveFunction); - yield log({ - kind: 'reactive', - name: 'PropagateScopeDependencies', - value: reactiveFunction, - }); + if (!env.config.enablePropagateDepsInHIR) { + 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..941c60dea9d4f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -0,0 +1,469 @@ +import {CompilerError} from '../CompilerError'; +import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables'; +import {Set_intersect, Set_union, getOrInsertDefault} from '../Utils/utils'; +import { + BasicBlock, + BlockId, + GeneratedSource, + HIRFunction, + Identifier, + IdentifierId, + Place, + ReactiveScopeDependency, + ScopeId, +} from './HIR'; + +/** + * Helper function for `PropagateScopeDependencies`. + * Uses control flow graph analysis to determine which `Identifier`s can + * be assumed to be non-null objects, on a per-block basis. + * + * Here is an example: + * ```js + * function useFoo(x, y, z) { + * // NOT safe to hoist PropertyLoads here + * if (...) { + * // safe to hoist loads from x + * read(x.a); + * return; + * } + * // safe to hoist loads from y, z + * read(y.b); + * if (...) { + * // safe to hoist loads from y, z + * read(z.a); + * } else { + * // safe to hoist loads from y, z + * read(z.b); + * } + * // safe to hoist loads from y, z + * return; + * } + * ``` + * + * Note that we currently do NOT account for mutable / declaration range + * when doing the CFG-based traversal, producing results that are technically + * incorrect but filtered by PropagateScopeDeps (which only takes dependencies + * on constructed value -- i.e. a scope's dependencies must have mutable ranges + * ending earlier than the scope start). + * + * Take this example, this function will infer x.foo.bar as non-nullable for bb0, + * via the intersection of bb1 & bb2 which in turn comes from bb3. This is technically + * incorrect bb0 is before / during x's mutable range. + * bb0: + * const x = ...; + * if cond then bb1 else bb2 + * bb1: + * ... + * goto bb3 + * bb2: + * ... + * goto bb3: + * bb3: + * x.foo.bar + */ +export function collectHoistablePropertyLoads( + fn: HIRFunction, + temporaries: ReadonlyMap, +): ReadonlyMap { + const nodes = collectPropertyLoadsInBlocks(fn, temporaries); + propagateNonNull(fn, nodes); + + const nodesKeyedByScopeId = new Map(); + for (const [_, block] of fn.body.blocks) { + if (block.terminal.kind === 'scope') { + nodesKeyedByScopeId.set( + block.terminal.scope.id, + nodes.get(block.terminal.block)!, + ); + } + } + + return nodesKeyedByScopeId; +} + +export type BlockInfo = { + block: BasicBlock; + assumedNonNullObjects: ReadonlySet; +}; + +export function getProperty( + object: Place, + propertyName: string, + temporaries: ReadonlyMap, +): ReactiveScopeDependency { + /* + * (1) Get the base object either from the temporary sidemap (e.g. a LoadLocal) + * or a deep copy of an existing property dependency. + * Example 1: + * $0 = LoadLocal x + * $1 = PropertyLoad $0.y + * getProperty($0, ...) -> resolvedObject = x, resolvedDependency = null + * + * Example 2: + * $0 = LoadLocal x + * $1 = PropertyLoad $0.y + * $2 = PropertyLoad $1.z + * getProperty($1, ...) -> resolvedObject = null, resolvedDependency = x.y + * + * Example 3: + * $0 = Call(...) + * $1 = PropertyLoad $0.y + * getProperty($0, ...) -> resolvedObject = null, resolvedDependency = null + */ + const resolvedDependency = temporaries.get(object.identifier.id); + + /** + * (2) Push the last PropertyLoad + * TODO(mofeiZ): understand optional chaining + */ + let property: ReactiveScopeDependency; + if (resolvedDependency == null) { + property = { + identifier: object.identifier, + path: [{property: propertyName, optional: false}], + }; + } else { + property = { + identifier: resolvedDependency.identifier, + path: [ + ...resolvedDependency.path, + {property: propertyName, optional: false}, + ], + }; + } + return property; +} + +export function resolveTemporary( + place: Place, + temporaries: ReadonlyMap, +): Identifier { + return temporaries.get(place.identifier.id) ?? place.identifier; +} + +/** + * Tree data structure to dedupe property loads (e.g. a.b.c) + * and make computing sets 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 { + /** + * Reads from a statically scoped variable are always safe in JS, + * with the exception of TDZ (not addressed by this pass). + */ + 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 #getOrCreateProperty( + 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, optional: false}]), + }, + }; + node.properties.set(property, child); + } + return child; + } + + getPropertyLoadNode(n: ReactiveScopeDependency): PropertyLoadNode { + CompilerError.invariant(n.path.length > 0, { + reason: + '[CollectHoistablePropertyLoads] Expected property node, found root node', + loc: GeneratedSource, + }); + /** + * We add ReactiveScopeDependencies according to instruction ordering, + * so all subpaths of a PropertyLoad should already exist + * (e.g. a.b is added before a.b.c), + */ + let currNode = this.#getOrCreateRoot(n.identifier); + for (let i = 0; i < n.path.length - 1; i++) { + currNode = assertNonNull(currNode.properties.get(n.path[i].property)); + } + + return Tree.#getOrCreateProperty(currNode, n.path.at(-1)!.property); + } +} + +function collectPropertyLoadsInBlocks( + fn: HIRFunction, + temporaries: ReadonlyMap, +): ReadonlyMap { + /** + * Due to current limitations of mutable range inference, there are edge cases in + * which we infer known-immutable values (e.g. props or hook params) to have a + * mutable range and scope. + * (see `destructure-array-declaration-to-context-var` fixture) + * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps + * is being rewritten to HIR). + */ + const knownImmutableIdentifiers = new Set(); + if (fn.fnType === 'Component' || fn.fnType === 'Hook') { + for (const p of fn.params) { + if (p.kind === 'Identifier') { + knownImmutableIdentifiers.add(p.identifier); + } + } + } + const tree = new Tree(); + const nodes = new Map(); + for (const [_, block] of fn.body.blocks) { + const assumedNonNullObjects = new Set(); + for (const instr of block.instructions) { + if (instr.value.kind === 'PropertyLoad') { + const property = getProperty( + instr.value.object, + instr.value.property, + temporaries, + ); + const propertyNode = tree.getPropertyLoadNode(property); + const object = instr.value.object.identifier; + /** + * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges + * are not valid with respect to current instruction id numbering. + * We use attached reactive scope ranges as a proxy for mutable range, but this + * is an overestimate as (1) scope ranges merge and align to form valid program + * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to + * non-mutable identifiers. + * + * See comment at top of function for why we track known immutable identifiers. + */ + const isMutableAtInstr = + object.mutableRange.end > object.mutableRange.start + 1 && + object.scope != null && + inRange(instr, object.scope.range); + if ( + !isMutableAtInstr || + knownImmutableIdentifiers.has(propertyNode.fullPath.identifier) + ) { + let curr = propertyNode.parent; + while (curr != null) { + assumedNonNullObjects.add(curr); + curr = curr.parent; + } + } + } + // TODO handle destructuring + } + + nodes.set(block.id, { + block, + assumedNonNullObjects, + }); + } + return nodes; +} + +function propagateNonNull( + fn: HIRFunction, + nodes: ReadonlyMap, +): void { + const blockSuccessors = new Map>(); + const terminalPreds = new Set(); + + for (const [blockId, block] of fn.body.blocks) { + for (const pred of block.preds) { + getOrInsertDefault(blockSuccessors, pred, new Set()).add(blockId); + } + if (block.terminal.kind === 'throw' || block.terminal.kind === 'return') { + terminalPreds.add(blockId); + } + } + + /** + * In the context of a control flow graph, the identifiers that a block + * can assume are non-null can be calculated from the following: + * X = Union(Intersect(X_neighbors), X) + */ + function recursivelyPropagateNonNull( + nodeId: BlockId, + direction: 'forward' | 'backward', + traversalState: Map, + nonNullObjectsByBlock: Map>, + ): boolean { + /** + * Avoid re-visiting computed or currently active nodes, which can + * occur when the control flow graph has backedges. + */ + 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: ${direction}`, + loc: GeneratedSource, + }); + } + const neighbors = Array.from( + direction === 'backward' + ? (blockSuccessors.get(nodeId) ?? []) + : node.block.preds, + ); + + let changed = false; + for (const pred of neighbors) { + if (!traversalState.has(pred)) { + const neighborChanged = recursivelyPropagateNonNull( + pred, + direction, + traversalState, + nonNullObjectsByBlock, + ); + changed ||= neighborChanged; + } + } + /** + * Note that a predecessor / successor can only be active (status != 'done') + * if it is a self-loop or other transitive cycle. Active neighbors can be + * filtered out (i.e. not included in the intersection) + * Example: self loop. + * X = Union(Intersect(X, ...X_other_neighbors), X) + * + * Example: transitive cycle through node Y, for some Y that is a + * predecessor / successor of X. + * X = Union( + * Intersect( + * Union(Intersect(X, ...Y_other_neighbors), Y), + * ...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 included in + * the intersection) + */ + const neighborAccesses = Set_intersect( + Array.from(neighbors) + .filter(n => traversalState.get(n) === 'done') + .map(n => assertNonNull(nonNullObjectsByBlock.get(n))), + ); + + const prevObjects = assertNonNull(nonNullObjectsByBlock.get(nodeId)); + const newObjects = Set_union(prevObjects, neighborAccesses); + + nonNullObjectsByBlock.set(nodeId, newObjects); + traversalState.set(nodeId, 'done'); + changed ||= prevObjects.size !== newObjects.size; + return changed; + } + const fromEntry = new Map>(); + const fromExit = new Map>(); + for (const [blockId, blockInfo] of nodes) { + fromEntry.set(blockId, blockInfo.assumedNonNullObjects); + fromExit.set(blockId, blockInfo.assumedNonNullObjects); + } + const traversalState = new Map(); + const reversedBlocks = [...fn.body.blocks]; + reversedBlocks.reverse(); + + let i = 0; + let changed; + do { + i++; + changed = false; + for (const [blockId] of fn.body.blocks) { + const forwardChanged = recursivelyPropagateNonNull( + blockId, + 'forward', + traversalState, + fromEntry, + ); + changed ||= forwardChanged; + } + traversalState.clear(); + for (const [blockId] of reversedBlocks) { + const backwardChanged = recursivelyPropagateNonNull( + blockId, + 'backward', + traversalState, + fromExit, + ); + changed ||= backwardChanged; + } + traversalState.clear(); + } while (changed); + + /** + * TODO: validate against meta internal code, then remove in future PR. + * Currently cannot come up with a case that requires fixed-point iteration. + */ + CompilerError.invariant(i <= 2, { + reason: 'require fixed-point iteration', + description: `#iterations = ${i}`, + 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)), + ); + } +} + +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..ecc1844b006aa --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts @@ -0,0 +1,267 @@ +/** + * 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; + +/** + * Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR + * for detailed explanation. + */ +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.Access, + }; + this.#roots.set(identifier, rootNode); + } + return rootNode; + } + + addDependency(dep: ReactiveScopePropertyDependency): void { + const {path} = dep; + let currNode = this.#getOrCreateRoot(dep.identifier, false); + + const accessType = PropertyAccessType.Access; + + 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.property); + currChild.accessType = merge(currChild.accessType, accessType); + currNode = currChild; + } + + /* + * 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.Dependency, + ); + } + + 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++].property); + } + 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.map(s => ({property: s, optional: false})), + }); + } + } + + 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 { + Access = 'Access', + NonNullAccess = 'NonNullAccess', + Dependency = 'Dependency', + NonNullDependency = 'NonNullDependency', +} + +const MIN_ACCESS_TYPE = PropertyAccessType.Access; +/** + * "NonNull" means that PropertyReads from a node are side-effect free, + * as the node is (1) immutable and (2) has unconditional propertyloads + * somewhere in the cfg. + */ +function isNonNull(access: PropertyAccessType): boolean { + return ( + access === PropertyAccessType.NonNullAccess || + access === PropertyAccessType.NonNullDependency + ); +} +function isDependency(access: PropertyAccessType): boolean { + return ( + access === PropertyAccessType.Dependency || + 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 | Dependency` 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.Dependency; + } else { + return PropertyAccessType.Access; + } + } +} + +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 non-null 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}]; + } + } +} + +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; +} 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..4c7dac004d80a --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -0,0 +1,557 @@ +import { + ScopeId, + HIRFunction, + Place, + Instruction, + ReactiveScopeDependency, + Identifier, + ReactiveScope, + isObjectMethodType, + isRefValueType, + isUseRefType, + makeInstructionId, + InstructionId, + InstructionKind, + GeneratedSource, + DeclarationId, + areEqualPaths, + IdentifierId, +} from './HIR'; +import { + BlockInfo, + collectHoistablePropertyLoads, + getProperty, +} from './CollectHoistablePropertyLoads'; +import { + ScopeBlockTraversal, + eachInstructionOperand, + eachInstructionValueOperand, + eachPatternOperand, + eachTerminalOperand, +} from './visitors'; +import {Stack, empty} from '../Utils/Stack'; +import {CompilerError} from '../CompilerError'; +import {Iterable_some} from '../Utils/utils'; +import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR'; + +export function propagateScopeDependenciesHIR(fn: HIRFunction): void { + const usedOutsideDeclaringScope = + findTemporariesUsedOutsideDeclaringScope(fn); + const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope); + + const hoistablePropertyLoads = collectHoistablePropertyLoads(fn, temporaries); + + const scopeDeps = collectDependencies( + fn, + usedOutsideDeclaringScope, + temporaries, + ); + + /** + * 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}); + } + /** + * Step 2: Mark hoistable dependencies, given the basic block in + * which the scope begins. + */ + recordHoistablePropertyReads(hoistablePropertyLoads, scope.id, tree); + const candidates = tree.deriveMinimalDependencies(); + for (const candidateDep of candidates) { + if ( + !Iterable_some( + scope.dependencies, + existingDep => + existingDep.identifier.declarationId === + candidateDep.identifier.declarationId && + areEqualPaths(existingDep.path, candidateDep.path), + ) + ) + scope.dependencies.add(candidateDep); + } + } +} + +function findTemporariesUsedOutsideDeclaringScope( + fn: HIRFunction, +): ReadonlySet { + /* + * tracks all relevant LoadLocal and PropertyLoad lvalues + * and the scope where they are defined + */ + const declarations = new Map(); + const prunedScopes = new Set(); + const scopeTraversal = new ScopeBlockTraversal(); + const usedOutsideDeclaringScope = new Set(); + + function handlePlace(place: Place): void { + const declaringScope = declarations.get(place.identifier.declarationId); + if ( + declaringScope != null && + !scopeTraversal.isScopeActive(declaringScope) && + !prunedScopes.has(declaringScope) + ) { + // Declaring scope is not active === used outside declaring scope + usedOutsideDeclaringScope.add(place.identifier.declarationId); + } + } + + function handleInstruction(instr: Instruction): void { + const scope = scopeTraversal.currentScope; + if (scope == null || prunedScopes.has(scope)) { + return; + } + switch (instr.value.kind) { + case 'LoadLocal': + case 'LoadContext': + case 'PropertyLoad': { + declarations.set(instr.lvalue.identifier.declarationId, scope); + break; + } + default: { + break; + } + } + } + + for (const [blockId, block] of fn.body.blocks) { + scopeTraversal.recordScopes(block); + const scopeStartInfo = scopeTraversal.blockInfos.get(blockId); + if (scopeStartInfo?.kind === 'begin' && scopeStartInfo.pruned) { + prunedScopes.add(scopeStartInfo.scope.id); + } + 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); + } + } + return usedOutsideDeclaringScope; +} + +/** + * @returns mapping of LoadLocal and PropertyLoad to the source of the load. + * ```js + * // source + * foo(a.b); + * + * // HIR: a potential sidemap is {0: a, 1: a.b, 2: foo} + * $0 = LoadLocal 'a' + * $1 = PropertyLoad $0, 'b' + * $2 = LoadLocal 'foo' + * $3 = CallExpression $2($1) + * ``` + * Only map LoadLocal and PropertyLoad lvalues to their source if we know that + * reordering the read (from the time-of-load to time-of-use) is valid. + * + * If a LoadLocal or PropertyLoad instruction is within the reactive scope range + * (a proxy for mutable range) of the load source, later instructions may + * reassign / mutate the source value. Since it's incorrect to reorder these + * load instructions to after their scope ranges, we also do not store them in + * identifier sidemaps. + * + * Take this example (from fixture + * `evaluation-order-mutate-call-after-dependency-load`) + * ```js + * // source + * function useFoo(arg) { + * const arr = [1, 2, 3, ...arg]; + * return [ + * arr.length, + * arr.push(0) + * ]; + * } + * + * // IR pseudocode + * scope @0 { + * $0 = arr = ArrayExpression [1, 2, 3, ...arg] + * $1 = arr.length + * $2 = arr.push(0) + * } + * scope @1 { + * $3 = ArrayExpression [$1, $2] + * } + * ``` + * Here, it's invalid for scope@1 to take `arr.length` as a dependency instead + * of $1, as the evaluation of `arr.length` changes between instructions $1 and + * $3. We do not track $1 -> arr.length in this case. + */ +function collectTemporariesSidemap( + fn: HIRFunction, + usedOutsideDeclaringScope: ReadonlySet, +): ReadonlyMap { + const temporaries = new Map(); + for (const [_, block] of fn.body.blocks) { + for (const instr of block.instructions) { + const {value, lvalue} = instr; + const usedOutside = usedOutsideDeclaringScope.has( + lvalue.identifier.declarationId, + ); + + if (value.kind === 'PropertyLoad' && !usedOutside) { + const property = getProperty(value.object, value.property, temporaries); + temporaries.set(lvalue.identifier.id, property); + } else if ( + value.kind === 'LoadLocal' && + lvalue.identifier.name == null && + value.place.identifier.name !== null && + !usedOutside + ) { + temporaries.set(lvalue.identifier.id, { + identifier: value.place.identifier, + path: [], + }); + } + } + } + return temporaries; +} + +type Decl = { + id: InstructionId; + scope: Stack; +}; + +class Context { + #declarations: Map = new Map(); + #reassignments: Map = new Map(); + + #scopes: Stack = empty(); + // Reactive dependencies used in the current reactive scope. + #dependencies: Stack> = empty(); + deps: Map> = new Map(); + + #temporaries: ReadonlyMap; + #temporariesUsedOutsideScope: ReadonlySet; + + constructor( + temporariesUsedOutsideScope: ReadonlySet, + temporaries: ReadonlyMap, + ) { + this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope; + this.#temporaries = temporaries; + } + + enterScope(scope: ReactiveScope): void { + // Set context for new scope + this.#dependencies = this.#dependencies.push([]); + this.#scopes = this.#scopes.push(scope); + } + + exitScope(scope: ReactiveScope, pruned: boolean): void { + // Save dependencies we collected from the exiting scope + const scopedDependencies = this.#dependencies.value; + CompilerError.invariant(scopedDependencies != null, { + reason: '[PropagateScopeDeps]: Unexpected scope mismatch', + loc: scope.loc, + }); + + // Restore context of previous scope + this.#scopes = this.#scopes.pop(); + this.#dependencies = this.#dependencies.pop(); + + /* + * 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 (this.#checkValidDependency(dep)) { + this.#dependencies.value?.push(dep); + } + } + + if (!pruned) { + this.deps.set(scope, scopedDependencies); + } + } + + isUsedOutsideDeclaringScope(place: Place): boolean { + return this.#temporariesUsedOutsideScope.has( + place.identifier.declarationId, + ); + } + + /* + * 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.declarationId)) { + this.#declarations.set(identifier.declarationId, decl); + } + this.#reassignments.set(identifier, decl); + } + + // 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)?.property === '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.declarationId); + 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 { + /* + * 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. + */ + this.visitDependency( + this.#temporaries.get(place.identifier.id) ?? { + identifier: place.identifier, + path: [], + }, + ); + } + + visitProperty(object: Place, property: string): void { + const nextDependency = getProperty(object, property, this.#temporaries); + 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.declarationId, + ); + if ( + originalDeclaration !== undefined && + originalDeclaration.scope.value !== null + ) { + originalDeclaration.scope.each(scope => { + if ( + !this.#isScopeActive(scope) && + !Iterable_some( + scope.declarations.values(), + decl => + decl.identifier.declarationId === + maybeDependency.identifier.declarationId, + ) + ) { + scope.declarations.set(maybeDependency.identifier.id, { + identifier: maybeDependency.identifier, + scope: originalDeclaration.scope.value!, + }); + } + }); + } + + if (this.#checkValidDependency(maybeDependency)) { + this.#dependencies.value!.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 && + !Iterable_some( + currentScope.reassignments, + identifier => + identifier.declarationId === place.identifier.declarationId, + ) && + this.#checkValidDependency({identifier: place.identifier, path: []}) + ) { + currentScope.reassignments.add(place.identifier); + } + } +} + +function handleInstruction(instr: Instruction, context: Context): void { + 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 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: ReadonlySet, + temporaries: ReadonlyMap, +): Map> { + const context = new Context(usedOutsideDeclaringScope, temporaries); + + 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(), + }); + } + } + + const scopeTraversal = new ScopeBlockTraversal(); + + for (const [blockId, block] of fn.body.blocks) { + scopeTraversal.recordScopes(block); + const scopeBlockInfo = scopeTraversal.blockInfos.get(blockId); + if (scopeBlockInfo?.kind === 'begin') { + context.enterScope(scopeBlockInfo.scope); + } else if (scopeBlockInfo?.kind === 'end') { + context.exitScope(scopeBlockInfo.scope, scopeBlockInfo?.pruned); + } + + 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: ReadonlyMap, + scopeId: ScopeId, + tree: ReactiveScopeDependencyTreeHIR, +): void { + const node = nodes.get(scopeId); + CompilerError.invariant(node != null, { + reason: '[PropagateScopeDependencies] Scope not found in tracked blocks', + loc: GeneratedSource, + }); + + for (const item of node.assumedNonNullObjects) { + tree.markNodesNonNull({ + ...item.fullPath, + }); + } +} 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 904b7a4038dec..217bc3132bd14 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -6,7 +6,9 @@ */ import {assertExhaustive} from '../Utils/utils'; +import {CompilerError} from '..'; import { + BasicBlock, BlockId, Instruction, InstructionValue, @@ -14,7 +16,9 @@ import { Pattern, Place, ReactiveInstruction, + ReactiveScope, ReactiveValue, + ScopeId, SpreadPattern, Terminal, } from './HIR'; @@ -1149,3 +1153,72 @@ export function* eachTerminalOperand(terminal: Terminal): Iterable { } } } + +/** + * Helper class for traversing scope blocks in HIR-form. + */ +export class ScopeBlockTraversal { + // Live stack of active scopes + #activeScopes: Array = []; + blockInfos: Map< + BlockId, + | { + kind: 'end'; + scope: ReactiveScope; + pruned: boolean; + } + | { + kind: 'begin'; + scope: ReactiveScope; + pruned: boolean; + fallthrough: BlockId; + } + > = new Map(); + + recordScopes(block: BasicBlock): void { + const blockInfo = this.blockInfos.get(block.id); + if (blockInfo?.kind === 'begin') { + this.#activeScopes.push(blockInfo.scope.id); + } else if (blockInfo?.kind === 'end') { + 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(); + } + + if ( + block.terminal.kind === 'scope' || + block.terminal.kind === 'pruned-scope' + ) { + CompilerError.invariant( + !this.blockInfos.has(block.terminal.block) && + !this.blockInfos.has(block.terminal.fallthrough), + { + 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, + }); + this.blockInfos.set(block.terminal.fallthrough, { + kind: 'end', + scope: block.terminal.scope, + pruned: block.terminal.kind === 'pruned-scope', + }); + } + } + + isScopeActive(scopeId: ScopeId): boolean { + return this.#activeScopes.indexOf(scopeId) !== -1; + } + get currentScope(): ScopeId | null { + return this.#activeScopes.at(-1) ?? null; + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts index 126772f591b41..0d0b37ce58afe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/InferReactiveScopeVariables.ts @@ -13,6 +13,8 @@ import { HIRFunction, Identifier, Instruction, + InstructionId, + MutableRange, Place, ReactiveScope, makeInstructionId, @@ -186,8 +188,14 @@ function mergeLocation(l: SourceLocation, r: SourceLocation): SourceLocation { } // Is the operand mutable at this given instruction -export function isMutable({id}: Instruction, place: Place): boolean { - const range = place.identifier.mutableRange; +export function isMutable(instr: {id: InstructionId}, place: Place): boolean { + return inRange(instr, place.identifier.mutableRange); +} + +export function inRange( + {id}: {id: InstructionId}, + range: MutableRange, +): boolean { return id >= range.start && id < range.end; } 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 6e07e14a8d0e6..6b813d597560c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -83,16 +83,33 @@ 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); - } +export function Set_union(a: ReadonlySet, b: ReadonlySet): Set { + 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 Iterable_some( iter: Iterable, pred: (item: T) => boolean, 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 fdb7203785716..26b56ea2a4f4d 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 @@ -10,7 +10,7 @@ function Component(props) { x = identity(props.value[0]); }; foo(); - return {x}; + return
{x}
; } export const FIXTURE_ENTRYPOINT = { @@ -45,7 +45,7 @@ function Component(props) { const t0 = x; let t1; if ($[2] !== t0) { - t1 = { x: t0 }; + t1 =
{t0}
; $[2] = t0; $[3] = t1; } else { @@ -62,4 +62,4 @@ export const FIXTURE_ENTRYPOINT = { ``` ### Eval output -(kind: ok) {"x":42} \ No newline at end of file +(kind: ok)
42
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.js index 55675de9abb40..bc9324a35f06d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/destructure-array-declaration-to-context-var.js @@ -6,7 +6,7 @@ function Component(props) { x = identity(props.value[0]); }; foo(); - return {x}; + return
{x}
; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.expect.md new file mode 100644 index 0000000000000..acad3c3092126 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +/** + * Test that we preserve order of evaluation on the following case scope@0 + * ```js + * // simplified HIR + * scope@0 + * ... + * $0 = arr.length + * $1 = arr.push(...) + * + * scope@1 <-- here we should depend on $0 (the value of the property load before the + * mutable call) + * [$0, $1] + * ``` + */ +function useFoo(source: Array): [number, number] { + const arr = [1, 2, 3, ...source]; + return [arr.length, arr.push(0)]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [[5, 6]], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; /** + * Test that we preserve order of evaluation on the following case scope@0 + * ```js + * // simplified HIR + * scope@0 + * ... + * $0 = arr.length + * $1 = arr.push(...) + * + * scope@1 <-- here we should depend on $0 (the value of the property load before the + * mutable call) + * [$0, $1] + * ``` + */ +function useFoo(source) { + const $ = _c(6); + let t0; + let t1; + if ($[0] !== source) { + const arr = [1, 2, 3, ...source]; + t0 = arr.length; + t1 = arr.push(0); + $[0] = source; + $[1] = t0; + $[2] = t1; + } else { + t0 = $[1]; + t1 = $[2]; + } + let t2; + if ($[3] !== t0 || $[4] !== t1) { + t2 = [t0, t1]; + $[3] = t0; + $[4] = t1; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [[5, 6]], +}; + +``` + +### Eval output +(kind: ok) [5,6] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.ts new file mode 100644 index 0000000000000..c2fa617f51c73 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-call-after-dependency-load.ts @@ -0,0 +1,23 @@ +/** + * Test that we preserve order of evaluation on the following case scope@0 + * ```js + * // simplified HIR + * scope@0 + * ... + * $0 = arr.length + * $1 = arr.push(...) + * + * scope@1 <-- here we should depend on $0 (the value of the property load before the + * mutable call) + * [$0, $1] + * ``` + */ +function useFoo(source: Array): [number, number] { + const arr = [1, 2, 3, ...source]; + return [arr.length, arr.push(0)]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [[5, 6]], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.expect.md new file mode 100644 index 0000000000000..b2bf1e36a4878 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.expect.md @@ -0,0 +1,83 @@ + +## Input + +```javascript +/** + * Test that we preserve order of evaluation on the following case scope@0 + * ```js + * // simplified HIR + * scope@0 + * ... + * $0 = arr.length + * $1 = arr.length = 0 + * + * scope@1 <-- here we should depend on $0 (the value of the property load before the + * property store) + * [$0, $1] + * ``` + */ +function useFoo(source: Array): [number, number] { + const arr = [1, 2, 3, ...source]; + return [arr.length, (arr.length = 0)]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [[5, 6]], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; /** + * Test that we preserve order of evaluation on the following case scope@0 + * ```js + * // simplified HIR + * scope@0 + * ... + * $0 = arr.length + * $1 = arr.length = 0 + * + * scope@1 <-- here we should depend on $0 (the value of the property load before the + * property store) + * [$0, $1] + * ``` + */ +function useFoo(source) { + const $ = _c(6); + let t0; + let t1; + if ($[0] !== source) { + const arr = [1, 2, 3, ...source]; + t0 = arr.length; + t1 = arr.length = 0; + $[0] = source; + $[1] = t0; + $[2] = t1; + } else { + t0 = $[1]; + t1 = $[2]; + } + let t2; + if ($[3] !== t0 || $[4] !== t1) { + t2 = [t0, t1]; + $[3] = t0; + $[4] = t1; + $[5] = t2; + } else { + t2 = $[5]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [[5, 6]], +}; + +``` + +### Eval output +(kind: ok) [5,0] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.ts new file mode 100644 index 0000000000000..8798cd99c7f77 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/evaluation-order-mutate-store-after-dependency-load.ts @@ -0,0 +1,23 @@ +/** + * Test that we preserve order of evaluation on the following case scope@0 + * ```js + * // simplified HIR + * scope@0 + * ... + * $0 = arr.length + * $1 = arr.length = 0 + * + * scope@1 <-- here we should depend on $0 (the value of the property load before the + * property store) + * [$0, $1] + * ``` + */ +function useFoo(source: Array): [number, number] { + const arr = [1, 2, 3, ...source]; + return [arr.length, (arr.length = 0)]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [[5, 6]], +}; 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..8cbaeb3f89465 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.expect.md @@ -0,0 +1,67 @@ + +## Input + +```javascript +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false +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 @enablePropagateDepsInHIR:false +import { Stringify } from "shared-runtime"; + +function Component(t0) { + const $ = _c(7); + 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 = props == null ? _temp : f; + $[2] = props; + $[3] = f; + $[4] = t2; + } else { + t2 = $[4]; + } + let t3; + if ($[5] !== t2) { + t3 = ; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} +function _temp() {} + +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..2ede54db5f364 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr-conditional-access-2.tsx @@ -0,0 +1,12 @@ +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false +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 eb05286fa29a5..f2fa20feb5477 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" @@ -2,7 +2,7 @@ ## Input ```javascript -// @enableTreatFunctionDepsAsConditional +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false function Component(props) { function getLength() { return props.bar.length; @@ -21,7 +21,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional +import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false function Component(props) { const $ = _c(5); let t0; diff --git "a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.js" "b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.js" index 6e59fb947d150..9bff3e5cdb53b 100644 --- "a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.js" +++ "b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.js" @@ -1,4 +1,4 @@ -// @enableTreatFunctionDepsAsConditional +// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false function Component(props) { function getLength() { return props.bar.length; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.expect.md new file mode 100644 index 0000000000000..e885982310117 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +function Component(props) { + const data = useMemo(() => { + return props?.items.edges?.nodes.map(); + }, [props?.items.edges?.nodes]); + return ; +} + +``` + + +## Error + +``` + 1 | // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR + 2 | function Component(props) { +> 3 | const data = useMemo(() => { + | ^^^^^^^ +> 4 | return props?.items.edges?.nodes.map(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 5 | }, [props?.items.edges?.nodes]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:5) + 6 | return ; + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.expect.md new file mode 100644 index 0000000000000..3559b2bd58b28 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + x.push(props.items); + return x; + }, [props.items]); + return ; +} + +``` + + +## Error + +``` + 2 | import {ValidateMemoization} from 'shared-runtime'; + 3 | function Component(props) { +> 4 | const data = useMemo(() => { + | ^^^^^^^ +> 5 | const x = []; + | ^^^^^^^^^^^^^^^^^ +> 6 | x.push(props?.items); + | ^^^^^^^^^^^^^^^^^ +> 7 | x.push(props.items); + | ^^^^^^^^^^^^^^^^^ +> 8 | return x; + | ^^^^^^^^^^^^^^^^^ +> 9 | }, [props.items]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:9) + 10 | return ; + 11 | } + 12 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md new file mode 100644 index 0000000000000..429f168836b82 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + return x; + }, [props?.items]); + return ; +} + +``` + + +## Error + +``` + 2 | import {ValidateMemoization} from 'shared-runtime'; + 3 | function Component(props) { +> 4 | const data = useMemo(() => { + | ^^^^^^^ +> 5 | const x = []; + | ^^^^^^^^^^^^^^^^^ +> 6 | x.push(props?.items); + | ^^^^^^^^^^^^^^^^^ +> 7 | return x; + | ^^^^^^^^^^^^^^^^^ +> 8 | }, [props?.items]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:8) + 9 | return ; + 10 | } + 11 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional-optional.expect.md new file mode 100644 index 0000000000000..d54ba8d0ecfe6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional-optional.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + if (props.cond) { + x.push(props?.items); + } + return x; + }, [props?.items, props.cond]); + return ( + + ); +} + +``` + + +## Error + +``` + 2 | import {ValidateMemoization} from 'shared-runtime'; + 3 | function Component(props) { +> 4 | const data = useMemo(() => { + | ^^^^^^^ +> 5 | const x = []; + | ^^^^^^^^^^^^^^^^^ +> 6 | x.push(props?.items); + | ^^^^^^^^^^^^^^^^^ +> 7 | if (props.cond) { + | ^^^^^^^^^^^^^^^^^ +> 8 | x.push(props?.items); + | ^^^^^^^^^^^^^^^^^ +> 9 | } + | ^^^^^^^^^^^^^^^^^ +> 10 | return x; + | ^^^^^^^^^^^^^^^^^ +> 11 | }, [props?.items, props.cond]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11) + 12 | return ( + 13 | + 14 | ); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional-optional.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional-optional.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional-optional.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional.expect.md new file mode 100644 index 0000000000000..1b2c10a9ac777 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + if (props.cond) { + x.push(props.items); + } + return x; + }, [props?.items, props.cond]); + return ( + + ); +} + +``` + + +## Error + +``` + 2 | import {ValidateMemoization} from 'shared-runtime'; + 3 | function Component(props) { +> 4 | const data = useMemo(() => { + | ^^^^^^^ +> 5 | const x = []; + | ^^^^^^^^^^^^^^^^^ +> 6 | x.push(props?.items); + | ^^^^^^^^^^^^^^^^^ +> 7 | if (props.cond) { + | ^^^^^^^^^^^^^^^^^ +> 8 | x.push(props.items); + | ^^^^^^^^^^^^^^^^^ +> 9 | } + | ^^^^^^^^^^^^^^^^^ +> 10 | return x; + | ^^^^^^^^^^^^^^^^^ +> 11 | }, [props?.items, props.cond]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:11) + 12 | return ( + 13 | + 14 | ); +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-with-conditional.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md deleted file mode 100644 index 4c4eee6cb5fa2..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md +++ /dev/null @@ -1,48 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -function Component(props) { - const data = useMemo(() => { - return props?.items.edges?.nodes.map(); - }, [props?.items.edges?.nodes]); - return ; -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -function Component(props) { - const $ = _c(4); - - props?.items.edges?.nodes; - let t0; - let t1; - if ($[0] !== props?.items.edges?.nodes) { - t1 = props?.items.edges?.nodes.map(); - $[0] = props?.items.edges?.nodes; - $[1] = t1; - } else { - t1 = $[1]; - } - t0 = t1; - const data = t0; - let t2; - if ($[2] !== data) { - t2 = ; - $[2] = data; - $[3] = t2; - } else { - t2 = $[3]; - } - return t2; -} - -``` - -### 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/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.expect.md deleted file mode 100644 index b4a55fcb61eee..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.expect.md +++ /dev/null @@ -1,62 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {ValidateMemoization} from 'shared-runtime'; -function Component(props) { - const data = useMemo(() => { - const x = []; - x.push(props?.items); - x.push(props.items); - return x; - }, [props.items]); - return ; -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import { ValidateMemoization } from "shared-runtime"; -function Component(props) { - const $ = _c(7); - let t0; - let x; - if ($[0] !== props.items) { - x = []; - x.push(props?.items); - x.push(props.items); - $[0] = props.items; - $[1] = x; - } else { - x = $[1]; - } - t0 = x; - const data = t0; - let t1; - if ($[2] !== props.items) { - t1 = [props.items]; - $[2] = props.items; - $[3] = t1; - } else { - t1 = $[3]; - } - let t2; - if ($[4] !== t1 || $[5] !== data) { - t2 = ; - $[4] = t1; - $[5] = data; - $[6] = t2; - } else { - t2 = $[6]; - } - return t2; -} - -``` - -### 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/propagate-scope-deps-hir-fork/optional-member-expression-single.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.expect.md deleted file mode 100644 index 73bac78f93bea..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.expect.md +++ /dev/null @@ -1,63 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {ValidateMemoization} from 'shared-runtime'; -function Component(props) { - const data = useMemo(() => { - const x = []; - x.push(props?.items); - return x; - }, [props?.items]); - return ; -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import { ValidateMemoization } from "shared-runtime"; -function Component(props) { - const $ = _c(7); - - props?.items; - let t0; - let x; - if ($[0] !== props?.items) { - x = []; - x.push(props?.items); - $[0] = props?.items; - $[1] = x; - } else { - x = $[1]; - } - t0 = x; - const data = t0; - const t1 = props?.items; - let t2; - if ($[2] !== t1) { - t2 = [t1]; - $[2] = t1; - $[3] = t2; - } else { - t2 = $[3]; - } - let t3; - if ($[4] !== t2 || $[5] !== data) { - t3 = ; - $[4] = t2; - $[5] = data; - $[6] = t3; - } else { - t3 = $[6]; - } - return t3; -} - -``` - -### 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/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional-optional.expect.md deleted file mode 100644 index 59db7ab3c3426..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional-optional.expect.md +++ /dev/null @@ -1,74 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {ValidateMemoization} from 'shared-runtime'; -function Component(props) { - const data = useMemo(() => { - const x = []; - x.push(props?.items); - if (props.cond) { - x.push(props?.items); - } - return x; - }, [props?.items, props.cond]); - return ( - - ); -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import { ValidateMemoization } from "shared-runtime"; -function Component(props) { - const $ = _c(9); - - props?.items; - let t0; - let x; - if ($[0] !== props?.items || $[1] !== props.cond) { - x = []; - x.push(props?.items); - if (props.cond) { - x.push(props?.items); - } - $[0] = props?.items; - $[1] = props.cond; - $[2] = x; - } else { - x = $[2]; - } - t0 = x; - const data = t0; - - const t1 = props?.items; - let t2; - if ($[3] !== t1 || $[4] !== props.cond) { - t2 = [t1, props.cond]; - $[3] = t1; - $[4] = props.cond; - $[5] = t2; - } else { - t2 = $[5]; - } - let t3; - if ($[6] !== t2 || $[7] !== data) { - t3 = ; - $[6] = t2; - $[7] = data; - $[8] = t3; - } else { - t3 = $[8]; - } - return t3; -} - -``` - -### 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/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional.expect.md deleted file mode 100644 index 8d656e231aa3f..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-with-conditional.expect.md +++ /dev/null @@ -1,74 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {ValidateMemoization} from 'shared-runtime'; -function Component(props) { - const data = useMemo(() => { - const x = []; - x.push(props?.items); - if (props.cond) { - x.push(props.items); - } - return x; - }, [props?.items, props.cond]); - return ( - - ); -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import { ValidateMemoization } from "shared-runtime"; -function Component(props) { - const $ = _c(9); - - props?.items; - let t0; - let x; - if ($[0] !== props?.items || $[1] !== props.cond) { - x = []; - x.push(props?.items); - if (props.cond) { - x.push(props.items); - } - $[0] = props?.items; - $[1] = props.cond; - $[2] = x; - } else { - x = $[2]; - } - t0 = x; - const data = t0; - - const t1 = props?.items; - let t2; - if ($[3] !== t1 || $[4] !== props.cond) { - t2 = [t1, props.cond]; - $[3] = t1; - $[4] = props.cond; - $[5] = t2; - } else { - t2 = $[5]; - } - let t3; - if ($[6] !== t2 || $[7] !== data) { - t3 = ; - $[6] = t2; - $[7] = data; - $[8] = t3; - } else { - t3 = $[8]; - } - return t3; -} - -``` - -### 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/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md index f13bfe7d61705..d56dcb63aed18 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md @@ -31,10 +31,10 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function Component(props) { const $ = _c(2); let x; - if ($[0] !== props.a?.b) { + if ($[0] !== props.a) { x = []; x.push(props.a?.b); - $[0] = props.a?.b; + $[0] = props.a; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md index 733657746cc5c..af49da4b64854 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md @@ -63,20 +63,13 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md index a13a918a312cb..0f155c79dea93 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md @@ -46,11 +46,11 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function Component(props) { const $ = _c(2); let x; - if ($[0] !== props.a.b) { + if ($[0] !== props.a) { x = []; x.push(props.a?.b); x.push(props.a.b.c); - $[0] = props.a.b; + $[0] = props.a; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md index df9dec4fb6827..cf2d1d413741e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md @@ -22,25 +22,16 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function Component(props) { - const $ = _c(5); + const $ = _c(2); let x; - if ($[0] !== props.items?.length || $[1] !== props.items?.edges) { + if ($[0] !== props.items) { x = []; x.push(props.items?.length); - let t0; - if ($[3] !== props.items?.edges) { - t0 = props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []; - $[3] = props.items?.edges; - $[4] = t0; - } else { - t0 = $[4]; - } - x.push(t0); - $[0] = props.items?.length; - $[1] = props.items?.edges; - $[2] = x; + x.push(props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []); + $[0] = props.items; + $[1] = x; } else { - x = $[2]; + x = $[1]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/promote-uncond.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/promote-uncond.expect.md index 81d972d5b9113..902a1578c8870 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/promote-uncond.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/promote-uncond.expect.md @@ -36,19 +36,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/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md index 39f301432e51f..cf4e4f93274bb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md @@ -24,14 +24,14 @@ function HomeDiscoStoreItemTileRating(props) { const $ = _c(4); const item = useFragment(); let count; - if ($[0] !== item?.aggregates) { + if ($[0] !== item) { count = 0; const aggregates = item?.aggregates || []; aggregates.forEach((aggregate) => { count = count + (aggregate.count || 0); count; }); - $[0] = item?.aggregates; + $[0] = item; $[1] = count; } else { count = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-cascading-eliminated-phis.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-cascading-eliminated-phis.expect.md index f11300a7557df..6277ffbf404fe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-cascading-eliminated-phis.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-cascading-eliminated-phis.expect.md @@ -37,10 +37,16 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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); @@ -54,13 +60,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/propagate-scope-deps-hir-fork/ssa-leave-case.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-leave-case.expect.md index 6205a6e50900f..cc6713b29503e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-leave-case.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-leave-case.expect.md @@ -40,9 +40,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR import { Stringify } from "shared-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) { @@ -56,10 +56,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/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction-with-mutation.expect.md index 6dfdb906bd26d..eb623477049be 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction-with-mutation.expect.md @@ -32,17 +32,19 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR import { mutate } from "shared-runtime"; function useFoo(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; mutate(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/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction.expect.md index 1f2bb8f4f933b..a737b1809469c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-destruction.expect.md @@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function useFoo(props) { - const $ = _c(4); + const $ = _c(5); let x; if ($[0] !== props.bar) { x = []; @@ -37,12 +37,13 @@ function useFoo(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/propagate-scope-deps-hir-fork/ssa-renaming-ternary-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-with-mutation.expect.md index e8808bd9520fe..6a2184712a812 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary-with-mutation.expect.md @@ -32,17 +32,19 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR import { mutate } from "shared-runtime"; function useFoo(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; mutate(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/propagate-scope-deps-hir-fork/ssa-renaming-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary.expect.md index 1ba20962f74e0..a6750238dc303 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-ternary.expect.md @@ -27,7 +27,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function useFoo(props) { - const $ = _c(4); + const $ = _c(5); let x; if ($[0] !== props.bar) { x = []; @@ -37,12 +37,13 @@ function useFoo(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/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary-with-mutation.expect.md index 00917d11efc7d..062c7ac8fca30 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary-with-mutation.expect.md @@ -32,17 +32,19 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR import { arrayPush } from "shared-runtime"; function useFoo(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/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary.expect.md index 67590ad846242..c5cf366fb6101 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-unconditional-ternary.expect.md @@ -29,7 +29,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function useFoo(props) { - const $ = _c(4); + const $ = _c(6); let x; if ($[0] !== props.bar) { x = []; @@ -39,12 +39,14 @@ function useFoo(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/propagate-scope-deps-hir-fork/ssa-renaming-via-destructuring-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-via-destructuring-with-mutation.expect.md index 583c32f2f63df..42288dda64247 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-via-destructuring-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-via-destructuring-with-mutation.expect.md @@ -36,9 +36,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR import { mutate } from "shared-runtime"; function useFoo(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) { @@ -47,10 +47,12 @@ function useFoo(props) { } mutate(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/propagate-scope-deps-hir-fork/ssa-renaming-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-with-mutation.expect.md index 92a7e6b25c79c..bac466cd6e1e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/ssa-renaming-with-mutation.expect.md @@ -36,9 +36,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR import { mutate } from "shared-runtime"; function useFoo(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) { @@ -47,10 +47,12 @@ function useFoo(props) { } mutate(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/propagate-scope-deps-hir-fork/switch-non-final-default.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch-non-final-default.expect.md index aa122bc849039..37846215b1d9e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch-non-final-default.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch-non-final-default.expect.md @@ -34,10 +34,10 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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: { @@ -46,11 +46,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; } @@ -63,23 +63,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/propagate-scope-deps-hir-fork/switch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch.expect.md index 6c48b781a2992..1be4143849266 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/switch.expect.md @@ -29,10 +29,10 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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: { @@ -45,23 +45,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/propagate-scope-deps-hir-fork/try-catch-mutate-outer-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-mutate-outer-value.expect.md index e47a528fd375a..4fef6820554e1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-mutate-outer-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-mutate-outer-value.expect.md @@ -29,9 +29,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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; @@ -43,9 +43,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/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md index f69994b0a8531..914001f3737bd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md @@ -32,9 +32,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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); @@ -44,11 +44,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/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md index bc47228371f87..30ecdf6d59e9d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md @@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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 { @@ -47,11 +47,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/propagate-scope-deps-hir-fork/useMemo-multiple-if-else.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/useMemo-multiple-if-else.expect.md index 2c37f63189e8b..d654221dc837e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/useMemo-multiple-if-else.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/useMemo-multiple-if-else.expect.md @@ -34,11 +34,16 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR 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); @@ -49,12 +54,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/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.expect.md new file mode 100644 index 0000000000000..701702f9dd7ff --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.expect.md @@ -0,0 +1,107 @@ + +## Input + +```javascript +import {makeObject_Primitives, setPropertyByKey} from 'shared-runtime'; + +function useFoo({value, cond}) { + let x: any = makeObject_Primitives(); + if (cond) { + setPropertyByKey(x, 'a', null); + } else { + setPropertyByKey(x, 'a', {b: 2}); + } + + /** + * y should take a dependency on `x`, not `x.a.b` here + */ + const y = []; + if (!cond) { + y.push(x.a.b); + } + + x = makeObject_Primitives(); + setPropertyByKey(x, 'a', {b: value}); + + return [y, x.a.b]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{value: 3, cond: true}], + sequentialRenders: [ + {value: 3, cond: true}, + {value: 3, cond: false}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { makeObject_Primitives, setPropertyByKey } from "shared-runtime"; + +function useFoo(t0) { + const $ = _c(10); + const { value, cond } = t0; + let x; + if ($[0] !== cond) { + x = makeObject_Primitives(); + if (cond) { + setPropertyByKey(x, "a", null); + } else { + setPropertyByKey(x, "a", { b: 2 }); + } + $[0] = cond; + $[1] = x; + } else { + x = $[1]; + } + let y; + if ($[2] !== cond || $[3] !== x) { + y = []; + if (!cond) { + y.push(x.a.b); + } + $[2] = cond; + $[3] = x; + $[4] = y; + } else { + y = $[4]; + } + if ($[5] !== value) { + x = makeObject_Primitives(); + setPropertyByKey(x, "a", { b: value }); + $[5] = value; + $[6] = x; + } else { + x = $[6]; + } + let t1; + if ($[7] !== y || $[8] !== x.a.b) { + t1 = [y, x.a.b]; + $[7] = y; + $[8] = x.a.b; + $[9] = t1; + } else { + t1 = $[9]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{ value: 3, cond: true }], + sequentialRenders: [ + { value: 3, cond: true }, + { value: 3, cond: false }, + ], +}; + +``` + +### Eval output +(kind: ok) [[],3] +[[2],3] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.tsx new file mode 100644 index 0000000000000..3f75571bd700c --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance.tsx @@ -0,0 +1,32 @@ +import {makeObject_Primitives, setPropertyByKey} from 'shared-runtime'; + +function useFoo({value, cond}) { + let x: any = makeObject_Primitives(); + if (cond) { + setPropertyByKey(x, 'a', null); + } else { + setPropertyByKey(x, 'a', {b: 2}); + } + + /** + * y should take a dependency on `x`, not `x.a.b` here + */ + const y = []; + if (!cond) { + y.push(x.a.b); + } + + x = makeObject_Primitives(); + setPropertyByKey(x, 'a', {b: value}); + + return [y, x.a.b]; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [{value: 3, cond: true}], + sequentialRenders: [ + {value: 3, cond: true}, + {value: 3, cond: false}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.expect.md new file mode 100644 index 0000000000000..54ee5676b3f6d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.expect.md @@ -0,0 +1,96 @@ + +## Input + +```javascript +import {identity, shallowCopy, Stringify, useIdentity} from 'shared-runtime'; + +type HasA = {kind: 'hasA'; a: {value: number}}; +type HasC = {kind: 'hasC'; c: {value: number}}; +function Foo({cond}: {cond: boolean}) { + let x: HasA | HasC = shallowCopy({kind: 'hasA', a: {value: 2}}); + /** + * This read of x.a.value is outside of x's identifier mutable + * range + scope range. We mark this ssa instance (x_@0) as having + * a non-null object property `x.a`. + */ + Math.max(x.a.value, 2); + if (cond) { + x = shallowCopy({kind: 'hasC', c: {value: 3}}); + } + + /** + * Since this x (x_@2 = phi(x_@0, x_@1)) is a different ssa instance, + * we cannot safely hoist a read of `x.a.value` + */ + return ; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{cond: false}], + sequentialRenders: [{cond: false}, {cond: true}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { identity, shallowCopy, Stringify, useIdentity } from "shared-runtime"; + +type HasA = { kind: "hasA"; a: { value: number } }; +type HasC = { kind: "hasC"; c: { value: number } }; +function Foo(t0) { + const $ = _c(7); + const { cond } = t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = shallowCopy({ kind: "hasA", a: { value: 2 } }); + $[0] = t1; + } else { + t1 = $[0]; + } + let x = t1; + + Math.max(x.a.value, 2); + if (cond) { + let t2; + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + t2 = shallowCopy({ kind: "hasC", c: { value: 3 } }); + $[1] = t2; + } else { + t2 = $[1]; + } + x = t2; + } + let t2; + if ($[2] !== cond || $[3] !== x) { + t2 = !cond && [(x as HasA).a.value + 2]; + $[2] = cond; + $[3] = x; + $[4] = t2; + } else { + t2 = $[4]; + } + let t3; + if ($[5] !== t2) { + t3 = ; + $[5] = t2; + $[6] = t3; + } else { + t3 = $[6]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ cond: false }], + sequentialRenders: [{ cond: false }, { cond: true }], +}; + +``` + +### Eval output +(kind: ok)
{"val":[4]}
+
{"val":false}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.tsx new file mode 100644 index 0000000000000..147ca85809355 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/hoist-deps-diff-ssa-instance1.tsx @@ -0,0 +1,27 @@ +import {identity, shallowCopy, Stringify, useIdentity} from 'shared-runtime'; + +type HasA = {kind: 'hasA'; a: {value: number}}; +type HasC = {kind: 'hasC'; c: {value: number}}; +function Foo({cond}: {cond: boolean}) { + let x: HasA | HasC = shallowCopy({kind: 'hasA', a: {value: 2}}); + /** + * This read of x.a.value is outside of x's identifier mutable + * range + scope range. We mark this ssa instance (x_@0) as having + * a non-null object property `x.a`. + */ + Math.max(x.a.value, 2); + if (cond) { + x = shallowCopy({kind: 'hasC', c: {value: 3}}); + } + + /** + * Since this x (x_@2 = phi(x_@0, x_@1)) is a different ssa instance, + * we cannot safely hoist a read of `x.a.value` + */ + return ; +} +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{cond: false}], + sequentialRenders: [{cond: false}, {cond: true}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.expect.md new file mode 100644 index 0000000000000..ccd81b3e14425 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.expect.md @@ -0,0 +1,114 @@ + +## Input + +```javascript +import { + identity, + makeObject_Primitives, + setPropertyByKey, +} from 'shared-runtime'; + +/** + * A bit of an edge case, but we could further optimize here by merging + * re-orderability of nodes across phis. + */ +function useFoo(cond) { + let x; + if (cond) { + /** start of scope for x_@0 */ + x = {}; + setPropertyByKey(x, 'a', {b: 2}); + /** end of scope for x_@0 */ + Math.max(x.a.b, 0); + } else { + /** start of scope for x_@1 */ + x = makeObject_Primitives(); + setPropertyByKey(x, 'a', {b: 3}); + /** end of scope for x_@1 */ + Math.max(x.a.b, 0); + } + /** + * At this point, we have a phi node. + * x_@2 = phi(x_@0, x_@1) + * + * We can assume that both x_@0 and x_@1 both have non-null `x.a` properties, + * so we can infer that x_@2 does as well. + */ + + // Here, y should take a dependency on `x.a.b` + const y = []; + if (identity(cond)) { + y.push(x.a.b); + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { + identity, + makeObject_Primitives, + setPropertyByKey, +} from "shared-runtime"; + +/** + * A bit of an edge case, but we could further optimize here by merging + * re-orderability of nodes across phis. + */ +function useFoo(cond) { + const $ = _c(5); + let x; + if (cond) { + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + x = {}; + setPropertyByKey(x, "a", { b: 2 }); + $[0] = x; + } else { + x = $[0]; + } + + Math.max(x.a.b, 0); + } else { + if ($[1] === Symbol.for("react.memo_cache_sentinel")) { + x = makeObject_Primitives(); + setPropertyByKey(x, "a", { b: 3 }); + $[1] = x; + } else { + x = $[1]; + } + + Math.max(x.a.b, 0); + } + let y; + if ($[2] !== cond || $[3] !== x) { + y = []; + if (identity(cond)) { + y.push(x.a.b); + } + $[2] = cond; + $[3] = x; + $[4] = y; + } else { + y = $[4]; + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; + +``` + +### Eval output +(kind: ok) [2] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.ts new file mode 100644 index 0000000000000..749b6c0a831c0 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-merge-ssa-phi-access-nodes.ts @@ -0,0 +1,45 @@ +import { + identity, + makeObject_Primitives, + setPropertyByKey, +} from 'shared-runtime'; + +/** + * A bit of an edge case, but we could further optimize here by merging + * re-orderability of nodes across phis. + */ +function useFoo(cond) { + let x; + if (cond) { + /** start of scope for x_@0 */ + x = {}; + setPropertyByKey(x, 'a', {b: 2}); + /** end of scope for x_@0 */ + Math.max(x.a.b, 0); + } else { + /** start of scope for x_@1 */ + x = makeObject_Primitives(); + setPropertyByKey(x, 'a', {b: 3}); + /** end of scope for x_@1 */ + Math.max(x.a.b, 0); + } + /** + * At this point, we have a phi node. + * x_@2 = phi(x_@0, x_@1) + * + * We can assume that both x_@0 and x_@1 both have non-null `x.a` properties, + * so we can infer that x_@2 does as well. + */ + + // Here, y should take a dependency on `x.a.b` + const y = []; + if (identity(cond)) { + y.push(x.a.b); + } + return y; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [true], +}; 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..34979e9de9485 --- /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] +[null] +[null] \ 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..c4e4819f6b8fe --- /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/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..a2a0e3bef9b76 --- /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..ff000fd86699d --- /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..cf2ad80e7ac51 --- /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..be9f3e7ab9ce1 --- /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/snap/src/sprout/shared-runtime.ts b/compiler/packages/snap/src/sprout/shared-runtime.ts index 4426982ec78f9..0f3e09b12e127 100644 --- a/compiler/packages/snap/src/sprout/shared-runtime.ts +++ b/compiler/packages/snap/src/sprout/shared-runtime.ts @@ -98,6 +98,15 @@ export function setProperty(arg: any, property: any): void { } } +export function setPropertyByKey< + T, + TKey extends keyof T, + TProperty extends T[TKey], +>(arg: T, key: TKey, property: TProperty): T { + arg[key] = property; + return arg; +} + export function arrayPush(arr: Array, ...values: Array): void { arr.push(...values); } @@ -125,7 +134,7 @@ export function calculateExpensiveNumber(x: number): number { /** * Functions that do not mutate their parameters */ -export function shallowCopy(obj: object): object { +export function shallowCopy(obj: T): T { return Object.assign({}, obj); }