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 7ae520a144c9b..1127e91029328 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Entrypoint/Pipeline.ts @@ -57,7 +57,6 @@ import { mergeReactiveScopesThatInvalidateTogether, promoteUsedTemporaries, propagateEarlyReturns, - propagateScopeDependencies, pruneHoistedContexts, pruneNonEscapingScopes, pruneNonReactiveDependencies, @@ -348,14 +347,12 @@ function* runWithEnvironment( }); assertTerminalSuccessorsExist(hir); assertTerminalPredsExist(hir); - if (env.config.enablePropagateDepsInHIR) { - propagateScopeDependenciesHIR(hir); - yield log({ - kind: 'hir', - name: 'PropagateScopeDependenciesHIR', - value: hir, - }); - } + propagateScopeDependenciesHIR(hir); + yield log({ + kind: 'hir', + name: 'PropagateScopeDependenciesHIR', + value: hir, + }); if (env.config.inlineJsxTransform) { inlineJsxTransform(hir, env.config.inlineJsxTransform); @@ -383,15 +380,6 @@ function* runWithEnvironment( }); assertScopeInstructionsWithinScopes(reactiveFunction); - if (!env.config.enablePropagateDepsInHIR) { - propagateScopeDependencies(reactiveFunction); - yield log({ - kind: 'reactive', - name: 'PropagateScopeDependencies', - value: reactiveFunction, - }); - } - pruneNonEscapingScopes(reactiveFunction); yield log({ kind: 'reactive', diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts index 80593d6275868..d3c919a6d8afe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -1,5 +1,6 @@ import {CompilerError} from '../CompilerError'; import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables'; +import {printDependency} from '../ReactiveScopes/PrintReactiveFunction'; import { Set_equal, Set_filter, @@ -7,7 +8,6 @@ import { Set_union, getOrInsertDefault, } from '../Utils/utils'; -import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies'; import { BasicBlock, BlockId, @@ -21,7 +21,8 @@ import { ReactiveScopeDependency, ScopeId, } from './HIR'; -import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR'; + +const DEBUG_PRINT = false; /** * Helper function for `PropagateScopeDependencies`. Uses control flow graph @@ -86,15 +87,8 @@ export function collectHoistablePropertyLoads( fn: HIRFunction, temporaries: ReadonlyMap, hoistableFromOptionals: ReadonlyMap, - nestedFnImmutableContext: ReadonlySet | null, ): ReadonlyMap { const registry = new PropertyPathRegistry(); - - const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn); - const actuallyEvaluatedTemporaries = new Map( - [...temporaries].filter(([id]) => !functionExpressionLoads.has(id)), - ); - /** * 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 @@ -111,14 +105,51 @@ export function collectHoistablePropertyLoads( } } } - const nodes = collectNonNullsInBlocks(fn, { - temporaries: actuallyEvaluatedTemporaries, + return collectHoistablePropertyLoadsImpl(fn, { + temporaries, knownImmutableIdentifiers, hoistableFromOptionals, registry, - nestedFnImmutableContext, + nestedFnImmutableContext: null, }); - propagateNonNull(fn, nodes, registry); +} + +type CollectHoistablePropertyLoadsContext = { + temporaries: ReadonlyMap; + knownImmutableIdentifiers: ReadonlySet; + hoistableFromOptionals: ReadonlyMap; + registry: PropertyPathRegistry; + /** + * (For nested / inner function declarations) + * Context variables (i.e. captured from an outer scope) that are immutable. + * Note that this technically could be merged into `knownImmutableIdentifiers`, + * but are currently kept separate for readability. + */ + nestedFnImmutableContext: ReadonlySet | null; +}; +function collectHoistablePropertyLoadsImpl( + fn: HIRFunction, + context: CollectHoistablePropertyLoadsContext, +): ReadonlyMap { + const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn); + const actuallyEvaluatedTemporaries = new Map( + [...context.temporaries].filter(([id]) => !functionExpressionLoads.has(id)), + ); + + const nodes = collectNonNullsInBlocks(fn, { + ...context, + temporaries: actuallyEvaluatedTemporaries, + }); + propagateNonNull(fn, nodes, context.registry); + + if (DEBUG_PRINT) { + console.log('(printing hoistable nodes in blocks)'); + for (const [blockId, node] of nodes) { + console.log( + `bb${blockId}: ${[...node.assumedNonNullObjects].map(n => printDependency(n.fullPath)).join(' ')}`, + ); + } + } return nodes; } @@ -243,7 +274,7 @@ class PropertyPathRegistry { function getMaybeNonNullInInstruction( instr: InstructionValue, - context: CollectNonNullsInBlocksContext, + context: CollectHoistablePropertyLoadsContext, ): PropertyPathNode | null { let path = null; if (instr.kind === 'PropertyLoad') { @@ -262,7 +293,7 @@ function getMaybeNonNullInInstruction( function isImmutableAtInstr( identifier: Identifier, instr: InstructionId, - context: CollectNonNullsInBlocksContext, + context: CollectHoistablePropertyLoadsContext, ): boolean { if (context.nestedFnImmutableContext != null) { /** @@ -295,22 +326,9 @@ function isImmutableAtInstr( } } -type CollectNonNullsInBlocksContext = { - temporaries: ReadonlyMap; - knownImmutableIdentifiers: ReadonlySet; - hoistableFromOptionals: ReadonlyMap; - registry: PropertyPathRegistry; - /** - * (For nested / inner function declarations) - * Context variables (i.e. captured from an outer scope) that are immutable. - * Note that this technically could be merged into `knownImmutableIdentifiers`, - * but are currently kept separate for readability. - */ - nestedFnImmutableContext: ReadonlySet | null; -}; function collectNonNullsInBlocks( fn: HIRFunction, - context: CollectNonNullsInBlocksContext, + context: CollectHoistablePropertyLoadsContext, ): ReadonlyMap { /** * Known non-null objects such as functional component props can be safely @@ -348,27 +366,25 @@ function collectNonNullsInBlocks( assumedNonNullObjects.add(maybeNonNull); } if ( - instr.value.kind === 'FunctionExpression' && + (instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod') && !fn.env.config.enableTreatFunctionDepsAsConditional ) { const innerFn = instr.value.loweredFunc; - const innerTemporaries = collectTemporariesSidemap( - innerFn.func, - new Set(), - ); - const innerOptionals = collectOptionalChainSidemap(innerFn.func); - const innerHoistableMap = collectHoistablePropertyLoads( + const innerHoistableMap = collectHoistablePropertyLoadsImpl( innerFn.func, - innerTemporaries, - innerOptionals.hoistableObjects, - context.nestedFnImmutableContext ?? - new Set( - innerFn.func.context - .filter(place => - isImmutableAtInstr(place.identifier, instr.id, context), - ) - .map(place => place.identifier.id), - ), + { + ...context, + nestedFnImmutableContext: + context.nestedFnImmutableContext ?? + new Set( + innerFn.func.context + .filter(place => + isImmutableAtInstr(place.identifier, instr.id, context), + ) + .map(place => place.identifier.id), + ), + }, ); const innerHoistables = assertNonNull( innerHoistableMap.get(innerFn.func.body.entry), @@ -591,7 +607,10 @@ function collectFunctionExpressionFakeLoads( for (const [_, block] of fn.body.blocks) { for (const {lvalue, value} of block.instructions) { - if (value.kind === 'FunctionExpression') { + if ( + value.kind === 'FunctionExpression' || + value.kind === 'ObjectMethod' + ) { for (const reference of value.loweredFunc.dependencies) { let curr: IdentifierId | undefined = reference.identifier.id; while (curr != null) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts index 453294784246f..0167c996b177d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts @@ -1,4 +1,5 @@ import {CompilerError} from '..'; +import {getOrInsertDefault} from '../Utils/utils'; import {assertNonNull} from './CollectHoistablePropertyLoads'; import { BlockId, @@ -22,25 +23,14 @@ export function collectOptionalChainSidemap( fn: HIRFunction, ): OptionalChainSidemap { const context: OptionalTraversalContext = { + currFn: fn, blocks: fn.body.blocks, seenOptionals: new Set(), - processedInstrsInOptional: new Set(), + processedInstrsInOptional: new Map(), temporariesReadInOptional: new Map(), hoistableObjects: new Map(), }; - for (const [_, block] of fn.body.blocks) { - if ( - block.terminal.kind === 'optional' && - !context.seenOptionals.has(block.id) - ) { - traverseOptionalBlock( - block as TBasicBlock, - context, - null, - ); - } - } - + traverseFunction(fn, context); return { temporariesReadInOptional: context.temporariesReadInOptional, processedInstrsInOptional: context.processedInstrsInOptional, @@ -96,8 +86,13 @@ export type OptionalChainSidemap = { * bb5: * $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4! * ``` + * + * Also note that InstructionIds are not unique across inner functions. */ - processedInstrsInOptional: ReadonlySet; + processedInstrsInOptional: ReadonlyMap< + HIRFunction, + ReadonlySet + >; /** * Records optional chains for which we can safely evaluate non-optional * PropertyLoads. e.g. given `a?.b.c`, we can evaluate any load from `a?.b` at @@ -115,16 +110,46 @@ export type OptionalChainSidemap = { }; type OptionalTraversalContext = { + currFn: HIRFunction; blocks: ReadonlyMap; // Track optional blocks to avoid outer calls into nested optionals seenOptionals: Set; - processedInstrsInOptional: Set; + processedInstrsInOptional: Map>; temporariesReadInOptional: Map; hoistableObjects: Map; }; +function traverseFunction( + fn: HIRFunction, + context: OptionalTraversalContext, +): void { + for (const [_, block] of fn.body.blocks) { + for (const instr of block.instructions) { + if ( + instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod' + ) { + traverseFunction(instr.value.loweredFunc.func, { + ...context, + currFn: instr.value.loweredFunc.func, + blocks: instr.value.loweredFunc.func.body.blocks, + }); + } + } + if ( + block.terminal.kind === 'optional' && + !context.seenOptionals.has(block.id) + ) { + traverseOptionalBlock( + block as TBasicBlock, + context, + null, + ); + } + } +} /** * Match the consequent and alternate blocks of an optional. * @returns propertyload computed by the consequent block, or null if the @@ -369,10 +394,13 @@ function traverseOptionalBlock( }, ], }; - context.processedInstrsInOptional.add( - matchConsequentResult.storeLocalInstrId, + const processedInstrsInOptionalByFn = getOrInsertDefault( + context.processedInstrsInOptional, + context.currFn, + new Set(), ); - context.processedInstrsInOptional.add(test.id); + processedInstrsInOptionalByFn.add(matchConsequentResult.storeLocalInstrId); + processedInstrsInOptionalByFn.add(test.id); context.temporariesReadInOptional.set( matchConsequentResult.consequentId, load, diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts index b85d9425cb7ac..31e42049fd72d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/Environment.ts @@ -230,15 +230,7 @@ const EnvironmentConfigSchema = z.object({ */ enableUseTypeAnnotations: z.boolean().default(false), - enablePropagateDepsInHIR: z.boolean().default(false), - - /** - * Enables inference of optional dependency chains. Without this flag - * a property chain such as `props?.items?.foo` will infer as a dep on - * just `props`. With this flag enabled, we'll infer that full path as - * the dependency. - */ - enableOptionalDependencies: z.boolean().default(true), + enableFunctionDependencyRewrite: z.boolean().default(true), /** * Enables inlining ReactElement object literals in place of JSX diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts index 855ca9121d26b..bd938db03eab1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -46,7 +46,7 @@ export function propagateScopeDependenciesHIR(fn: HIRFunction): void { const hoistablePropertyLoads = keyByScopeId( fn, - collectHoistablePropertyLoads(fn, temporaries, hoistableObjects, null), + collectHoistablePropertyLoads(fn, temporaries, hoistableObjects), ); const scopeDeps = collectDependencies( @@ -176,8 +176,10 @@ function findTemporariesUsedOutsideDeclaringScope( * $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. + * @param usedOutsideDeclaringScope is used to check the correctness of + * reordering LoadLocal / PropertyLoad calls. We only track a LoadLocal / + * PropertyLoad in the returned temporaries map if 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 @@ -215,7 +217,29 @@ export function collectTemporariesSidemap( fn: HIRFunction, usedOutsideDeclaringScope: ReadonlySet, ): ReadonlyMap { - const temporaries = new Map(); + const temporaries = new Map(); + collectTemporariesSidemapImpl( + fn, + usedOutsideDeclaringScope, + temporaries, + false, + ); + return temporaries; +} + +/** + * Recursive collect a sidemap of all `LoadLocal` and `PropertyLoads` with a + * function and all nested functions. + * + * Note that IdentifierIds are currently unique, so we can use a single + * Map across all nested functions. + */ +function collectTemporariesSidemapImpl( + fn: HIRFunction, + usedOutsideDeclaringScope: ReadonlySet, + temporaries: Map, + isInnerFn: boolean, +): void { for (const [_, block] of fn.body.blocks) { for (const instr of block.instructions) { const {value, lvalue} = instr; @@ -224,27 +248,51 @@ export function collectTemporariesSidemap( ); if (value.kind === 'PropertyLoad' && !usedOutside) { - const property = getProperty( - value.object, - value.property, - false, - temporaries, - ); - temporaries.set(lvalue.identifier.id, property); + if (!isInnerFn || temporaries.has(value.object.identifier.id)) { + /** + * All dependencies of a inner / nested function must have a base + * identifier from the outermost component / hook. This is because the + * compiler cannot break an inner function into multiple granular + * scopes. + */ + const property = getProperty( + value.object, + value.property, + false, + 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: [], - }); + if ( + !isInnerFn || + fn.context.some( + context => context.identifier.id === value.place.identifier.id, + ) + ) { + temporaries.set(lvalue.identifier.id, { + identifier: value.place.identifier, + path: [], + }); + } + } else if ( + value.kind === 'FunctionExpression' || + value.kind === 'ObjectMethod' + ) { + collectTemporariesSidemapImpl( + value.loweredFunc.func, + usedOutsideDeclaringScope, + temporaries, + true, + ); } } } - return temporaries; } function getProperty( @@ -310,6 +358,12 @@ class Context { #temporaries: ReadonlyMap; #temporariesUsedOutsideScope: ReadonlySet; + /** + * Tracks the traversal state. See Context.declare for explanation of why this + * is needed. + */ + inInnerFn: boolean = false; + constructor( temporariesUsedOutsideScope: ReadonlySet, temporaries: ReadonlyMap, @@ -360,12 +414,23 @@ class Context { } /* - * 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. + * 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. + * + * Note that we do not track declarations or reassignments within inner + * functions for the following reasons: + * - inner functions cannot be split by scope boundaries and are guaranteed + * to consume their own declarations + * - reassignments within inner functions are tracked as context variables, + * which already have extended mutable ranges to account for reassignments + * - *most importantly* it's currently simply incorrect to compare inner + * function instruction ids (tracked by `decl`) with outer ones (as stored + * by root identifier mutable ranges). */ declare(identifier: Identifier, decl: Decl): void { + if (this.inInnerFn) return; if (!this.#declarations.has(identifier.declarationId)) { this.#declarations.set(identifier.declarationId, decl); } @@ -575,7 +640,10 @@ function collectDependencies( fn: HIRFunction, usedOutsideDeclaringScope: ReadonlySet, temporaries: ReadonlyMap, - processedInstrsInOptional: ReadonlySet, + processedInstrsInOptional: ReadonlyMap< + HIRFunction, + ReadonlySet + >, ): Map> { const context = new Context(usedOutsideDeclaringScope, temporaries); @@ -595,35 +663,61 @@ function collectDependencies( 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); - } - - // Record referenced optional chains in phis - for (const phi of block.phis) { - for (const operand of phi.operands) { - const maybeOptionalChain = temporaries.get(operand[1].identifier.id); - if (maybeOptionalChain) { - context.visitDependency(maybeOptionalChain); + const shouldSkipInstructionDependencies = ( + fn: HIRFunction, + id: InstructionId, + ): boolean => { + return processedInstrsInOptional.get(fn)?.has(id) ?? false; + }; + + const handleFunction = (fn: HIRFunction): void => { + 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); + } + // Record referenced optional chains in phis + for (const phi of block.phis) { + for (const operand of phi.operands) { + const maybeOptionalChain = temporaries.get(operand[1].identifier.id); + if (maybeOptionalChain) { + context.visitDependency(maybeOptionalChain); + } } } - } - for (const instr of block.instructions) { - if (!processedInstrsInOptional.has(instr.id)) { - handleInstruction(instr, context); + for (const instr of block.instructions) { + if ( + fn.env.config.enableFunctionDependencyRewrite && + (instr.value.kind === 'FunctionExpression' || + instr.value.kind === 'ObjectMethod') + ) { + context.declare(instr.lvalue.identifier, { + id: instr.id, + scope: context.currentScope, + }); + /** + * Recursively visit the inner function to extract dependencies there + */ + const wasInInnerFn = context.inInnerFn; + context.inInnerFn = true; + handleFunction(instr.value.loweredFunc.func); + context.inInnerFn = wasInInnerFn; + } else if (!shouldSkipInstructionDependencies(fn, instr.id)) { + handleInstruction(instr, context); + } } - } - if (!processedInstrsInOptional.has(block.terminal.id)) { - for (const place of eachTerminalOperand(block.terminal)) { - context.visitOperand(place); + if (!shouldSkipInstructionDependencies(fn, block.terminal.id)) { + for (const place of eachTerminalOperand(block.terminal)) { + context.visitOperand(place); + } } } - } + }; + + handleFunction(fn); return context.deps; } 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 217bc3132bd14..c9ee803bfaffd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/visitors.ts @@ -1215,9 +1215,17 @@ export class ScopeBlockTraversal { } } + /** + * @returns if the given scope is currently 'active', i.e. if the scope start + * block but not the scope fallthrough has been recorded. + */ isScopeActive(scopeId: ScopeId): boolean { return this.#activeScopes.indexOf(scopeId) !== -1; } + + /** + * The current, innermost active scope. + */ get currentScope(): ScopeId | null { return this.#activeScopes.at(-1) ?? null; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts deleted file mode 100644 index dc1142b271e77..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PropagateScopeDependencies.ts +++ /dev/null @@ -1,1324 +0,0 @@ -/** - * 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 {Environment} from '../HIR'; -import { - areEqualPaths, - BlockId, - DeclarationId, - GeneratedSource, - Identifier, - InstructionId, - InstructionKind, - isObjectMethodType, - isRefValueType, - isUseRefType, - makeInstructionId, - Place, - PrunedReactiveScopeBlock, - ReactiveFunction, - ReactiveInstruction, - ReactiveOptionalCallValue, - ReactiveScope, - ReactiveScopeBlock, - ReactiveScopeDependency, - ReactiveTerminalStatement, - ReactiveValue, - ScopeId, -} from '../HIR/HIR'; -import {eachInstructionValueOperand, eachPatternOperand} from '../HIR/visitors'; -import {empty, Stack} from '../Utils/Stack'; -import {assertExhaustive, Iterable_some} from '../Utils/utils'; -import { - ReactiveScopeDependencyTree, - ReactiveScopePropertyDependency, -} from './DeriveMinimalDependencies'; -import {ReactiveFunctionVisitor, visitReactiveFunction} from './visitors'; - -/* - * Infers the dependencies of each scope to include variables whose values - * are non-stable and created prior to the start of the scope. Also propagates - * dependencies upwards, so that parent scope dependencies are the union of - * their direct dependencies and those of their child scopes. - */ -export function propagateScopeDependencies(fn: ReactiveFunction): void { - const escapingTemporaries: TemporariesUsedOutsideDefiningScope = { - declarations: new Map(), - usedOutsideDeclaringScope: new Set(), - }; - visitReactiveFunction(fn, new FindPromotedTemporaries(), escapingTemporaries); - - const context = new Context(escapingTemporaries.usedOutsideDeclaringScope); - 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(), - }); - } - } - visitReactiveFunction(fn, new PropagationVisitor(fn.env), context); -} - -type TemporariesUsedOutsideDefiningScope = { - /* - * tracks all relevant temporary declarations (currently LoadLocal and PropertyLoad) - * and the scope where they are defined - */ - declarations: Map; - // temporaries used outside of their defining scope - usedOutsideDeclaringScope: Set; -}; -class FindPromotedTemporaries extends ReactiveFunctionVisitor { - scopes: Array = []; - - override visitScope( - scope: ReactiveScopeBlock, - state: TemporariesUsedOutsideDefiningScope, - ): void { - this.scopes.push(scope.scope.id); - this.traverseScope(scope, state); - this.scopes.pop(); - } - - override visitInstruction( - instruction: ReactiveInstruction, - state: TemporariesUsedOutsideDefiningScope, - ): void { - // Visit all places first, then record temporaries which may need to be promoted - this.traverseInstruction(instruction, state); - - const scope = this.scopes.at(-1); - if (instruction.lvalue === null || scope === undefined) { - return; - } - switch (instruction.value.kind) { - case 'LoadLocal': - case 'LoadContext': - case 'PropertyLoad': { - state.declarations.set( - instruction.lvalue.identifier.declarationId, - scope, - ); - break; - } - default: { - break; - } - } - } - - override visitPlace( - _id: InstructionId, - place: Place, - state: TemporariesUsedOutsideDefiningScope, - ): void { - const declaringScope = state.declarations.get( - place.identifier.declarationId, - ); - if (declaringScope === undefined) { - return; - } - if (this.scopes.indexOf(declaringScope) === -1) { - // Declaring scope is not active === used outside declaring scope - state.usedOutsideDeclaringScope.add(place.identifier.declarationId); - } - } -} - -type DeclMap = Map; -type Decl = { - id: InstructionId; - scope: Stack; -}; - -/** - * TraversalState and PoisonState is used to track the poisoned state of a scope. - * - * A scope is poisoned when either of these conditions hold: - * - one of its own nested blocks is a jump target (for break/continues) - * - it is a outermost scope and contains a throw / return - * - * When a scope is poisoned, all dependencies (from instructions and inner scopes) - * are added as conditionally accessed. - */ -type ScopeTraversalState = { - value: ReactiveScope; - ownBlocks: Stack; -}; - -class PoisonState { - poisonedBlocks: Set = new Set(); - poisonedScopes: Set = new Set(); - isPoisoned: boolean = false; - - constructor( - poisonedBlocks: Set, - poisonedScopes: Set, - isPoisoned: boolean, - ) { - this.poisonedBlocks = poisonedBlocks; - this.poisonedScopes = poisonedScopes; - this.isPoisoned = isPoisoned; - } - - clone(): PoisonState { - return new PoisonState( - new Set(this.poisonedBlocks), - new Set(this.poisonedScopes), - this.isPoisoned, - ); - } - - take(other: PoisonState): PoisonState { - const copy = new PoisonState( - this.poisonedBlocks, - this.poisonedScopes, - this.isPoisoned, - ); - this.poisonedBlocks = other.poisonedBlocks; - this.poisonedScopes = other.poisonedScopes; - this.isPoisoned = other.isPoisoned; - return copy; - } - - merge( - others: Array, - currentScope: ScopeTraversalState | null, - ): void { - for (const other of others) { - for (const id of other.poisonedBlocks) { - this.poisonedBlocks.add(id); - } - for (const id of other.poisonedScopes) { - this.poisonedScopes.add(id); - } - } - this.#invalidate(currentScope); - } - - #invalidate(currentScope: ScopeTraversalState | null): void { - if (currentScope != null) { - if (this.poisonedScopes.has(currentScope.value.id)) { - this.isPoisoned = true; - return; - } else if ( - currentScope.ownBlocks.find(blockId => this.poisonedBlocks.has(blockId)) - ) { - this.isPoisoned = true; - return; - } - } - this.isPoisoned = false; - } - - /** - * Mark a block or scope as poisoned and update the `isPoisoned` flag. - * - * @param targetBlock id of the block which ends non-linear control flow. - * For a break/continue instruction, this is the target block. - * Throw and return instructions have no target and will poison the earliest - * active scope - */ - addPoisonTarget( - target: BlockId | null, - activeScopes: Stack, - ): void { - const currentScope = activeScopes.value; - if (target == null && currentScope != null) { - let cursor = activeScopes; - while (true) { - const next = cursor.pop(); - if (next.value == null) { - const poisonedScope = cursor.value!.value.id; - this.poisonedScopes.add(poisonedScope); - if (poisonedScope === currentScope?.value.id) { - this.isPoisoned = true; - } - break; - } else { - cursor = next; - } - } - } else if (target != null) { - this.poisonedBlocks.add(target); - if ( - !this.isPoisoned && - currentScope?.ownBlocks.find(blockId => blockId === target) - ) { - this.isPoisoned = true; - } - } - } - - /** - * Invoked during traversal when a poisoned scope becomes inactive - * @param id - * @param currentScope - */ - removeMaybePoisonedScope( - id: ScopeId, - currentScope: ScopeTraversalState | null, - ): void { - this.poisonedScopes.delete(id); - this.#invalidate(currentScope); - } - - removeMaybePoisonedBlock( - id: BlockId, - currentScope: ScopeTraversalState | null, - ): void { - this.poisonedBlocks.delete(id); - this.#invalidate(currentScope); - } -} - -class Context { - #temporariesUsedOutsideScope: Set; - #declarations: DeclMap = new Map(); - #reassignments: Map = new Map(); - // Reactive dependencies used in the current reactive scope. - #dependencies: ReactiveScopeDependencyTree = - new ReactiveScopeDependencyTree(); - /* - * We keep a sidemap for temporaries created by PropertyLoads, and do - * not store any control flow (i.e. #inConditionalWithinScope) here. - * - a ReactiveScope (A) containing a PropertyLoad may differ from the - * ReactiveScope (B) that uses the produced temporary. - * - codegen will inline these PropertyLoads back into scope (B) - */ - #properties: Map = new Map(); - #temporaries: Map = new Map(); - #inConditionalWithinScope: boolean = false; - /* - * Reactive dependencies used unconditionally in the current conditional. - * Composed of dependencies: - * - directly accessed within block (added in visitDep) - * - accessed by all cfg branches (added through promoteDeps) - */ - #depsInCurrentConditional: ReactiveScopeDependencyTree = - new ReactiveScopeDependencyTree(); - #scopes: Stack = empty(); - poisonState: PoisonState = new PoisonState(new Set(), new Set(), false); - - constructor(temporariesUsedOutsideScope: Set) { - this.#temporariesUsedOutsideScope = temporariesUsedOutsideScope; - } - - enter(scope: ReactiveScope, fn: () => void): Set { - // Save context of previous scope - const prevInConditional = this.#inConditionalWithinScope; - const previousDependencies = this.#dependencies; - const prevDepsInConditional: ReactiveScopeDependencyTree | null = this - .isPoisoned - ? this.#depsInCurrentConditional - : null; - if (prevDepsInConditional != null) { - this.#depsInCurrentConditional = new ReactiveScopeDependencyTree(); - } - - /* - * Set context for new scope - * A nested scope should add all deps it directly uses as its own - * unconditional deps, regardless of whether the nested scope is itself - * within a conditional - */ - const scopedDependencies = new ReactiveScopeDependencyTree(); - this.#inConditionalWithinScope = false; - this.#dependencies = scopedDependencies; - this.#scopes = this.#scopes.push({ - value: scope, - ownBlocks: empty(), - }); - this.poisonState.isPoisoned = false; - - fn(); - - // Restore context of previous scope - this.#scopes = this.#scopes.pop(); - this.poisonState.removeMaybePoisonedScope(scope.id, this.#scopes.value); - - this.#dependencies = previousDependencies; - this.#inConditionalWithinScope = prevInConditional; - - // Derive minimal dependencies now, since next line may mutate scopedDependencies - const minInnerScopeDependencies = - scopedDependencies.deriveMinimalDependencies(); - - /* - * propagate dependencies 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 - */ - this.#dependencies.addDepsFromInnerScope( - scopedDependencies, - this.#inConditionalWithinScope || this.isPoisoned, - this.#checkValidDependency.bind(this), - ); - - if (prevDepsInConditional != null) { - // Outer scope is poisoned - prevDepsInConditional.addDepsFromInnerScope( - this.#depsInCurrentConditional, - true, - this.#checkValidDependency.bind(this), - ); - this.#depsInCurrentConditional = prevDepsInConditional; - } - - return minInnerScopeDependencies; - } - - isUsedOutsideDeclaringScope(place: Place): boolean { - return this.#temporariesUsedOutsideScope.has( - place.identifier.declarationId, - ); - } - - /* - * Prints dependency tree to string for debugging. - * @param includeAccesses - * @returns string representation of DependencyTree - */ - printDeps(includeAccesses: boolean = false): string { - return this.#dependencies.printDeps(includeAccesses); - } - - /* - * We track and return unconditional accesses / deps within this conditional. - * If an object property is always used (i.e. in every conditional path), we - * want to promote it to an unconditional access / dependency. - * - * The caller of `enterConditional` is responsible determining for promotion. - * i.e. call promoteDepsFromExhaustiveConditionals to merge returned results. - * - * e.g. we want to mark props.a.b as an unconditional dep here - * if (foo(...)) { - * access(props.a.b); - * } else { - * access(props.a.b); - * } - */ - enterConditional(fn: () => void): ReactiveScopeDependencyTree { - const prevInConditional = this.#inConditionalWithinScope; - const prevUncondAccessed = this.#depsInCurrentConditional; - this.#inConditionalWithinScope = true; - this.#depsInCurrentConditional = new ReactiveScopeDependencyTree(); - fn(); - const result = this.#depsInCurrentConditional; - this.#inConditionalWithinScope = prevInConditional; - this.#depsInCurrentConditional = prevUncondAccessed; - return result; - } - - /* - * Add dependencies from exhaustive CFG paths into the current ReactiveDeps - * tree. If a property is used in every CFG path, it is promoted to an - * unconditional access / dependency here. - * @param depsInConditionals - */ - promoteDepsFromExhaustiveConditionals( - depsInConditionals: Array, - ): void { - this.#dependencies.promoteDepsFromExhaustiveConditionals( - depsInConditionals, - ); - this.#depsInCurrentConditional.promoteDepsFromExhaustiveConditionals( - depsInConditionals, - ); - } - - /* - * 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); - } - - declareTemporary(lvalue: Place, place: Place): void { - this.#temporaries.set(lvalue.identifier, place); - } - - resolveTemporary(place: Place): Place { - return this.#temporaries.get(place.identifier) ?? place; - } - - #getProperty( - object: Place, - property: string, - optional: boolean, - ): ReactiveScopePropertyDependency { - const resolvedObject = this.resolveTemporary(object); - const resolvedDependency = this.#properties.get(resolvedObject.identifier); - let objectDependency: ReactiveScopePropertyDependency; - /* - * (1) Create the base property dependency as either a LoadLocal (from a temporary) - * or a deep copy of an existing property dependency. - */ - if (resolvedDependency === undefined) { - objectDependency = { - identifier: resolvedObject.identifier, - path: [], - }; - } else { - objectDependency = { - identifier: resolvedDependency.identifier, - path: [...resolvedDependency.path], - }; - } - - objectDependency.path.push({property, optional}); - - return objectDependency; - } - - declareProperty( - lvalue: Place, - object: Place, - property: string, - optional: boolean, - ): void { - const nextDependency = this.#getProperty(object, property, optional); - this.#properties.set(lvalue.identifier, nextDependency); - } - - // 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?.value; - return ( - currentScope != null && - currentDeclaration !== undefined && - currentDeclaration.id < currentScope.range.start && - (currentDeclaration.scope == null || - currentDeclaration.scope.value?.value !== currentScope) - ); - } - - #isScopeActive(scope: ReactiveScope): boolean { - if (this.#scopes === null) { - return false; - } - return this.#scopes.find(state => state.value === scope); - } - - get currentScope(): Stack { - return this.#scopes; - } - - get isPoisoned(): boolean { - return this.poisonState.isPoisoned; - } - - visitOperand(place: Place): void { - const resolved = this.resolveTemporary(place); - /* - * if this operand is a temporary created for a property load, try to resolve it to - * the expanded Place. Fall back to using the operand as-is. - */ - - let dependency: ReactiveScopePropertyDependency = { - identifier: resolved.identifier, - path: [], - }; - if (resolved.identifier.name === null) { - const propertyDependency = this.#properties.get(resolved.identifier); - if (propertyDependency !== undefined) { - dependency = {...propertyDependency}; - } - } - this.visitDependency(dependency); - } - - visitProperty(object: Place, property: string, optional: boolean): void { - const nextDependency = this.#getProperty(object, property, optional); - this.visitDependency(nextDependency); - } - - visitDependency(maybeDependency: ReactiveScopePropertyDependency): 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 a free var - * (all other 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.value) && - // TODO LeaveSSA: key scope.declarations by DeclarationId - !Iterable_some( - scope.value.declarations.values(), - decl => - decl.identifier.declarationId === - maybeDependency.identifier.declarationId, - ) - ) { - scope.value.declarations.set(maybeDependency.identifier.id, { - identifier: maybeDependency.identifier, - scope: originalDeclaration.scope.value!.value, - }); - } - }); - } - - if (this.#checkValidDependency(maybeDependency)) { - const isPoisoned = this.isPoisoned; - this.#depsInCurrentConditional.add(maybeDependency, isPoisoned); - /* - * Add info about this dependency to the existing tree - * We do not try to join/reduce dependencies here due to missing info - */ - this.#dependencies.add( - maybeDependency, - this.#inConditionalWithinScope || isPoisoned, - ); - } - } - - /* - * 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?.value; - if ( - currentScope != null && - !Iterable_some( - currentScope.reassignments, - identifier => - identifier.declarationId === place.identifier.declarationId, - ) && - this.#checkValidDependency({identifier: place.identifier, path: []}) - ) { - // TODO LeaveSSA: scope.reassignments should be keyed by declarationid - currentScope.reassignments.add(place.identifier); - } - } - - pushLabeledBlock(id: BlockId): void { - const currentScope = this.#scopes.value; - if (currentScope != null) { - currentScope.ownBlocks = currentScope.ownBlocks.push(id); - } - } - popLabeledBlock(id: BlockId): void { - const currentScope = this.#scopes.value; - if (currentScope != null) { - const last = currentScope.ownBlocks.value; - currentScope.ownBlocks = currentScope.ownBlocks.pop(); - - CompilerError.invariant(last != null && last === id, { - reason: '[PropagateScopeDependencies] Misformed block stack', - loc: GeneratedSource, - }); - } - this.poisonState.removeMaybePoisonedBlock(id, currentScope); - } -} - -class PropagationVisitor extends ReactiveFunctionVisitor { - env: Environment; - - constructor(env: Environment) { - super(); - this.env = env; - } - - override visitScope(scope: ReactiveScopeBlock, context: Context): void { - const scopeDependencies = context.enter(scope.scope, () => { - this.visitBlock(scope.instructions, context); - }); - for (const candidateDep of scopeDependencies) { - if ( - !Iterable_some( - scope.scope.dependencies, - existingDep => - existingDep.identifier.declarationId === - candidateDep.identifier.declarationId && - areEqualPaths(existingDep.path, candidateDep.path), - ) - ) { - scope.scope.dependencies.add(candidateDep); - } - } - /* - * TODO LeaveSSA: fix existing bug with duplicate deps and reassignments - * see fixture ssa-cascading-eliminated-phis, note that we cache `x` - * twice because its both a dep and a reassignment. - * - * for (const reassignment of scope.scope.reassignments) { - * if ( - * Iterable_some( - * scope.scope.dependencies.values(), - * dep => - * dep.identifier.declarationId === reassignment.declarationId && - * dep.path.length === 0, - * ) - * ) { - * scope.scope.reassignments.delete(reassignment); - * } - * } - */ - } - - override visitPrunedScope( - scopeBlock: PrunedReactiveScopeBlock, - context: Context, - ): void { - /* - * NOTE: we explicitly throw away the deps, we only enter() the scope to record its - * declarations - */ - const _scopeDepdencies = context.enter(scopeBlock.scope, () => { - this.visitBlock(scopeBlock.instructions, context); - }); - } - - override visitInstruction( - instruction: ReactiveInstruction, - context: Context, - ): void { - const {id, value, lvalue} = instruction; - this.visitInstructionValue(context, id, value, lvalue); - if (lvalue == null) { - return; - } - context.declare(lvalue.identifier, { - id, - scope: context.currentScope, - }); - } - - extractOptionalProperty( - context: Context, - optionalValue: ReactiveOptionalCallValue, - lvalue: Place, - ): { - lvalue: Place; - object: Place; - property: string; - optional: boolean; - } | null { - const sequence = optionalValue.value; - CompilerError.invariant(sequence.kind === 'SequenceExpression', { - reason: 'Expected OptionalExpression value to be a SequenceExpression', - description: `Found a \`${sequence.kind}\``, - loc: sequence.loc, - }); - /** - * Base case: inner ` "?." ` - *``` - * = OptionalExpression optional=true (`optionalValue` is here) - * Sequence (`sequence` is here) - * t0 = LoadLocal - * Sequence - * t1 = PropertyLoad t0 . - * LoadLocal t1 - * ``` - */ - if ( - sequence.instructions.length === 1 && - sequence.instructions[0].lvalue !== null && - sequence.instructions[0].value.kind === 'LoadLocal' && - sequence.instructions[0].value.place.identifier.name !== null && - !context.isUsedOutsideDeclaringScope(sequence.instructions[0].lvalue) && - sequence.value.kind === 'SequenceExpression' && - sequence.value.instructions.length === 1 && - sequence.value.instructions[0].value.kind === 'PropertyLoad' && - sequence.value.instructions[0].value.object.identifier.id === - sequence.instructions[0].lvalue.identifier.id && - sequence.value.instructions[0].lvalue !== null && - sequence.value.value.kind === 'LoadLocal' && - sequence.value.value.place.identifier.id === - sequence.value.instructions[0].lvalue.identifier.id - ) { - context.declareTemporary( - sequence.instructions[0].lvalue, - sequence.instructions[0].value.place, - ); - const propertyLoad = sequence.value.instructions[0].value; - return { - lvalue, - object: propertyLoad.object, - property: propertyLoad.property, - optional: optionalValue.optional, - }; - } - /** - * Base case 2: inner ` "." "?." - * ``` - * = OptionalExpression optional=true (`optionalValue` is here) - * Sequence (`sequence` is here) - * t0 = Sequence - * t1 = LoadLocal - * ... // see note - * PropertyLoad t1 . - * [46] Sequence - * t2 = PropertyLoad t0 . - * [46] LoadLocal t2 - * ``` - * - * Note that it's possible to have additional inner chained non-optional - * property loads at "...", from an expression like `a?.b.c.d.e`. We could - * expand to support this case by relaxing the check on the inner sequence - * length, ensuring all instructions after the first LoadLocal are PropertyLoad - * and then iterating to ensure that the lvalue of the previous is always - * the object of the next PropertyLoad, w the final lvalue as the object - * of the sequence.value's object. - * - * But this case is likely rare in practice, usually once you're optional - * chaining all property accesses are optional (not `a?.b.c` but `a?.b?.c`). - * Also, HIR-based PropagateScopeDeps will handle this case so it doesn't - * seem worth it to optimize for that edge-case here. - */ - if ( - sequence.instructions.length === 1 && - sequence.instructions[0].lvalue !== null && - sequence.instructions[0].value.kind === 'SequenceExpression' && - sequence.instructions[0].value.instructions.length === 1 && - sequence.instructions[0].value.instructions[0].lvalue !== null && - sequence.instructions[0].value.instructions[0].value.kind === - 'LoadLocal' && - sequence.instructions[0].value.instructions[0].value.place.identifier - .name !== null && - !context.isUsedOutsideDeclaringScope( - sequence.instructions[0].value.instructions[0].lvalue, - ) && - sequence.instructions[0].value.value.kind === 'PropertyLoad' && - sequence.instructions[0].value.value.object.identifier.id === - sequence.instructions[0].value.instructions[0].lvalue.identifier.id && - sequence.value.kind === 'SequenceExpression' && - sequence.value.instructions.length === 1 && - sequence.value.instructions[0].lvalue !== null && - sequence.value.instructions[0].value.kind === 'PropertyLoad' && - sequence.value.instructions[0].value.object.identifier.id === - sequence.instructions[0].lvalue.identifier.id && - sequence.value.value.kind === 'LoadLocal' && - sequence.value.value.place.identifier.id === - sequence.value.instructions[0].lvalue.identifier.id - ) { - // LoadLocal - context.declareTemporary( - sequence.instructions[0].value.instructions[0].lvalue, - sequence.instructions[0].value.instructions[0].value.place, - ); - // PropertyLoad . (the inner non-optional property) - context.declareProperty( - sequence.instructions[0].lvalue, - sequence.instructions[0].value.value.object, - sequence.instructions[0].value.value.property, - false, - ); - const propertyLoad = sequence.value.instructions[0].value; - return { - lvalue, - object: propertyLoad.object, - property: propertyLoad.property, - optional: optionalValue.optional, - }; - } - - /** - * Composed case: - * - ` "." or "?." ` - * - ` "." or "?>" ` - * - * This case is convoluted, note how `t0` appears as an lvalue *twice* - * and then is an operand of an intermediate LoadLocal and then the - * object of the final PropertyLoad: - * - * ``` - * = OptionalExpression optional=false (`optionalValue` is here) - * Sequence (`sequence` is here) - * t0 = Sequence - * t0 = - * - * LoadLocal t0 - * Sequence - * t1 = PropertyLoad t0. - * LoadLocal t1 - * ``` - */ - if ( - sequence.instructions.length === 1 && - sequence.instructions[0].value.kind === 'SequenceExpression' && - sequence.instructions[0].value.instructions.length === 1 && - sequence.instructions[0].value.instructions[0].lvalue !== null && - sequence.instructions[0].value.instructions[0].value.kind === - 'OptionalExpression' && - sequence.instructions[0].value.value.kind === 'LoadLocal' && - sequence.instructions[0].value.value.place.identifier.id === - sequence.instructions[0].value.instructions[0].lvalue.identifier.id && - sequence.value.kind === 'SequenceExpression' && - sequence.value.instructions.length === 1 && - sequence.value.instructions[0].lvalue !== null && - sequence.value.instructions[0].value.kind === 'PropertyLoad' && - sequence.value.instructions[0].value.object.identifier.id === - sequence.instructions[0].value.value.place.identifier.id && - sequence.value.value.kind === 'LoadLocal' && - sequence.value.value.place.identifier.id === - sequence.value.instructions[0].lvalue.identifier.id - ) { - const {lvalue: innerLvalue, value: innerOptional} = - sequence.instructions[0].value.instructions[0]; - const innerProperty = this.extractOptionalProperty( - context, - innerOptional, - innerLvalue, - ); - if (innerProperty === null) { - return null; - } - context.declareProperty( - innerProperty.lvalue, - innerProperty.object, - innerProperty.property, - innerProperty.optional, - ); - const propertyLoad = sequence.value.instructions[0].value; - return { - lvalue, - object: propertyLoad.object, - property: propertyLoad.property, - optional: optionalValue.optional, - }; - } - return null; - } - - visitOptionalExpression( - context: Context, - id: InstructionId, - value: ReactiveOptionalCallValue, - lvalue: Place | null, - ): void { - /** - * If this is the first optional=true optional in a recursive OptionalExpression - * subtree, we check to see if the subtree is of the form: - * ``` - * NestedOptional = - * ` . / ?. ` - * ` . / ?. ` - * ``` - * - * Ie strictly a chain like `foo?.bar?.baz` or `a?.b.c`. If the subtree contains - * any other types of expressions - for example `foo?.[makeKey(a)]` - then this - * will return null and we'll go to the default handling below. - * - * If the tree does match the NestedOptional shape, then we'll have recorded - * a sequence of declareProperty calls, and the final visitProperty call here - * will record that optional chain as a dependency (since we know it's about - * to be referenced via its lvalue which is non-null). - */ - if ( - lvalue !== null && - value.optional && - this.env.config.enableOptionalDependencies - ) { - const inner = this.extractOptionalProperty(context, value, lvalue); - if (inner !== null) { - context.visitProperty(inner.object, inner.property, inner.optional); - return; - } - } - - // Otherwise we treat everything after the optional as conditional - const inner = value.value; - /* - * OptionalExpression value is a SequenceExpression where the instructions - * represent the code prior to the `?` and the final value represents the - * conditional code that follows. - */ - CompilerError.invariant(inner.kind === 'SequenceExpression', { - reason: 'Expected OptionalExpression value to be a SequenceExpression', - description: `Found a \`${value.kind}\``, - loc: value.loc, - suggestions: null, - }); - // Instructions are the unconditionally executed portion before the `?` - for (const instr of inner.instructions) { - this.visitInstruction(instr, context); - } - // The final value is the conditional portion following the `?` - context.enterConditional(() => { - this.visitReactiveValue(context, id, inner.value, null); - }); - } - - visitReactiveValue( - context: Context, - id: InstructionId, - value: ReactiveValue, - lvalue: Place | null, - ): void { - switch (value.kind) { - case 'OptionalExpression': { - this.visitOptionalExpression(context, id, value, lvalue); - break; - } - case 'LogicalExpression': { - this.visitReactiveValue(context, id, value.left, null); - context.enterConditional(() => { - this.visitReactiveValue(context, id, value.right, null); - }); - break; - } - case 'ConditionalExpression': { - this.visitReactiveValue(context, id, value.test, null); - - const consequentDeps = context.enterConditional(() => { - this.visitReactiveValue(context, id, value.consequent, null); - }); - const alternateDeps = context.enterConditional(() => { - this.visitReactiveValue(context, id, value.alternate, null); - }); - context.promoteDepsFromExhaustiveConditionals([ - consequentDeps, - alternateDeps, - ]); - break; - } - case 'SequenceExpression': { - for (const instr of value.instructions) { - this.visitInstruction(instr, context); - } - this.visitInstructionValue(context, id, value.value, null); - break; - } - case 'FunctionExpression': { - if (this.env.config.enableTreatFunctionDepsAsConditional) { - context.enterConditional(() => { - for (const operand of eachInstructionValueOperand(value)) { - context.visitOperand(operand); - } - }); - } else { - for (const operand of eachInstructionValueOperand(value)) { - context.visitOperand(operand); - } - } - break; - } - case 'ReactiveFunctionValue': { - CompilerError.invariant(false, { - reason: `Unexpected ReactiveFunctionValue`, - loc: value.loc, - description: null, - suggestions: null, - }); - } - default: { - for (const operand of eachInstructionValueOperand(value)) { - context.visitOperand(operand); - } - } - } - } - - visitInstructionValue( - context: Context, - id: InstructionId, - value: ReactiveValue, - lvalue: Place | null, - ): void { - if (value.kind === 'LoadLocal' && lvalue !== null) { - if ( - value.place.identifier.name !== null && - lvalue.identifier.name === null && - !context.isUsedOutsideDeclaringScope(lvalue) - ) { - context.declareTemporary(lvalue, value.place); - } else { - context.visitOperand(value.place); - } - } else if (value.kind === 'PropertyLoad') { - if (lvalue !== null && !context.isUsedOutsideDeclaringScope(lvalue)) { - context.declareProperty(lvalue, value.object, value.property, false); - } else { - context.visitProperty(value.object, value.property, false); - } - } 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 { - this.visitReactiveValue(context, id, value, lvalue); - } - } - - enterTerminal(stmt: ReactiveTerminalStatement, context: Context): void { - if (stmt.label != null) { - context.pushLabeledBlock(stmt.label.id); - } - const terminal = stmt.terminal; - switch (terminal.kind) { - case 'continue': - case 'break': { - context.poisonState.addPoisonTarget( - terminal.target, - context.currentScope, - ); - break; - } - case 'throw': - case 'return': { - context.poisonState.addPoisonTarget(null, context.currentScope); - break; - } - } - } - exitTerminal(stmt: ReactiveTerminalStatement, context: Context): void { - if (stmt.label != null) { - context.popLabeledBlock(stmt.label.id); - } - } - - override visitTerminal( - stmt: ReactiveTerminalStatement, - context: Context, - ): void { - this.enterTerminal(stmt, context); - const terminal = stmt.terminal; - switch (terminal.kind) { - case 'break': - case 'continue': { - break; - } - case 'return': { - context.visitOperand(terminal.value); - break; - } - case 'throw': { - context.visitOperand(terminal.value); - break; - } - case 'for': { - this.visitReactiveValue(context, terminal.id, terminal.init, null); - this.visitReactiveValue(context, terminal.id, terminal.test, null); - context.enterConditional(() => { - this.visitBlock(terminal.loop, context); - if (terminal.update !== null) { - this.visitReactiveValue( - context, - terminal.id, - terminal.update, - null, - ); - } - }); - break; - } - case 'for-of': { - this.visitReactiveValue(context, terminal.id, terminal.init, null); - context.enterConditional(() => { - this.visitBlock(terminal.loop, context); - }); - break; - } - case 'for-in': { - this.visitReactiveValue(context, terminal.id, terminal.init, null); - context.enterConditional(() => { - this.visitBlock(terminal.loop, context); - }); - break; - } - case 'do-while': { - this.visitBlock(terminal.loop, context); - context.enterConditional(() => { - this.visitReactiveValue(context, terminal.id, terminal.test, null); - }); - break; - } - case 'while': { - this.visitReactiveValue(context, terminal.id, terminal.test, null); - context.enterConditional(() => { - this.visitBlock(terminal.loop, context); - }); - break; - } - case 'if': { - context.visitOperand(terminal.test); - const {consequent, alternate} = terminal; - /* - * Consequent and alternate branches are mutually exclusive, - * so we save and restore the poison state here. - */ - const prevPoisonState = context.poisonState.clone(); - const depsInIf = context.enterConditional(() => { - this.visitBlock(consequent, context); - }); - if (alternate !== null) { - const ifPoisonState = context.poisonState.take(prevPoisonState); - const depsInElse = context.enterConditional(() => { - this.visitBlock(alternate, context); - }); - context.poisonState.merge( - [ifPoisonState], - context.currentScope.value, - ); - context.promoteDepsFromExhaustiveConditionals([depsInIf, depsInElse]); - } - break; - } - case 'switch': { - context.visitOperand(terminal.test); - const isDefaultOnly = - terminal.cases.length === 1 && terminal.cases[0].test == null; - if (isDefaultOnly) { - const case_ = terminal.cases[0]; - if (case_.block != null) { - this.visitBlock(case_.block, context); - break; - } - } - const depsInCases = []; - let foundDefault = false; - /** - * Switch branches are mutually exclusive - */ - const prevPoisonState = context.poisonState.clone(); - const mutExPoisonStates: Array = []; - /* - * This can underestimate unconditional accesses due to the current - * CFG representation for fallthrough. This is safe. It only - * reduces granularity of dependencies. - */ - for (const {test, block} of terminal.cases) { - if (test !== null) { - context.visitOperand(test); - } else { - foundDefault = true; - } - if (block !== undefined) { - mutExPoisonStates.push( - context.poisonState.take(prevPoisonState.clone()), - ); - depsInCases.push( - context.enterConditional(() => { - this.visitBlock(block, context); - }), - ); - } - } - if (foundDefault) { - context.promoteDepsFromExhaustiveConditionals(depsInCases); - } - context.poisonState.merge( - mutExPoisonStates, - context.currentScope.value, - ); - break; - } - case 'label': { - this.visitBlock(terminal.block, context); - break; - } - case 'try': { - this.visitBlock(terminal.block, context); - this.visitBlock(terminal.handler, context); - break; - } - default: { - assertExhaustive( - terminal, - `Unexpected terminal kind \`${(terminal as any).kind}\``, - ); - } - } - this.exitTerminal(stmt, context); - } -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts index eb778305611cf..8841ae92795c8 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/index.ts @@ -17,7 +17,6 @@ export {mergeReactiveScopesThatInvalidateTogether} from './MergeReactiveScopesTh export {printReactiveFunction} from './PrintReactiveFunction'; export {promoteUsedTemporaries} from './PromoteUsedTemporaries'; export {propagateEarlyReturns} from './PropagateEarlyReturns'; -export {propagateScopeDependencies} from './PropagateScopeDependencies'; export {pruneAllReactiveScopes} from './PruneAllReactiveScopes'; export {pruneHoistedContexts} from './PruneHoistedContexts'; export {pruneNonEscapingScopes} from './PruneNonEscapingScopes'; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.expect.md index e4e47dfde9e2b..d6331db4e7ea3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-hoisting-functionexpr.expect.md @@ -58,7 +58,7 @@ function Component(t0) { const $ = _c(5); const { obj, isObjNull } = t0; let t1; - if ($[0] !== isObjNull || $[1] !== obj.prop) { + if ($[0] !== isObjNull || $[1] !== obj) { t1 = () => { if (!isObjNull) { return obj.prop; @@ -67,7 +67,7 @@ function Component(t0) { } }; $[0] = isObjNull; - $[1] = obj.prop; + $[1] = obj; $[2] = t1; } else { t1 = $[2]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.expect.md index 56ca1f7722e45..839821b349a6f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.expect.md @@ -38,16 +38,24 @@ import { identity } from "shared-runtime"; * try-catch block, as that might throw */ function useFoo(maybeNullObject) { - const $ = _c(2); + const $ = _c(4); let y; - if ($[0] !== maybeNullObject.value.inner) { + if ($[0] !== maybeNullObject) { y = []; try { - y.push(identity(maybeNullObject.value.inner)); + let t0; + if ($[2] !== maybeNullObject.value.inner) { + t0 = identity(maybeNullObject.value.inner); + $[2] = maybeNullObject.value.inner; + $[3] = t0; + } else { + t0 = $[3]; + } + y.push(t0); } catch { y.push("null"); } - $[0] = maybeNullObject.value.inner; + $[0] = maybeNullObject; $[1] = y; } else { y = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md index 53deac41495c6..c071d5d20ed99 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-func-mutate-2.expect.md @@ -26,29 +26,20 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function component(a, b) { - const $ = _c(5); - let t0; - if ($[0] !== b) { - t0 = { b }; - $[0] = b; - $[1] = t0; - } else { - t0 = $[1]; - } - const y = t0; + const $ = _c(2); + const y = { b }; let z; - if ($[2] !== a || $[3] !== y.b) { + if ($[0] !== a) { z = { a }; const x = function () { z.a = 2; }; x(); - $[2] = a; - $[3] = y.b; - $[4] = z; + $[0] = a; + $[1] = z; } else { - z = $[4]; + z = $[1]; } return z; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md index 76648c251a101..3f795b604e382 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-break-labeled.expect.md @@ -33,9 +33,14 @@ import { c as _c } from "react/compiler-runtime"; /** * props.b *does* influence `a` */ function Component(props) { - const $ = _c(2); + const $ = _c(5); let a; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { a = []; a.push(props.a); bb0: { @@ -47,10 +52,13 @@ function Component(props) { } a.push(props.d); - $[0] = props; - $[1] = a; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; } else { - a = $[1]; + a = $[4]; } return a; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md index 82537902bfa19..5e708b95c6fe9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-early-return.expect.md @@ -70,10 +70,10 @@ import { c as _c } from "react/compiler-runtime"; /** * props.b does *not* influence `a` */ function ComponentA(props) { - const $ = _c(3); + const $ = _c(5); let a_DEBUG; let t0; - if ($[0] !== props) { + if ($[0] !== props.a || $[1] !== props.b || $[2] !== props.d) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { a_DEBUG = []; @@ -85,12 +85,14 @@ function ComponentA(props) { a_DEBUG.push(props.d); } - $[0] = props; - $[1] = a_DEBUG; - $[2] = t0; + $[0] = props.a; + $[1] = props.b; + $[2] = props.d; + $[3] = a_DEBUG; + $[4] = t0; } else { - a_DEBUG = $[1]; - t0 = $[2]; + a_DEBUG = $[3]; + t0 = $[4]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; @@ -102,9 +104,14 @@ function ComponentA(props) { * props.b *does* influence `a` */ function ComponentB(props) { - const $ = _c(2); + const $ = _c(5); let a; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { a = []; a.push(props.a); if (props.b) { @@ -112,10 +119,13 @@ function ComponentB(props) { } a.push(props.d); - $[0] = props; - $[1] = a; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; } else { - a = $[1]; + a = $[4]; } return a; } @@ -124,10 +134,15 @@ function ComponentB(props) { * props.b *does* influence `a`, but only in a way that is never observable */ function ComponentC(props) { - const $ = _c(3); + const $ = _c(6); let a; let t0; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { a = []; @@ -140,12 +155,15 @@ function ComponentC(props) { a.push(props.d); } - $[0] = props; - $[1] = a; - $[2] = t0; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; + $[5] = t0; } else { - a = $[1]; - t0 = $[2]; + a = $[4]; + t0 = $[5]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; @@ -157,10 +175,15 @@ function ComponentC(props) { * props.b *does* influence `a` */ function ComponentD(props) { - const $ = _c(3); + const $ = _c(6); let a; let t0; - if ($[0] !== props) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d + ) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { a = []; @@ -173,12 +196,15 @@ function ComponentD(props) { a.push(props.d); } - $[0] = props; - $[1] = a; - $[2] = t0; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = a; + $[5] = t0; } else { - a = $[1]; - t0 = $[2]; + a = $[4]; + t0 = $[5]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md index ad638cf28d871..fa8348c200972 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/conditional-on-mutable.expect.md @@ -36,9 +36,9 @@ function mayMutate() {} ```javascript import { c as _c } from "react/compiler-runtime"; function ComponentA(props) { - const $ = _c(2); + const $ = _c(4); let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p1 || $[2] !== props.p2) { const a = []; const b = []; if (b) { @@ -49,18 +49,20 @@ function ComponentA(props) { } t0 = ; - $[0] = props; - $[1] = t0; + $[0] = props.p0; + $[1] = props.p1; + $[2] = props.p2; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } return t0; } function ComponentB(props) { - const $ = _c(2); + const $ = _c(4); let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p1 || $[2] !== props.p2) { const a = []; const b = []; if (mayMutate(b)) { @@ -71,10 +73,12 @@ function ComponentB(props) { } t0 = ; - $[0] = props; - $[1] = t0; + $[0] = props.p0; + $[1] = props.p1; + $[2] = props.p2; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md index 2d33981f73fd1..5db4756ad3606 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-nested-early-return-within-reactive-scope.expect.md @@ -31,9 +31,9 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(5); + const $ = _c(7); let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const x = []; @@ -41,12 +41,12 @@ function Component(props) { x.push(props.a); if (props.b) { let t1; - if ($[2] !== props.b) { + if ($[4] !== props.b) { t1 = [props.b]; - $[2] = props.b; - $[3] = t1; + $[4] = props.b; + $[5] = t1; } else { - t1 = $[3]; + t1 = $[5]; } const y = t1; x.push(y); @@ -58,20 +58,22 @@ function Component(props) { break bb0; } else { let t1; - if ($[4] === Symbol.for("react.memo_cache_sentinel")) { + if ($[6] === Symbol.for("react.memo_cache_sentinel")) { t1 = foo(); - $[4] = t1; + $[6] = t1; } else { - t1 = $[4]; + t1 = $[6]; } t0 = t1; break bb0; } } - $[0] = props; - $[1] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.b; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md index 6c3525e9e77eb..42caf4e39b39e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/early-return-within-reactive-scope.expect.md @@ -45,9 +45,9 @@ import { c as _c } from "react/compiler-runtime"; import { makeArray } from "shared-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(6); let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const x = []; @@ -57,21 +57,23 @@ function Component(props) { break bb0; } else { let t1; - if ($[2] !== props.b) { + if ($[4] !== props.b) { t1 = makeArray(props.b); - $[2] = props.b; - $[3] = t1; + $[4] = props.b; + $[5] = t1; } else { - t1 = $[3]; + t1 = $[5]; } t0 = t1; break bb0; } } - $[0] = props; - $[1] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.b; + $[3] = t0; } else { - t0 = $[1]; + t0 = $[3]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md new file mode 100644 index 0000000000000..d9c2b599998b7 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +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/optional-member-expression-with-conditional-optional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional-optional.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md new file mode 100644 index 0000000000000..57b7d48facbd5 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies +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/optional-member-expression-with-conditional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.hoist-optional-member-expression-with-conditional.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-optional.expect.md index 75c5d61d407f0..8bf7f5bc71d0e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-optional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/error.todo-optional-call-chain-in-optional.expect.md @@ -25,7 +25,7 @@ export const FIXTURE_ENTRYPONT = { 1 | function useFoo(props: {value: {x: string; y: string} | null}) { 2 | const value = props.value; > 3 | return createArray(value?.x, value?.y)?.join(', '); - | ^^^^^^^^ Todo: Unexpected terminal kind `optional` for optional test block (3:3) + | ^^^^^^^^ Todo: Unexpected terminal kind `optional` for optional fallthrough block (3:3) 4 | } 5 | 6 | function createArray(...args: Array): Array { 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 index 8cbaeb3f89465..396292103ff62 100644 --- 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 @@ -2,7 +2,7 @@ ## Input ```javascript -// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false +// @enableTreatFunctionDepsAsConditional import {Stringify} from 'shared-runtime'; function Component({props}) { @@ -20,7 +20,7 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false +import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional import { Stringify } from "shared-runtime"; function Component(t0) { 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 index 2ede54db5f364..ab3e00f9ba2b1 100644 --- 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 @@ -1,4 +1,4 @@ -// @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false +// @enableTreatFunctionDepsAsConditional import {Stringify} from 'shared-runtime'; function Component({props}) { 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 f2fa20feb5477..76f27fdb3ff8b 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 @enablePropagateDepsInHIR:false +// @enableTreatFunctionDepsAsConditional function Component(props) { function getLength() { return props.bar.length; @@ -21,15 +21,15 @@ export const FIXTURE_ENTRYPOINT = { ## Code ```javascript -import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional @enablePropagateDepsInHIR:false +import { c as _c } from "react/compiler-runtime"; // @enableTreatFunctionDepsAsConditional function Component(props) { const $ = _c(5); let t0; - if ($[0] !== props) { + if ($[0] !== props.bar) { t0 = function getLength() { return props.bar.length; }; - $[0] = props; + $[0] = props.bar; $[1] = t0; } else { t0 = $[1]; diff --git "a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.js" "b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/functionexpr\342\200\223conditional-access.js" index 9bff3e5cdb53b..6e59fb947d150 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 @enablePropagateDepsInHIR:false +// @enableTreatFunctionDepsAsConditional function Component(props) { function getLength() { return props.bar.length; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md index bed1c329f0d10..a578e4a41d28f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/iife-return-modified-later-phi.expect.md @@ -26,9 +26,9 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(3); let items; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a) { let t0; if (props.cond) { t0 = []; @@ -38,10 +38,11 @@ function Component(props) { items = t0; items?.push(props.a); - $[0] = props; - $[1] = items; + $[0] = props.cond; + $[1] = props.a; + $[2] = items; } else { - items = $[1]; + items = $[2]; } return items; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.expect.md index 31e2cadf9f7db..f415c20528bfb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.expect.md @@ -33,11 +33,11 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let x; - if ($[0] !== a.b.c.d) { + if ($[0] !== a.b.c.d.e) { x = []; x.push(a?.b.c?.d.e); x.push(a.b?.c.d?.e); - $[0] = a.b.c.d; + $[0] = a.b.c.d.e; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-outlining-child-stored-in-id.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-outlining-child-stored-in-id.expect.md index fd7ca41bcffa3..86e9adaabcbbd 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-outlining-child-stored-in-id.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-outlining-child-stored-in-id.expect.md @@ -53,7 +53,7 @@ function Component(arr) { const $ = _c(3); const x = useX(); let t0; - if ($[0] !== arr || $[1] !== x) { + if ($[0] !== x || $[1] !== arr) { t0 = arr.map((i) => { arr.map((i_0, id) => { const T0 = _temp; @@ -63,8 +63,8 @@ function Component(arr) { return jsx; }); }); - $[0] = arr; - $[1] = x; + $[0] = x; + $[1] = arr; $[2] = t0; } else { t0 = $[2]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.expect.md index 0acf33b2ed87f..92a24194a359d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.expect.md @@ -120,29 +120,29 @@ function useFoo(t0) { } const x = t1; let t2; - if ($[2] !== prop2?.inner) { + if ($[2] !== prop2?.inner.value) { t2 = identity(prop2?.inner.value)?.toString(); - $[2] = prop2?.inner; + $[2] = prop2?.inner.value; $[3] = t2; } else { t2 = $[3]; } const y = t2; let t3; - if ($[4] !== prop3 || $[5] !== prop4) { + if ($[4] !== prop3 || $[5] !== prop4?.inner) { t3 = prop3?.fn(prop4?.inner.value).toString(); $[4] = prop3; - $[5] = prop4; + $[5] = prop4?.inner; $[6] = t3; } else { t3 = $[6]; } const z = t3; let t4; - if ($[7] !== prop5 || $[8] !== prop6) { + if ($[7] !== prop5 || $[8] !== prop6?.inner) { t4 = prop5?.fn(prop6?.inner.value)?.toString(); $[7] = prop5; - $[8] = prop6; + $[8] = prop6?.inner; $[9] = t4; } else { t4 = $[9]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md index 8a20f9186b447..b5534114c08a1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/object-mutated-in-consequent-alternate-both-return.expect.md @@ -29,9 +29,9 @@ import { c as _c } from "react/compiler-runtime"; import { makeObject_Primitives } from "shared-runtime"; function Component(props) { - const $ = _c(2); + const $ = _c(3); let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.value) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const object = makeObject_Primitives(); @@ -45,10 +45,11 @@ function Component(props) { break bb0; } } - $[0] = props; - $[1] = t0; + $[0] = props.cond; + $[1] = props.value; + $[2] = t0; } else { - t0 = $[1]; + t0 = $[2]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md deleted file mode 100644 index 77ded20d939bd..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional-optional.expect.md +++ /dev/null @@ -1,74 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies -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 -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/optional-member-expression-with-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.expect.md deleted file mode 100644 index 10c23085d8e6b..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-with-conditional.expect.md +++ /dev/null @@ -1,74 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies -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 -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/partial-early-return-within-reactive-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md index 398161f0c6a24..266d87628c511 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/partial-early-return-within-reactive-scope.expect.md @@ -30,10 +30,10 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(6); let y; let t0; - if ($[0] !== props) { + if ($[0] !== props.cond || $[1] !== props.a || $[2] !== props.b) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { const x = []; @@ -43,11 +43,11 @@ function Component(props) { break bb0; } else { let t1; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + if ($[5] === Symbol.for("react.memo_cache_sentinel")) { t1 = foo(); - $[3] = t1; + $[5] = t1; } else { - t1 = $[3]; + t1 = $[5]; } y = t1; if (props.b) { @@ -56,12 +56,14 @@ function Component(props) { } } } - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.b; + $[3] = y; + $[4] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[3]; + t0 = $[4]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push-consecutive-phis.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push-consecutive-phis.expect.md index f17bcc92cb7ce..16edbf2e23690 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push-consecutive-phis.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push-consecutive-phis.expect.md @@ -49,7 +49,7 @@ import { c as _c } from "react/compiler-runtime"; import { makeArray } from "shared-runtime"; function Component(props) { - const $ = _c(3); + const $ = _c(6); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = {}; @@ -59,7 +59,12 @@ function Component(props) { } const x = t0; let t1; - if ($[1] !== props) { + if ( + $[1] !== props.cond || + $[2] !== props.cond2 || + $[3] !== props.value || + $[4] !== props.value2 + ) { let y; if (props.cond) { if (props.cond2) { @@ -74,10 +79,13 @@ function Component(props) { y.push(x); t1 = [x, y]; - $[1] = props; - $[2] = t1; + $[1] = props.cond; + $[2] = props.cond2; + $[3] = props.value; + $[4] = props.value2; + $[5] = t1; } else { - t1 = $[2]; + t1 = $[5]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md index f58eed10fda5a..58e2c8f869a6a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-array-push.expect.md @@ -36,7 +36,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(3); + const $ = _c(4); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = {}; @@ -46,7 +46,7 @@ function Component(props) { } const x = t0; let t1; - if ($[1] !== props) { + if ($[1] !== props.cond || $[2] !== props.value) { let y; if (props.cond) { y = [props.value]; @@ -57,10 +57,11 @@ function Component(props) { y.push(x); t1 = [x, y]; - $[1] = props; - $[2] = t1; + $[1] = props.cond; + $[2] = props.value; + $[3] = t1; } else { - t1 = $[2]; + t1 = $[3]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md index 70551c8e9d7b0..641711e893884 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/phi-type-inference-property-store.expect.md @@ -32,7 +32,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @debug function Component(props) { - const $ = _c(3); + const $ = _c(4); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { t0 = {}; @@ -42,7 +42,7 @@ function Component(props) { } const x = t0; let t1; - if ($[1] !== props) { + if ($[1] !== props.cond || $[2] !== props.a) { let y; if (props.cond) { y = {}; @@ -53,10 +53,11 @@ function Component(props) { y.x = x; t1 = [x, y]; - $[1] = props; - $[2] = t1; + $[1] = props.cond; + $[2] = props.a; + $[3] = t1; } else { - t1 = $[2]; + t1 = $[3]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-conditional-access-own-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-conditional-access-own-scope.expect.md new file mode 100644 index 0000000000000..8579b773e6218 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-conditional-access-own-scope.expect.md @@ -0,0 +1,50 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useCallback} from 'react'; + +function Component({propA, propB}) { + return useCallback(() => { + if (propA) { + return { + value: propB.x.y, + }; + } + }, [propA, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{propA: 1, propB: {x: {y: []}}}], +}; + +``` + + +## Error + +``` + 3 | + 4 | function Component({propA, propB}) { +> 5 | return useCallback(() => { + | ^^^^^^^ +> 6 | if (propA) { + | ^^^^^^^^^^^^^^^^ +> 7 | return { + | ^^^^^^^^^^^^^^^^ +> 8 | value: propB.x.y, + | ^^^^^^^^^^^^^^^^ +> 9 | }; + | ^^^^^^^^^^^^^^^^ +> 10 | } + | ^^^^^^^^^^^^^^^^ +> 11 | }, [propA, propB.x.y]); + | ^^^^ 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 (5:11) + 12 | } + 13 | + 14 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-conditional-access-own-scope.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-conditional-access-own-scope.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-infer-conditional-value-block.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-infer-conditional-value-block.expect.md new file mode 100644 index 0000000000000..e77e79fd98e0e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-infer-conditional-value-block.expect.md @@ -0,0 +1,59 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useCallback} from 'react'; +import {identity, mutate} from 'shared-runtime'; + +function useHook(propA, propB) { + return useCallback(() => { + const x = {}; + if (identity(null) ?? propA.a) { + mutate(x); + return { + value: propB.x.y, + }; + } + }, [propA.a, propB.x.y]); +} + +export const FIXTURE_ENTRYPOINT = { + fn: useHook, + params: [{a: 1}, {x: {y: 3}}], +}; + +``` + + +## Error + +``` + 4 | + 5 | function useHook(propA, propB) { +> 6 | return useCallback(() => { + | ^^^^^^^ +> 7 | const x = {}; + | ^^^^^^^^^^^^^^^^^ +> 8 | if (identity(null) ?? propA.a) { + | ^^^^^^^^^^^^^^^^^ +> 9 | mutate(x); + | ^^^^^^^^^^^^^^^^^ +> 10 | return { + | ^^^^^^^^^^^^^^^^^ +> 11 | value: propB.x.y, + | ^^^^^^^^^^^^^^^^^ +> 12 | }; + | ^^^^^^^^^^^^^^^^^ +> 13 | } + | ^^^^^^^^^^^^^^^^^ +> 14 | }, [propA.a, propB.x.y]); + | ^^^^ 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 (6:14) + +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 (6:14) + 15 | } + 16 | + 17 | export const FIXTURE_ENTRYPOINT = { +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-infer-conditional-value-block.ts similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.ts rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.hoist-useCallback-infer-conditional-value-block.ts diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.expect.md new file mode 100644 index 0000000000000..ae44f27912293 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.expect.md @@ -0,0 +1,53 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +/** + * TODO: we're currently bailing out because `contextVar` is a context variable + * and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad + * sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted + * `LoadContext` and `PropertyLoad` instructions into the outer function, which + * we took as eligible dependencies. + * + * One solution is to simply record `LoadContext` identifiers into the + * temporaries sidemap when the instruction occurs *after* the context + * variable's mutable range. + */ +function Foo(props) { + let contextVar; + if (props.cond) { + contextVar = {val: 2}; + } else { + contextVar = {}; + } + + const cb = useCallback(() => [contextVar.val], [contextVar.val]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{cond: true}], +}; + +``` + + +## Error + +``` + 22 | } + 23 | +> 24 | const cb = useCallback(() => [contextVar.val], [contextVar.val]); + | ^^^^^^^^^^^^^^^^^^^^^^ 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 (24:24) + 25 | + 26 | return ; + 27 | } +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.tsx new file mode 100644 index 0000000000000..8447e3960dc51 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/error.todo-useCallback-captures-reassigned-context-property.tsx @@ -0,0 +1,32 @@ +// @validatePreserveExistingMemoizationGuarantees +import {useCallback} from 'react'; +import {Stringify} from 'shared-runtime'; + +/** + * TODO: we're currently bailing out because `contextVar` is a context variable + * and not recorded into the PropagateScopeDeps LoadLocal / PropertyLoad + * sidemap. Previously, we were able to avoid this as `BuildHIR` hoisted + * `LoadContext` and `PropertyLoad` instructions into the outer function, which + * we took as eligible dependencies. + * + * One solution is to simply record `LoadContext` identifiers into the + * temporaries sidemap when the instruction occurs *after* the context + * variable's mutable range. + */ +function Foo(props) { + let contextVar; + if (props.cond) { + contextVar = {val: 2}; + } else { + contextVar = {}; + } + + const cb = useCallback(() => [contextVar.val], [contextVar.val]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{cond: true}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md deleted file mode 100644 index db69bc2821b12..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.expect.md +++ /dev/null @@ -1,81 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees -import {useCallback} from 'react'; -import {Stringify} from 'shared-runtime'; - -function Foo(props) { - let contextVar; - if (props.cond) { - contextVar = {val: 2}; - } else { - contextVar = {}; - } - - const cb = useCallback(() => [contextVar.val], [contextVar.val]); - - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{cond: true}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees -import { useCallback } from "react"; -import { Stringify } from "shared-runtime"; - -function Foo(props) { - const $ = _c(6); - let contextVar; - if ($[0] !== props.cond) { - if (props.cond) { - contextVar = { val: 2 }; - } else { - contextVar = {}; - } - $[0] = props.cond; - $[1] = contextVar; - } else { - contextVar = $[1]; - } - - const t0 = contextVar; - let t1; - if ($[2] !== t0.val) { - t1 = () => [contextVar.val]; - $[2] = t0.val; - $[3] = t1; - } else { - t1 = $[3]; - } - contextVar; - const cb = t1; - let t2; - if ($[4] !== cb) { - t2 = ; - $[4] = cb; - $[5] = t2; - } else { - t2 = $[5]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{ cond: true }], -}; - -``` - -### Eval output -(kind: ok)
{"cb":{"kind":"Function","result":[2]},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx deleted file mode 100644 index cb6f65a9f4f52..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context-property.tsx +++ /dev/null @@ -1,21 +0,0 @@ -// @validatePreserveExistingMemoizationGuarantees -import {useCallback} from 'react'; -import {Stringify} from 'shared-runtime'; - -function Foo(props) { - let contextVar; - if (props.cond) { - contextVar = {val: 2}; - } else { - contextVar = {}; - } - - const cb = useCallback(() => [contextVar.val], [contextVar.val]); - - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{cond: true}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md index b66661fbcaa08..41994e1e56e44 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-captures-reassigned-context.expect.md @@ -45,18 +45,16 @@ function Foo(props) { } else { x = $[1]; } - - const t0 = x; - let t1; - if ($[2] !== t0) { - t1 = () => [x]; - $[2] = t0; - $[3] = t1; + let t0; + if ($[2] !== x) { + t0 = () => [x]; + $[2] = x; + $[3] = t0; } else { - t1 = $[3]; + t0 = $[3]; } x; - const cb = t1; + const cb = t0; return cb; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.expect.md deleted file mode 100644 index a90492f7a1774..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-conditional-access-own-scope.expect.md +++ /dev/null @@ -1,58 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees -import {useCallback} from 'react'; - -function Component({propA, propB}) { - return useCallback(() => { - if (propA) { - return { - value: propB.x.y, - }; - } - }, [propA, propB.x.y]); -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{propA: 1, propB: {x: {y: []}}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees -import { useCallback } from "react"; - -function Component(t0) { - const $ = _c(3); - const { propA, propB } = t0; - let t1; - if ($[0] !== propA || $[1] !== propB.x.y) { - t1 = () => { - if (propA) { - return { value: propB.x.y }; - } - }; - $[0] = propA; - $[1] = propB.x.y; - $[2] = t1; - } else { - t1 = $[2]; - } - return t1; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ propA: 1, propB: { x: { y: [] } } }], -}; - -``` - -### Eval output -(kind: ok) "[[ function params=0 ]]" \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md index b141c27614d24..96cec0cd265bf 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md @@ -70,28 +70,26 @@ function useBar(t0, cond) { if (cond) { x = b; } - - const t2 = x; - let t3; - if ($[1] !== a || $[2] !== t2) { - t3 = () => [a, x]; - $[1] = a; - $[2] = t2; - $[3] = t3; + let t2; + if ($[1] !== x || $[2] !== a) { + t2 = () => [a, x]; + $[1] = x; + $[2] = a; + $[3] = t2; } else { - t3 = $[3]; + t2 = $[3]; } x; - const cb = t3; - let t4; + const cb = t2; + let t3; if ($[4] !== cb) { - t4 = ; + t3 = ; $[4] = cb; - $[5] = t4; + $[5] = t3; } else { - t4 = $[5]; + t3 = $[5]; } - return t4; + return t3; } export const FIXTURE_ENTRYPOINT = { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.expect.md deleted file mode 100644 index d6c01643f5e31..0000000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-infer-conditional-value-block.expect.md +++ /dev/null @@ -1,63 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees -import {useCallback} from 'react'; -import {identity, mutate} from 'shared-runtime'; - -function useHook(propA, propB) { - return useCallback(() => { - const x = {}; - if (identity(null) ?? propA.a) { - mutate(x); - return { - value: propB.x.y, - }; - } - }, [propA.a, propB.x.y]); -} - -export const FIXTURE_ENTRYPOINT = { - fn: useHook, - params: [{a: 1}, {x: {y: 3}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees -import { useCallback } from "react"; -import { identity, mutate } from "shared-runtime"; - -function useHook(propA, propB) { - const $ = _c(3); - let t0; - if ($[0] !== propA.a || $[1] !== propB.x.y) { - t0 = () => { - const x = {}; - if (identity(null) ?? propA.a) { - mutate(x); - return { value: propB.x.y }; - } - }; - $[0] = propA.a; - $[1] = propB.x.y; - $[2] = t0; - } else { - t0 = $[2]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useHook, - params: [{ a: 1 }, { x: { y: 3 } }], -}; - -``` - -### Eval output -(kind: ok) "[[ function params=0 ]]" \ 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/infer-objectmethod-cond-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-objectmethod-cond-access.expect.md new file mode 100644 index 0000000000000..2c7bf6bbe599e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-objectmethod-cond-access.expect.md @@ -0,0 +1,82 @@ + +## Input + +```javascript +// @enablePropagateDepsInHIR +import {Stringify} from 'shared-runtime'; + +function Foo({a, shouldReadA}) { + return ( + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{a: null, shouldReadA: true}], + sequentialRenders: [ + {a: null, shouldReadA: true}, + {a: null, shouldReadA: false}, + {a: {b: {c: 4}}, shouldReadA: true}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR +import { Stringify } from "shared-runtime"; + +function Foo(t0) { + const $ = _c(3); + const { a, shouldReadA } = t0; + let t1; + if ($[0] !== shouldReadA || $[1] !== a) { + t1 = ( + + ); + $[0] = shouldReadA; + $[1] = a; + $[2] = t1; + } else { + t1 = $[2]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ a: null, shouldReadA: true }], + sequentialRenders: [ + { a: null, shouldReadA: true }, + { a: null, shouldReadA: false }, + { a: { b: { c: 4 } }, shouldReadA: true }, + ], +}; + +``` + +### Eval output +(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] +
{"objectMethod":{"method":{"kind":"Function","result":null}},"shouldInvokeFns":true}
+
{"objectMethod":{"method":{"kind":"Function","result":4}},"shouldInvokeFns":true}
\ 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/infer-objectmethod-cond-access.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-objectmethod-cond-access.js new file mode 100644 index 0000000000000..2c8488bb29216 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-objectmethod-cond-access.js @@ -0,0 +1,26 @@ +// @enablePropagateDepsInHIR +import {Stringify} from 'shared-runtime'; + +function Foo({a, shouldReadA}) { + return ( + + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{a: null, shouldReadA: true}], + sequentialRenders: [ + {a: null, shouldReadA: true}, + {a: null, shouldReadA: false}, + {a: {b: {c: 4}}, shouldReadA: true}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md index 02e60eff91d61..ed56ff068113b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md @@ -34,9 +34,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a.b) { + if ($[0] !== a.b?.c.d?.e) { t1 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a.b; + $[0] = a.b?.c.d?.e; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md new file mode 100644 index 0000000000000..73df2b615b9ef --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.expect.md @@ -0,0 +1,61 @@ + +## Input + +```javascript +// @enablePropagateDepsInHIR +import {Stringify} from 'shared-runtime'; + +function Foo({data}) { + return ( + data.a.d} bar={data.a?.b.c} shouldInvokeFns={true} /> + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{data: {a: null}}], + sequentialRenders: [{data: {a: {b: {c: 4}}}}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR +import { Stringify } from "shared-runtime"; + +function Foo(t0) { + const $ = _c(5); + const { data } = t0; + let t1; + if ($[0] !== data.a.d) { + t1 = () => data.a.d; + $[0] = data.a.d; + $[1] = t1; + } else { + t1 = $[1]; + } + const t2 = data.a?.b.c; + let t3; + if ($[2] !== t1 || $[3] !== t2) { + t3 = ; + $[2] = t1; + $[3] = t2; + $[4] = t3; + } else { + t3 = $[4]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{ data: { a: null } }], + sequentialRenders: [{ data: { a: { b: { c: 4 } } } }], +}; + +``` + +### Eval output +(kind: ok)
{"foo":{"kind":"Function"},"bar":4,"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.tsx new file mode 100644 index 0000000000000..05ed136d5f7a8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-invariant.tsx @@ -0,0 +1,14 @@ +// @enablePropagateDepsInHIR +import {Stringify} from 'shared-runtime'; + +function Foo({data}) { + return ( + data.a.d} bar={data.a?.b.c} shouldInvokeFns={true} /> + ); +} + +export const FIXTURE_ENTRYPOINT = { + fn: Foo, + params: [{data: {a: null}}], + sequentialRenders: [{data: {a: {b: {c: 4}}}}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/react-namespace.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/react-namespace.expect.md index 0afc5b651bb94..cab231da32517 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/react-namespace.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/react-namespace.expect.md @@ -29,36 +29,38 @@ import { c as _c } from "react/compiler-runtime"; const FooContext = React.createContext({ current: null }); function Component(props) { - const $ = _c(5); + const $ = _c(7); React.useContext(FooContext); const ref = React.useRef(); const [x, setX] = React.useState(false); let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + if ($[0] !== ref) { t0 = () => { setX(true); ref.current = true; }; - $[0] = t0; + $[0] = ref; + $[1] = t0; } else { - t0 = $[0]; + t0 = $[1]; } const onClick = t0; let t1; - if ($[1] !== props.children) { + if ($[2] !== props.children) { t1 = React.cloneElement(props.children); - $[1] = props.children; - $[2] = t1; + $[2] = props.children; + $[3] = t1; } else { - t1 = $[2]; + t1 = $[3]; } let t2; - if ($[3] !== t1) { + if ($[4] !== onClick || $[5] !== t1) { t2 =
{t1}
; - $[3] = t1; - $[4] = t2; + $[4] = onClick; + $[5] = t1; + $[6] = t2; } else { - t2 = $[4]; + t2 = $[6]; } return t2; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md index 12a84b14f449a..896a547fec25c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md @@ -15,9 +15,9 @@ import { c as _c } from "react/compiler-runtime"; function Component(props) { const $ = _c(2); let t0; - if ($[0] !== props.post.feedback.comments) { + if ($[0] !== props.post.feedback.comments?.edges) { t0 = props.post.feedback.comments?.edges?.map(render); - $[0] = props.post.feedback.comments; + $[0] = props.post.feedback.comments?.edges; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassigned-phi-in-returned-function-expression.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassigned-phi-in-returned-function-expression.expect.md index 5c6c680e05e91..39ce103cca071 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassigned-phi-in-returned-function-expression.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reassigned-phi-in-returned-function-expression.expect.md @@ -23,7 +23,7 @@ import { c as _c } from "react/compiler-runtime"; function Component(props) { const $ = _c(2); let t0; - if ($[0] !== props.str) { + if ($[0] !== props) { t0 = () => { let str; if (arguments.length) { @@ -34,7 +34,7 @@ function Component(props) { global.log(str); }; - $[0] = props.str; + $[0] = props; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.expect.md index 4d45d3f3c6da6..352552bf02ad1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.expect.md @@ -38,7 +38,7 @@ function Foo(t0) { const $ = _c(3); const { a, shouldReadA } = t0; let t1; - if ($[0] !== shouldReadA || $[1] !== a.b.c) { + if ($[0] !== shouldReadA || $[1] !== a) { t1 = ( { @@ -51,7 +51,7 @@ function Foo(t0) { /> ); $[0] = shouldReadA; - $[1] = a.b.c; + $[1] = a; $[2] = t1; } else { t1 = $[2]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond.expect.md index 9a95e7dc875b4..fa265ae1f8d74 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond.expect.md @@ -65,12 +65,12 @@ function useFoo(t0) { const $ = _c(2); const { screen } = t0; let t1; - if ($[0] !== screen?.title_text) { + if ($[0] !== screen) { t1 = screen?.title_text != null ? "(not null)" : identity({ title: screen.title_text }); - $[0] = screen?.title_text; + $[0] = screen; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md index c54d0828ecefe..37d347cd9a0b4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/join-uncond-scopes-cond-deps.expect.md @@ -61,20 +61,13 @@ import { c as _c } from "react/compiler-runtime"; // This tests an optimization, import { CONST_TRUE, setProperty } from "shared-runtime"; function useJoinCondDepsInUncondScopes(props) { - const $ = _c(4); + const $ = _c(2); let t0; if ($[0] !== props.a.b) { const y = {}; - let x; - if ($[2] !== props) { - x = {}; - if (CONST_TRUE) { - setProperty(x, props.a.b); - } - $[2] = props; - $[3] = x; - } else { - x = $[3]; + const x = {}; + if (CONST_TRUE) { + setProperty(x, props.a.b); } setProperty(y, props.a.b); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md index 09806d8b4b15d..9186ec84d6e21 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/promote-uncond.expect.md @@ -34,19 +34,20 @@ import { identity } from "shared-runtime"; // and promote it to an unconditional dependency. function usePromoteUnconditionalAccessToDependency(props, other) { - const $ = _c(3); + const $ = _c(4); let x; - if ($[0] !== props.a || $[1] !== other) { + if ($[0] !== props.a.a.a || $[1] !== props.a.b || $[2] !== other) { x = {}; x.a = props.a.a.a; if (identity(other)) { x.c = props.a.b.c; } - $[0] = props.a; - $[1] = other; - $[2] = x; + $[0] = props.a.a.a; + $[1] = props.a.b; + $[2] = other; + $[3] = x; } else { - x = $[2]; + x = $[3]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md index 157e2de81a1ff..bb99a5d90fe2b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md @@ -31,9 +31,9 @@ function useFoo(t0) { const $ = _c(2); const { a } = t0; let t1; - if ($[0] !== a.b) { + if ($[0] !== a.b?.c.d?.e) { t1 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a.b; + $[0] = a.b?.c.d?.e; $[1] = t1; } else { t1 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.expect.md index 8b5a2eb1a0b35..95c6a403deba2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ref-parameter-mutate-in-effect.expect.md @@ -26,28 +26,32 @@ import { c as _c } from "react/compiler-runtime"; import { useEffect } from "react"; function Foo(props, ref) { - const $ = _c(4); + const $ = _c(5); let t0; - let t1; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + if ($[0] !== ref) { t0 = () => { ref.current = 2; }; + $[0] = ref; + $[1] = t0; + } else { + t0 = $[1]; + } + let t1; + if ($[2] === Symbol.for("react.memo_cache_sentinel")) { t1 = []; - $[0] = t0; - $[1] = t1; + $[2] = t1; } else { - t0 = $[0]; - t1 = $[1]; + t1 = $[2]; } useEffect(t0, t1); let t2; - if ($[2] !== props.bar) { + if ($[3] !== props.bar) { t2 =
{props.bar}
; - $[2] = props.bar; - $[3] = t2; + $[3] = props.bar; + $[4] = t2; } else { - t2 = $[3]; + t2 = $[4]; } return t2; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md index 6af0cf0af7bca..c39b85e5ba636 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-cascading-eliminated-phis.expect.md @@ -36,10 +36,16 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(4); + const $ = _c(7); let x = 0; let values; - if ($[0] !== props || $[1] !== x) { + if ( + $[0] !== props.a || + $[1] !== props.b || + $[2] !== props.c || + $[3] !== props.d || + $[4] !== x + ) { values = []; const y = props.a || props.b; values.push(y); @@ -53,13 +59,16 @@ function Component(props) { } values.push(x); - $[0] = props; - $[1] = x; - $[2] = values; - $[3] = x; + $[0] = props.a; + $[1] = props.b; + $[2] = props.c; + $[3] = props.d; + $[4] = x; + $[5] = values; + $[6] = x; } else { - values = $[2]; - x = $[3]; + values = $[5]; + x = $[6]; } return values; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md index a10ad5fae4108..dd61d1fee1280 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-leave-case.expect.md @@ -39,9 +39,9 @@ import { c as _c } from "react/compiler-runtime"; 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) { @@ -55,10 +55,11 @@ function Component(props) { {y}
); - $[0] = props; - $[1] = t0; + $[0] = props.p0; + $[1] = props.p1; + $[2] = t0; } else { - t0 = $[1]; + t0 = $[2]; } return t0; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md index 3e7fd4bf5f859..c6c7489a4e66d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction-with-mutation.expect.md @@ -31,17 +31,19 @@ import { c as _c } from "react/compiler-runtime"; 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/ssa-renaming-ternary-destruction.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md index 9b3aad524ce14..693b94d886ce2 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-destruction.expect.md @@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function useFoo(props) { - const $ = _c(4); + const $ = _c(5); let x; if ($[0] !== props.bar) { x = []; @@ -36,12 +36,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/ssa-renaming-ternary-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-with-mutation.expect.md index de9466c4daa9d..283e55630b898 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary-with-mutation.expect.md @@ -31,17 +31,19 @@ import { c as _c } from "react/compiler-runtime"; 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/ssa-renaming-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md index e199863257bfe..97cfa052aff98 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-ternary.expect.md @@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function useFoo(props) { - const $ = _c(4); + const $ = _c(5); let x; if ($[0] !== props.bar) { x = []; @@ -36,12 +36,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/ssa-renaming-unconditional-ternary-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md index 16981f69cda90..1c4b48cb7c40f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary-with-mutation.expect.md @@ -31,17 +31,19 @@ export const FIXTURE_ENTRYPOINT = { import { c as _c } from "react/compiler-runtime"; import { arrayPush } from "shared-runtime"; function 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/ssa-renaming-unconditional-ternary.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md index 99b50ac231342..5571c3cfe56a6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-ternary.expect.md @@ -28,7 +28,7 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; function useFoo(props) { - const $ = _c(4); + const $ = _c(6); let x; if ($[0] !== props.bar) { x = []; @@ -38,12 +38,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/ssa-renaming-unconditional-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-with-mutation.expect.md index f4689e5795552..9f1e21d7c7497 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-unconditional-with-mutation.expect.md @@ -39,9 +39,9 @@ import { c as _c } from "react/compiler-runtime"; 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) { @@ -53,10 +53,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/ssa-renaming-via-destructuring-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-via-destructuring-with-mutation.expect.md index ed1056c47c0f8..81cc777522bca 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-via-destructuring-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-via-destructuring-with-mutation.expect.md @@ -35,9 +35,9 @@ import { c as _c } from "react/compiler-runtime"; 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) { @@ -46,10 +46,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/ssa-renaming-with-mutation.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-with-mutation.expect.md index 26cd73a82b53f..f48cec2c238ba 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-with-mutation.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/ssa-renaming-with-mutation.expect.md @@ -35,9 +35,9 @@ import { c as _c } from "react/compiler-runtime"; 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) { @@ -46,10 +46,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/switch-non-final-default.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-non-final-default.expect.md index 915218fcfa467..0a5e7103c602e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-non-final-default.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch-non-final-default.expect.md @@ -33,10 +33,10 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(7); + const $ = _c(8); let y; let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p2) { const x = []; bb0: switch (props.p0) { case 1: { @@ -45,11 +45,11 @@ function Component(props) { case true: { x.push(props.p2); let t1; - if ($[3] === Symbol.for("react.memo_cache_sentinel")) { + if ($[4] === Symbol.for("react.memo_cache_sentinel")) { t1 = []; - $[3] = t1; + $[4] = t1; } else { - t1 = $[3]; + t1 = $[4]; } y = t1; } @@ -62,23 +62,24 @@ function Component(props) { } t0 = ; - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.p0; + $[1] = props.p2; + $[2] = y; + $[3] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[2]; + t0 = $[3]; } const child = t0; y.push(props.p4); let t1; - if ($[4] !== y || $[5] !== child) { + if ($[5] !== y || $[6] !== child) { t1 = {child}; - $[4] = y; - $[5] = child; - $[6] = t1; + $[5] = y; + $[6] = child; + $[7] = t1; } else { - t1 = $[6]; + t1 = $[7]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md index 0c5aea9c7da9c..b83c1fcb7b68f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/switch.expect.md @@ -28,10 +28,10 @@ function Component(props) { ```javascript import { c as _c } from "react/compiler-runtime"; function Component(props) { - const $ = _c(6); + const $ = _c(8); let y; let t0; - if ($[0] !== props) { + if ($[0] !== props.p0 || $[1] !== props.p2 || $[2] !== props.p3) { const x = []; switch (props.p0) { case true: { @@ -44,23 +44,25 @@ function Component(props) { } t0 = ; - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.p0; + $[1] = props.p2; + $[2] = props.p3; + $[3] = y; + $[4] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[3]; + t0 = $[4]; } const child = t0; y.push(props.p4); let t1; - if ($[3] !== y || $[4] !== child) { + if ($[5] !== y || $[6] !== child) { t1 = {child}; - $[3] = y; - $[4] = child; - $[5] = t1; + $[5] = y; + $[6] = child; + $[7] = t1; } else { - t1 = $[5]; + t1 = $[7]; } return t1; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md index 856d1326407ab..cab72226d27ef 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-mutate-outer-value.expect.md @@ -28,9 +28,9 @@ import { c as _c } from "react/compiler-runtime"; const { shallowCopy, throwErrorWithMessage } = require("shared-runtime"); function Component(props) { - const $ = _c(3); + const $ = _c(5); let x; - if ($[0] !== props.a) { + if ($[0] !== props) { x = []; try { let t0; @@ -42,9 +42,17 @@ function Component(props) { } x.push(t0); } catch { - x.push(shallowCopy({ a: props.a })); + let t0; + if ($[3] !== props.a) { + t0 = shallowCopy({ a: props.a }); + $[3] = props.a; + $[4] = t0; + } else { + t0 = $[4]; + } + x.push(t0); } - $[0] = props.a; + $[0] = props; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md index f2e46a6aff315..db8877f061bc1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-function-expression-returns-caught-value.expect.md @@ -31,7 +31,7 @@ import { throwInput } from "shared-runtime"; function Component(props) { const $ = _c(4); let t0; - if ($[0] !== props.value) { + if ($[0] !== props) { t0 = () => { try { throwInput([props.value]); @@ -40,7 +40,7 @@ function Component(props) { return e; } }; - $[0] = props.value; + $[0] = props; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-object-method-returns-caught-value.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-object-method-returns-caught-value.expect.md index 83f97ff6cbfbc..b760716a0c72f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-object-method-returns-caught-value.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/try-catch-within-object-method-returns-caught-value.expect.md @@ -33,7 +33,7 @@ import { throwInput } from "shared-runtime"; function Component(props) { const $ = _c(2); let t0; - if ($[0] !== props.value) { + if ($[0] !== props) { const object = { foo() { try { @@ -46,7 +46,7 @@ function Component(props) { }; t0 = object.foo(); - $[0] = props.value; + $[0] = props; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md index 05e465000d6cd..03725703f73d9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/useMemo-multiple-if-else.expect.md @@ -33,11 +33,16 @@ import { c as _c } from "react/compiler-runtime"; import { useMemo } from "react"; function Component(props) { - const $ = _c(3); + const $ = _c(6); let t0; bb0: { let y; - if ($[0] !== props) { + if ( + $[0] !== props.cond || + $[1] !== props.a || + $[2] !== props.cond2 || + $[3] !== props.b + ) { y = []; if (props.cond) { y.push(props.a); @@ -48,12 +53,15 @@ function Component(props) { } y.push(props.b); - $[0] = props; - $[1] = y; - $[2] = t0; + $[0] = props.cond; + $[1] = props.a; + $[2] = props.cond2; + $[3] = props.b; + $[4] = y; + $[5] = t0; } else { - y = $[1]; - t0 = $[2]; + y = $[4]; + t0 = $[5]; } t0 = y; }