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 ae15c0abbe..cb778c3292 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectHoistablePropertyLoads.ts @@ -1,13 +1,6 @@ import {CompilerError} from '../CompilerError'; import {inRange} from '../ReactiveScopes/InferReactiveScopeVariables'; -import { - Set_equal, - Set_filter, - Set_intersect, - Set_union, - getOrInsertDefault, -} from '../Utils/utils'; -import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies'; +import {Set_intersect, Set_union, getOrInsertDefault} from '../Utils/utils'; import { BasicBlock, BlockId, @@ -17,16 +10,14 @@ import { Identifier, IdentifierId, InstructionId, - InstructionValue, ReactiveScopeDependency, ScopeId, } from './HIR'; -import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR'; /** - * Helper function for `PropagateScopeDependencies`. Uses control flow graph - * analysis to determine which `Identifier`s can be assumed to be non-null - * objects, on a per-block basis. + * Helper function for `PropagateScopeDependencies`. + * Uses control flow graph analysis to determine which `Identifier`s can + * be assumed to be non-null objects, on a per-block basis. * * Here is an example: * ```js @@ -51,16 +42,15 @@ import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR'; * } * ``` * - * Note that we currently do NOT account for mutable / declaration range when - * doing the CFG-based traversal, producing results that are technically + * Note that we currently do NOT account for mutable / declaration range + * when doing the CFG-based traversal, producing results that are technically * incorrect but filtered by PropagateScopeDeps (which only takes dependencies * on constructed value -- i.e. a scope's dependencies must have mutable ranges * ending earlier than the scope start). * - * Take this example, this function will infer x.foo.bar as non-nullable for - * bb0, via the intersection of bb1 & bb2 which in turn comes from bb3. This is - * technically incorrect bb0 is before / during x's mutable range. - * ``` + * Take this example, this function will infer x.foo.bar as non-nullable for bb0, + * via the intersection of bb1 & bb2 which in turn comes from bb3. This is technically + * incorrect bb0 is before / during x's mutable range. * bb0: * const x = ...; * if cond then bb1 else bb2 @@ -72,71 +62,27 @@ import {collectTemporariesSidemap} from './PropagateScopeDependenciesHIR'; * goto bb3: * bb3: * x.foo.bar - * ``` - * - * @param fn - * @param temporaries sidemap of identifier -> baseObject.a.b paths. Does not - * contain optional chains. - * @param hoistableFromOptionals sidemap of optionalBlock -> baseObject?.a - * optional paths for which it's safe to evaluate non-optional loads (see - * CollectOptionalChainDependencies). - * @returns */ export function collectHoistablePropertyLoads( fn: HIRFunction, temporaries: ReadonlyMap, - hoistableFromOptionals: ReadonlyMap, - nestedFnImmutableContext: ReadonlySet | null, -): ReadonlyMap { +): ReadonlyMap { const registry = new PropertyPathRegistry(); - const functionExpressionLoads = collectFunctionExpressionFakeLoads(fn); - const actuallyEvaluatedTemporaries = new Map( - [...temporaries].filter(([id]) => !functionExpressionLoads.has(id)), - ); + const nodes = collectNonNullsInBlocks(fn, temporaries, registry); + propagateNonNull(fn, nodes); - /** - * Due to current limitations of mutable range inference, there are edge cases in - * which we infer known-immutable values (e.g. props or hook params) to have a - * mutable range and scope. - * (see `destructure-array-declaration-to-context-var` fixture) - * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps - * is being rewritten to HIR). - */ - const knownImmutableIdentifiers = new Set(); - if (fn.fnType === 'Component' || fn.fnType === 'Hook') { - for (const p of fn.params) { - if (p.kind === 'Identifier') { - knownImmutableIdentifiers.add(p.identifier.id); - } - } - } - const nodes = collectNonNullsInBlocks(fn, { - temporaries: actuallyEvaluatedTemporaries, - knownImmutableIdentifiers, - hoistableFromOptionals, - registry, - nestedFnImmutableContext, - }); - propagateNonNull(fn, nodes, registry); - - return nodes; -} - -export function keyByScopeId( - fn: HIRFunction, - source: ReadonlyMap, -): ReadonlyMap { - const keyedByScopeId = new Map(); + const nodesKeyedByScopeId = new Map(); for (const [_, block] of fn.body.blocks) { if (block.terminal.kind === 'scope') { - keyedByScopeId.set( + nodesKeyedByScopeId.set( block.terminal.scope.id, - source.get(block.terminal.block)!, + nodes.get(block.terminal.block)!, ); } } - return keyedByScopeId; + + return nodesKeyedByScopeId; } export type BlockInfo = { @@ -150,21 +96,17 @@ export type BlockInfo = { */ type RootNode = { properties: Map; - optionalProperties: Map; parent: null; // Recorded to make later computations simpler fullPath: ReactiveScopeDependency; - hasOptional: boolean; root: IdentifierId; }; type PropertyPathNode = | { properties: Map; - optionalProperties: Map; parent: PropertyPathNode; fullPath: ReactiveScopeDependency; - hasOptional: boolean; } | RootNode; @@ -182,12 +124,10 @@ class PropertyPathRegistry { rootNode = { root: identifier.id, properties: new Map(), - optionalProperties: new Map(), fullPath: { identifier, path: [], }, - hasOptional: false, parent: null, }; this.roots.set(identifier.id, rootNode); @@ -199,20 +139,23 @@ class PropertyPathRegistry { parent: PropertyPathNode, entry: DependencyPathEntry, ): PropertyPathNode { - const map = entry.optional ? parent.optionalProperties : parent.properties; - let child = map.get(entry.property); + if (entry.optional) { + CompilerError.throwTodo({ + reason: 'handle optional nodes', + loc: GeneratedSource, + }); + } + let child = parent.properties.get(entry.property); if (child == null) { child = { properties: new Map(), - optionalProperties: new Map(), parent: parent, fullPath: { identifier: parent.fullPath.identifier, path: parent.fullPath.path.concat(entry), }, - hasOptional: parent.hasOptional || entry.optional, }; - map.set(entry.property, child); + parent.properties.set(entry.property, child); } return child; } @@ -241,77 +184,56 @@ class PropertyPathRegistry { } } -function getMaybeNonNullInInstruction( - instr: InstructionValue, - context: CollectNonNullsInBlocksContext, -): PropertyPathNode | null { - let path = null; - if (instr.kind === 'PropertyLoad') { - path = context.temporaries.get(instr.object.identifier.id) ?? { - identifier: instr.object.identifier, - path: [], - }; - } else if (instr.kind === 'Destructure') { - path = context.temporaries.get(instr.value.identifier.id) ?? null; - } else if (instr.kind === 'ComputedLoad') { - path = context.temporaries.get(instr.object.identifier.id) ?? null; - } - return path != null ? context.registry.getOrCreateProperty(path) : null; -} - -function isImmutableAtInstr( - identifier: Identifier, - instr: InstructionId, - context: CollectNonNullsInBlocksContext, -): boolean { - if (context.nestedFnImmutableContext != null) { - /** - * Comparing instructions ids across inner-outer function bodies is not valid, as they are numbered - */ - return context.nestedFnImmutableContext.has(identifier.id); - } else { - /** - * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges - * are not valid with respect to current instruction id numbering. - * We use attached reactive scope ranges as a proxy for mutable range, but this - * is an overestimate as (1) scope ranges merge and align to form valid program - * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to - * non-mutable identifiers. - * - * See comment in exported function for why we track known immutable identifiers. - */ - const mutableAtInstr = - identifier.mutableRange.end > identifier.mutableRange.start + 1 && - identifier.scope != null && - inRange( - { - id: instr, - }, - identifier.scope.range, - ); - return ( - !mutableAtInstr || context.knownImmutableIdentifiers.has(identifier.id) - ); - } -} - -type CollectNonNullsInBlocksContext = { - temporaries: ReadonlyMap; - knownImmutableIdentifiers: ReadonlySet; - hoistableFromOptionals: ReadonlyMap; - registry: PropertyPathRegistry; +function addNonNullPropertyPath( + source: Identifier, + sourceNode: PropertyPathNode, + instrId: InstructionId, + knownImmutableIdentifiers: Set, + result: Set, +): void { /** - * (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. + * Since this runs *after* buildReactiveScopeTerminals, identifier mutable ranges + * are not valid with respect to current instruction id numbering. + * We use attached reactive scope ranges as a proxy for mutable range, but this + * is an overestimate as (1) scope ranges merge and align to form valid program + * blocks and (2) passes like MemoizeFbtAndMacroOperands may assign scopes to + * non-mutable identifiers. + * + * See comment at top of function for why we track known immutable identifiers. */ - nestedFnImmutableContext: ReadonlySet | null; -}; + const isMutableAtInstr = + source.mutableRange.end > source.mutableRange.start + 1 && + source.scope != null && + inRange({id: instrId}, source.scope.range); + if ( + !isMutableAtInstr || + knownImmutableIdentifiers.has(sourceNode.fullPath.identifier.id) + ) { + result.add(sourceNode); + } +} + function collectNonNullsInBlocks( fn: HIRFunction, - context: CollectNonNullsInBlocksContext, + temporaries: ReadonlyMap, + registry: PropertyPathRegistry, ): ReadonlyMap { + /** + * Due to current limitations of mutable range inference, there are edge cases in + * which we infer known-immutable values (e.g. props or hook params) to have a + * mutable range and scope. + * (see `destructure-array-declaration-to-context-var` fixture) + * We track known immutable identifiers to reduce regressions (as PropagateScopeDeps + * is being rewritten to HIR). + */ + const knownImmutableIdentifiers = new Set(); + if (fn.fnType === 'Component' || fn.fnType === 'Hook') { + for (const p of fn.params) { + if (p.kind === 'Identifier') { + knownImmutableIdentifiers.add(p.identifier.id); + } + } + } /** * Known non-null objects such as functional component props can be safely * read from any block. @@ -323,66 +245,57 @@ function collectNonNullsInBlocks( fn.params[0].kind === 'Identifier' ) { const identifier = fn.params[0].identifier; - knownNonNullIdentifiers.add( - context.registry.getOrCreateIdentifier(identifier), - ); + knownNonNullIdentifiers.add(registry.getOrCreateIdentifier(identifier)); } const nodes = new Map(); for (const [_, block] of fn.body.blocks) { const assumedNonNullObjects = new Set( knownNonNullIdentifiers, ); + for (const instr of block.instructions) { + if (instr.value.kind === 'PropertyLoad') { + const source = temporaries.get(instr.value.object.identifier.id) ?? { + identifier: instr.value.object.identifier, + path: [], + }; + addNonNullPropertyPath( + instr.value.object.identifier, + registry.getOrCreateProperty(source), + instr.id, + knownImmutableIdentifiers, + assumedNonNullObjects, + ); + } else if (instr.value.kind === 'Destructure') { + const source = instr.value.value.identifier.id; + const sourceNode = temporaries.get(source); + if (sourceNode != null) { + addNonNullPropertyPath( + instr.value.value.identifier, + registry.getOrCreateProperty(sourceNode), + instr.id, + knownImmutableIdentifiers, + assumedNonNullObjects, + ); + } + } else if (instr.value.kind === 'ComputedLoad') { + const source = instr.value.object.identifier.id; + const sourceNode = temporaries.get(source); + if (sourceNode != null) { + addNonNullPropertyPath( + instr.value.object.identifier, + registry.getOrCreateProperty(sourceNode), + instr.id, + knownImmutableIdentifiers, + assumedNonNullObjects, + ); + } + } + } nodes.set(block.id, { block, assumedNonNullObjects, }); - const maybeOptionalChain = context.hoistableFromOptionals.get(block.id); - if (maybeOptionalChain != null) { - assumedNonNullObjects.add( - context.registry.getOrCreateProperty(maybeOptionalChain), - ); - continue; - } - for (const instr of block.instructions) { - const maybeNonNull = getMaybeNonNullInInstruction(instr.value, context); - if ( - maybeNonNull != null && - isImmutableAtInstr(maybeNonNull.fullPath.identifier, instr.id, context) - ) { - assumedNonNullObjects.add(maybeNonNull); - } - if ( - instr.value.kind === 'FunctionExpression' && - !fn.env.config.enableTreatFunctionDepsAsConditional - ) { - const innerFn = instr.value.loweredFunc; - const innerTemporaries = collectTemporariesSidemap( - innerFn.func, - new Set(), - ); - const innerOptionals = collectOptionalChainSidemap(innerFn.func); - const innerHoistableMap = collectHoistablePropertyLoads( - 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), - ), - ); - const innerHoistables = assertNonNull( - innerHoistableMap.get(innerFn.func.body.entry), - ); - for (const entry of innerHoistables.assumedNonNullObjects) { - assumedNonNullObjects.add(entry); - } - } - } } return nodes; } @@ -390,7 +303,6 @@ function collectNonNullsInBlocks( function propagateNonNull( fn: HIRFunction, nodes: ReadonlyMap, - registry: PropertyPathRegistry, ): void { const blockSuccessors = new Map>(); const terminalPreds = new Set(); @@ -476,17 +388,10 @@ function propagateNonNull( const prevObjects = assertNonNull(nodes.get(nodeId)).assumedNonNullObjects; const mergedObjects = Set_union(prevObjects, neighborAccesses); - reduceMaybeOptionalChains(mergedObjects, registry); assertNonNull(nodes.get(nodeId)).assumedNonNullObjects = mergedObjects; traversalState.set(nodeId, 'done'); - /** - * Note that it's not sufficient to compare set sizes since - * reduceMaybeOptionalChains may replace optional-chain loads with - * unconditional loads. This could in turn change `assumedNonNullObjects` of - * downstream blocks and backedges. - */ - changed ||= !Set_equal(prevObjects, mergedObjects); + changed ||= prevObjects.size !== mergedObjects.size; return changed; } const traversalState = new Map(); @@ -535,74 +440,3 @@ export function assertNonNull, U>( }); return value; } - -/** - * Any two optional chains with different operations . vs ?. but the same set of - * property strings paths de-duplicates. - * - * Intuitively: given ?.b, we know to be either hoistable or not. - * If unconditional reads from are hoistable, we can replace all - * ?.PROPERTY_STRING subpaths with .PROPERTY_STRING - */ -function reduceMaybeOptionalChains( - nodes: Set, - registry: PropertyPathRegistry, -): void { - let optionalChainNodes = Set_filter(nodes, n => n.hasOptional); - if (optionalChainNodes.size === 0) { - return; - } - let changed: boolean; - do { - changed = false; - - for (const original of optionalChainNodes) { - let {identifier, path: origPath} = original.fullPath; - let currNode: PropertyPathNode = - registry.getOrCreateIdentifier(identifier); - for (let i = 0; i < origPath.length; i++) { - const entry = origPath[i]; - // If the base is known to be non-null, replace with a non-optional load - const nextEntry: DependencyPathEntry = - entry.optional && nodes.has(currNode) - ? {property: entry.property, optional: false} - : entry; - currNode = PropertyPathRegistry.getOrCreatePropertyEntry( - currNode, - nextEntry, - ); - } - if (currNode !== original) { - changed = true; - optionalChainNodes.delete(original); - optionalChainNodes.add(currNode); - nodes.delete(original); - nodes.add(currNode); - } - } - } while (changed); -} - -function collectFunctionExpressionFakeLoads( - fn: HIRFunction, -): Set { - const sources = new Map(); - const functionExpressionReferences = new Set(); - - for (const [_, block] of fn.body.blocks) { - for (const {lvalue, value} of block.instructions) { - if (value.kind === 'FunctionExpression') { - for (const reference of value.loweredFunc.dependencies) { - let curr: IdentifierId | undefined = reference.identifier.id; - while (curr != null) { - functionExpressionReferences.add(curr); - curr = sources.get(curr); - } - } - } else if (value.kind === 'PropertyLoad') { - sources.set(lvalue.identifier.id, value.object.identifier.id); - } - } - } - return functionExpressionReferences; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts deleted file mode 100644 index 2f63b41cfd..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/CollectOptionalChainDependencies.ts +++ /dev/null @@ -1,387 +0,0 @@ -import {CompilerError} from '..'; -import {arrayNonNulls} from '../Utils/utils'; -import {assertNonNull} from './CollectHoistablePropertyLoads'; -import { - BlockId, - BasicBlock, - InstructionId, - IdentifierId, - ReactiveScopeDependency, - BranchTerminal, - TInstruction, - PropertyLoad, - StoreLocal, - GotoVariant, - TBasicBlock, - OptionalTerminal, - HIRFunction, -} from './HIR'; -import {printIdentifier} from './PrintHIR'; - -export function collectOptionalChainSidemap( - fn: HIRFunction, -): OptionalChainSidemap { - const context: OptionalTraversalContext = { - blocks: fn.body.blocks, - seenOptionals: new Set(), - processedInstrsInOptional: new Set(), - 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, - ); - } - } - - return { - temporariesReadInOptional: context.temporariesReadInOptional, - processedInstrsInOptional: context.processedInstrsInOptional, - hoistableObjects: context.hoistableObjects, - }; -} -export type OptionalChainSidemap = { - /** - * Stores the correct property mapping (e.g. `a?.b` instead of `a.b`) for - * dependency calculation. Note that we currently do not store anything on - * outer phi nodes. - */ - temporariesReadInOptional: ReadonlyMap; - /** - * Records instructions (PropertyLoads, StoreLocals, and test terminals) - * processed in this pass. When extracting dependencies in - * PropagateScopeDependencies, these instructions are skipped. - * - * E.g. given a?.b - * ``` - * bb0 - * $0 = LoadLocal 'a' - * test $0 then=bb1 <- Avoid adding dependencies from these instructions, as - * bb1 the sidemap produced by readOptionalBlock already maps - * $1 = PropertyLoad $0.'b' <- $1 and $2 back to a?.b. Instead, we want to add a?.b - * StoreLocal $2 = $1 <- as a dependency when $1 or $2 are later used in either - * - an unhoistable expression within an outer optional - * block e.g. MethodCall - * - a phi node (if the entire optional value is hoistable) - * ``` - * - * Note that mapping blockIds to their evaluated dependency path does not - * work, since values produced by inner optional chains may be referenced in - * outer ones - * ``` - * a?.b.c() - * -> - * bb0 - * $0 = LoadLocal 'a' - * test $0 then=bb1 - * bb1 - * $1 = PropertyLoad $0.'b' - * StoreLocal $2 = $1 - * goto bb2 - * bb2 - * test $2 then=bb3 - * bb3: - * $3 = PropertyLoad $2.'c' - * StoreLocal $4 = $3 - * goto bb4 - * bb4 - * test $4 then=bb5 - * bb5: - * $5 = MethodCall $2.$4() <--- here, we want to take a dep on $2 and $4! - * ``` - */ - processedInstrsInOptional: 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 - * the optional terminal in bb1. - * ```js - * bb1: - * ... - * Optional optional=false test=bb2 fallth=... - * bb2: - * Optional optional=true test=bb3 fallth=... - * ... - * ``` - */ - hoistableObjects: ReadonlyMap; -}; - -type OptionalTraversalContext = { - blocks: ReadonlyMap; - - // Track optional blocks to avoid outer calls into nested optionals - seenOptionals: Set; - - processedInstrsInOptional: Set; - temporariesReadInOptional: Map; - hoistableObjects: Map; -}; - -function matchOptionalTestBlock( - terminal: BranchTerminal, - blocks: ReadonlyMap, -): { - consequentId: IdentifierId; - property: string; - propertyId: IdentifierId; - storeLocalInstrId: InstructionId; - consequentGoto: BlockId; -} | null { - const consequentBlock = assertNonNull(blocks.get(terminal.consequent)); - if ( - consequentBlock.instructions.length === 2 && - consequentBlock.instructions[0].value.kind === 'PropertyLoad' && - consequentBlock.instructions[1].value.kind === 'StoreLocal' - ) { - const propertyLoad: TInstruction = consequentBlock - .instructions[0] as TInstruction; - const storeLocal: StoreLocal = consequentBlock.instructions[1].value; - const storeLocalInstrId = consequentBlock.instructions[1].id; - CompilerError.invariant( - propertyLoad.value.object.identifier.id === terminal.test.identifier.id, - { - reason: - '[OptionalChainDeps] Inconsistent optional chaining property load', - description: `Test=${printIdentifier(terminal.test.identifier)} PropertyLoad base=${printIdentifier(propertyLoad.value.object.identifier)}`, - loc: propertyLoad.loc, - }, - ); - - CompilerError.invariant( - storeLocal.value.identifier.id === propertyLoad.lvalue.identifier.id, - { - reason: '[OptionalChainDeps] Unexpected storeLocal', - loc: propertyLoad.loc, - }, - ); - if ( - consequentBlock.terminal.kind !== 'goto' || - consequentBlock.terminal.variant !== GotoVariant.Break - ) { - return null; - } - assertOptionalAlternateBlock(terminal, blocks); - return { - consequentId: storeLocal.lvalue.place.identifier.id, - property: propertyLoad.value.property, - propertyId: propertyLoad.lvalue.identifier.id, - storeLocalInstrId, - consequentGoto: consequentBlock.terminal.block, - }; - } - return null; -} - -function assertOptionalAlternateBlock( - terminal: BranchTerminal, - blocks: ReadonlyMap, -): void { - const alternate = assertNonNull(blocks.get(terminal.alternate)); - - CompilerError.invariant( - alternate.instructions.length === 2 && - alternate.instructions[0].value.kind === 'Primitive' && - alternate.instructions[1].value.kind === 'StoreLocal', - { - reason: 'Unexpected alternate structure', - loc: terminal.loc, - }, - ); -} - -/** - * Traverse into the optional block and all transitively referenced blocks to - * collect sidemaps of optional chain dependencies. - * - * @returns the IdentifierId representing the optional block if the block and - * all transitively referenced optional blocks precisely represent a chain of - * property loads. If any part of the optional chain is not hoistable, returns - * null. - */ -function traverseOptionalBlock( - optional: TBasicBlock, - context: OptionalTraversalContext, - outerAlternate: BlockId | null, -): IdentifierId | null { - context.seenOptionals.add(optional.id); - const maybeTest = context.blocks.get(optional.terminal.test)!; - let test: BranchTerminal; - let baseObject: ReactiveScopeDependency; - if (maybeTest.terminal.kind === 'branch') { - /** - * Explicitly calculate base of load - * - * Optional base expressions are currently within value blocks which cannot - * be interrupted by scope boundaries. As such, the only dependencies we can - * hoist out of optional chains are property load chains with no intervening - * instructions. - * - * Ideally, we would be able to flatten base instructions out of optional - * blocks, but this would require changes to HIR. - */ - CompilerError.invariant(optional.terminal.optional, { - reason: '[OptionalChainDeps] Expect base case to be always optional', - loc: optional.terminal.loc, - }); - /** - * Only match base expressions that are straightforward PropertyLoad chains - */ - if ( - maybeTest.instructions.length === 0 || - maybeTest.instructions[0].value.kind !== 'LoadLocal' - ) { - return null; - } - const path = maybeTest.instructions.slice(1).map((entry, i) => { - const instrVal = entry.value; - const prevEntry = maybeTest.instructions[i]; - if ( - instrVal.kind === 'PropertyLoad' && - instrVal.object.identifier.id === prevEntry.lvalue.identifier.id - ) { - return {property: instrVal.property, optional: false}; - } else { - return null; - } - }); - if (!arrayNonNulls(path)) { - return null; - } - CompilerError.invariant( - maybeTest.terminal.test.identifier.id === - maybeTest.instructions.at(-1)!.lvalue.identifier.id, - { - reason: '[OptionalChainDeps] Unexpected test expression', - loc: maybeTest.terminal.loc, - }, - ); - baseObject = { - identifier: maybeTest.instructions[0].value.place.identifier, - path, - }; - test = maybeTest.terminal; - } else if (maybeTest.terminal.kind === 'optional') { - /** - * This is either - * - ?.property (optional=true) - * - .property (optional=false) - * - - * - a optional base block with a separate nested optional-chain (e.g. a(c?.d)?.d) - */ - const testBlock = context.blocks.get(maybeTest.terminal.fallthrough)!; - if (testBlock!.terminal.kind !== 'branch') { - /** - * Fallthrough of the inner optional should be a block with no - * instructions, terminating with Test($) - */ - CompilerError.throwTodo({ - reason: `Unexpected terminal kind \`${testBlock.terminal.kind}\` for optional fallthrough block`, - loc: maybeTest.terminal.loc, - }); - } - /** - * Recurse into inner optional blocks to collect inner optional-chain - * expressions, regardless of whether we can match the outer one to a - * PropertyLoad. - */ - const innerOptional = traverseOptionalBlock( - maybeTest as TBasicBlock, - context, - testBlock.terminal.alternate, - ); - if (innerOptional == null) { - return null; - } - - /** - * Check that the inner optional is part of the same optional-chain as the - * outer one. This is not guaranteed, e.g. given a(c?.d)?.d - * ``` - * bb0: - * Optional test=bb1 - * bb1: - * $0 = LoadLocal a <-- part 1 of the outer optional-chaining base - * Optional test=bb2 fallth=bb5 <-- start of optional chain for c?.d - * bb2: - * ... (optional chain for c?.d) - * ... - * bb5: - * $1 = phi(c.d, undefined) <-- part 2 (continuation) of the outer optional-base - * $2 = Call $0($1) - * Branch $2 ... - * ``` - */ - if (testBlock.terminal.test.identifier.id !== innerOptional) { - return null; - } - - if (!optional.terminal.optional) { - /** - * If this is an non-optional load participating in an optional chain - * (e.g. loading the `c` property in `a?.b.c`), record that PropertyLoads - * from the inner optional value are hoistable. - */ - context.hoistableObjects.set( - optional.id, - assertNonNull(context.temporariesReadInOptional.get(innerOptional)), - ); - } - baseObject = assertNonNull( - context.temporariesReadInOptional.get(innerOptional), - ); - test = testBlock.terminal; - } else { - return null; - } - - if (test.alternate === outerAlternate) { - CompilerError.invariant(optional.instructions.length === 0, { - reason: - '[OptionalChainDeps] Unexpected instructions an inner optional block. ' + - 'This indicates that the compiler may be incorrectly concatenating two unrelated optional chains', - loc: optional.terminal.loc, - }); - } - const matchConsequentResult = matchOptionalTestBlock(test, context.blocks); - if (!matchConsequentResult) { - // Optional chain consequent is not hoistable e.g. a?.[computed()] - return null; - } - CompilerError.invariant( - matchConsequentResult.consequentGoto === optional.terminal.fallthrough, - { - reason: '[OptionalChainDeps] Unexpected optional goto-fallthrough', - description: `${matchConsequentResult.consequentGoto} != ${optional.terminal.fallthrough}`, - loc: optional.terminal.loc, - }, - ); - const load = { - identifier: baseObject.identifier, - path: [ - ...baseObject.path, - { - property: matchConsequentResult.property, - optional: optional.terminal.optional, - }, - ], - }; - context.processedInstrsInOptional.add( - matchConsequentResult.storeLocalInstrId, - ); - context.processedInstrsInOptional.add(test.id); - context.temporariesReadInOptional.set( - matchConsequentResult.consequentId, - load, - ); - context.temporariesReadInOptional.set(matchConsequentResult.propertyId, load); - return matchConsequentResult.consequentId; -} diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts index f5567b3e53..f2bb0b31f0 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/DeriveMinimalDependenciesHIR.ts @@ -6,173 +6,97 @@ */ import {CompilerError} from '../CompilerError'; -import { - DependencyPathEntry, - GeneratedSource, - Identifier, - ReactiveScopeDependency, -} from '../HIR'; +import {GeneratedSource, Identifier, ReactiveScopeDependency} from '../HIR'; import {printIdentifier} from '../HIR/PrintHIR'; import {ReactiveScopePropertyDependency} from '../ReactiveScopes/DeriveMinimalDependencies'; +const ENABLE_DEBUG_INVARIANTS = true; + /** * Simpler fork of DeriveMinimalDependencies, see PropagateScopeDependenciesHIR * for detailed explanation. */ export class ReactiveScopeDependencyTreeHIR { - /** - * Paths from which we can hoist PropertyLoads. If an `identifier`, - * `identifier.path`, or `identifier?.path` is in this map, it is safe to - * evaluate (non-optional) PropertyLoads from. - */ - #hoistableObjects: Map = new Map(); - #deps: Map = new Map(); + #roots: Map = new Map(); - /** - * @param hoistableObjects a set of paths from which we can safely evaluate - * PropertyLoads. Note that we expect these to not contain duplicates (e.g. - * both `a?.b` and `a.b`) only because CollectHoistablePropertyLoads merges - * duplicates when traversing the CFG. - */ - constructor(hoistableObjects: Iterable) { - for (const {path, identifier} of hoistableObjects) { - let currNode = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot( - identifier, - this.#hoistableObjects, - path.length > 0 && path[0].optional ? 'Optional' : 'NonNull', - ); - - for (let i = 0; i < path.length; i++) { - const prevAccessType = currNode.properties.get( - path[i].property, - )?.accessType; - const accessType = - i + 1 < path.length && path[i + 1].optional ? 'Optional' : 'NonNull'; - CompilerError.invariant( - prevAccessType == null || prevAccessType === accessType, - { - reason: 'Conflicting access types', - loc: GeneratedSource, - }, - ); - let nextNode = currNode.properties.get(path[i].property); - if (nextNode == null) { - nextNode = { - properties: new Map(), - accessType, - }; - currNode.properties.set(path[i].property, nextNode); - } - currNode = nextNode; - } - } - } - - static #getOrCreateRoot( + #getOrCreateRoot( identifier: Identifier, - roots: Map>, - defaultAccessType: T, - ): TreeNode { + accessType: PropertyAccessType, + ): DependencyNode { // roots can always be accessed unconditionally in JS - let rootNode = roots.get(identifier); + let rootNode = this.#roots.get(identifier); if (rootNode === undefined) { rootNode = { properties: new Map(), - accessType: defaultAccessType, + accessType, }; - roots.set(identifier, rootNode); + this.#roots.set(identifier, rootNode); } return rootNode; } - /** - * Join a dependency with `#hoistableObjects` to record the hoistable - * dependency. This effectively truncates @param dep to its maximal - * safe-to-evaluate subpath - */ addDependency(dep: ReactiveScopePropertyDependency): void { - const {identifier, path} = dep; - let depCursor = ReactiveScopeDependencyTreeHIR.#getOrCreateRoot( - identifier, - this.#deps, - PropertyAccessType.UnconditionalAccess, - ); - /** - * hoistableCursor is null if depCursor is not an object we can hoist - * property reads from otherwise, it represents the same node in the - * hoistable / cfg-informed tree - */ - let hoistableCursor: HoistableNode | undefined = - this.#hoistableObjects.get(identifier); + const {path} = dep; + let currNode = this.#getOrCreateRoot(dep.identifier, MIN_ACCESS_TYPE); - // All properties read 'on the way' to a dependency are marked as 'access' - for (const entry of path) { - let nextHoistableCursor: HoistableNode | undefined; - let nextDepCursor: DependencyNode; - if (entry.optional) { - /** - * No need to check the access type since we can match both optional or non-optionals - * in the hoistable - * e.g. a?.b is hoistable if a.b is hoistable - */ - if (hoistableCursor != null) { - nextHoistableCursor = hoistableCursor?.properties.get(entry.property); - } + const accessType = PropertyAccessType.Access; - let accessType; - if ( - hoistableCursor != null && - hoistableCursor.accessType === 'NonNull' - ) { - /** - * For an optional chain dep `a?.b`: if the hoistable tree only - * contains `a`, we can keep either `a?.b` or 'a.b' as a dependency. - * (note that we currently do the latter for perf) - */ - accessType = PropertyAccessType.UnconditionalAccess; - } else { - /** - * Given that it's safe to evaluate `depCursor` and optional load - * never throws, it's also safe to evaluate `depCursor?.entry` - */ - accessType = PropertyAccessType.OptionalAccess; - } - nextDepCursor = makeOrMergeProperty( - depCursor, - entry.property, - accessType, - ); - } else if ( - hoistableCursor != null && - hoistableCursor.accessType === 'NonNull' - ) { - nextHoistableCursor = hoistableCursor.properties.get(entry.property); - nextDepCursor = makeOrMergeProperty( - depCursor, - entry.property, - PropertyAccessType.UnconditionalAccess, - ); - } else { - /** - * Break to truncate the dependency on its first non-optional entry that PropertyLoads are not hoistable from - */ - break; - } - depCursor = nextDepCursor; - hoistableCursor = nextHoistableCursor; + currNode.accessType = merge(currNode.accessType, accessType); + + for (const property of path) { + // all properties read 'on the way' to a dependency are marked as 'access' + let currChild = makeOrMergeProperty( + currNode, + property.property, + accessType, + ); + currNode = currChild; } - // mark the final node as a dependency - depCursor.accessType = merge( - depCursor.accessType, - PropertyAccessType.OptionalDependency, + + /* + * If this property does not have a conditional path (i.e. a.b.c), the + * final property node should be marked as an conditional/unconditional + * `dependency` as based on control flow. + */ + currNode.accessType = merge( + currNode.accessType, + PropertyAccessType.Dependency, ); } + markNodesNonNull(dep: ReactiveScopePropertyDependency): void { + const accessType = PropertyAccessType.NonNullAccess; + let currNode = this.#roots.get(dep.identifier); + + let cursor = 0; + while (currNode != null && cursor < dep.path.length) { + currNode.accessType = merge(currNode.accessType, accessType); + currNode = currNode.properties.get(dep.path[cursor++].property); + } + if (currNode != null) { + currNode.accessType = merge(currNode.accessType, accessType); + } + } + + /** + * Derive a set of minimal dependencies that are safe to + * access unconditionally (with respect to nullthrows behavior) + */ deriveMinimalDependencies(): Set { const results = new Set(); - for (const [rootId, rootNode] of this.#deps.entries()) { - collectMinimalDependenciesInSubtree(rootNode, rootId, [], results); + for (const [rootId, rootNode] of this.#roots.entries()) { + if (ENABLE_DEBUG_INVARIANTS) { + assertWellFormedTree(rootNode); + } + const deps = deriveMinimalDependenciesInSubtree(rootNode, []); + + for (const dep of deps) { + results.add({ + identifier: rootId, + path: dep.path.map(s => ({property: s, optional: false})), + }); + } } return results; @@ -186,7 +110,7 @@ export class ReactiveScopeDependencyTreeHIR { printDeps(includeAccesses: boolean): string { let res: Array> = []; - for (const [rootId, rootNode] of this.#deps.entries()) { + for (const [rootId, rootNode] of this.#roots.entries()) { const rootResults = printSubtree(rootNode, includeAccesses).map( result => `${printIdentifier(rootId)}.${result}`, ); @@ -194,64 +118,31 @@ export class ReactiveScopeDependencyTreeHIR { } return res.flat().join('\n'); } - - static debug(roots: Map>): string { - const buf: Array = [`tree() [`]; - for (const [rootId, rootNode] of roots) { - buf.push(`${printIdentifier(rootId)} (${rootNode.accessType}):`); - this.#debugImpl(buf, rootNode, 1); - } - buf.push(']'); - return buf.length > 2 ? buf.join('\n') : buf.join(''); - } - - static #debugImpl( - buf: Array, - node: TreeNode, - depth: number = 0, - ): void { - for (const [property, childNode] of node.properties) { - buf.push(`${' '.repeat(depth)}.${property} (${childNode.accessType}):`); - this.#debugImpl(buf, childNode, depth + 1); - } - } } -/* - * Enum representing the access type of single property on a parent object. - * We distinguish on two independent axes: - * Optional / Unconditional: - * - whether this property is an optional load (within an optional chain) - * Access / Dependency: - * - Access: this property is read on the path of a dependency. We do not - * need to track change variables for accessed properties. Tracking accesses - * helps Forget do more granular dependency tracking. - * - Dependency: this property is read as a dependency and we must track changes - * to it for correctness. - * ```javascript - * // props.a is a dependency here and must be tracked - * deps: {props.a, props.a.b} ---> minimalDeps: {props.a} - * // props.a is just an access here and does not need to be tracked - * deps: {props.a.b} ---> minimalDeps: {props.a.b} - * ``` - */ enum PropertyAccessType { - OptionalAccess = 'OptionalAccess', - UnconditionalAccess = 'UnconditionalAccess', - OptionalDependency = 'OptionalDependency', - UnconditionalDependency = 'UnconditionalDependency', + Access = 'Access', + NonNullAccess = 'NonNullAccess', + Dependency = 'Dependency', + NonNullDependency = 'NonNullDependency', } -function isOptional(access: PropertyAccessType): boolean { +const MIN_ACCESS_TYPE = PropertyAccessType.Access; +/** + * "NonNull" means that PropertyReads from a node are side-effect free, + * as the node is (1) immutable and (2) has unconditional propertyloads + * somewhere in the cfg. + */ +function isNonNull(access: PropertyAccessType): boolean { return ( - access === PropertyAccessType.OptionalAccess || - access === PropertyAccessType.OptionalDependency + access === PropertyAccessType.NonNullAccess || + access === PropertyAccessType.NonNullDependency ); } function isDependency(access: PropertyAccessType): boolean { return ( - access === PropertyAccessType.OptionalDependency || - access === PropertyAccessType.UnconditionalDependency + access === PropertyAccessType.Dependency || + access === PropertyAccessType.NonNullDependency ); } @@ -259,70 +150,92 @@ function merge( access1: PropertyAccessType, access2: PropertyAccessType, ): PropertyAccessType { - const resultIsUnconditional = !(isOptional(access1) && isOptional(access2)); + const resultisNonNull = isNonNull(access1) || isNonNull(access2); const resultIsDependency = isDependency(access1) || isDependency(access2); /* * Straightforward merge. * This can be represented as bitwise OR, but is written out for readability * - * Observe that `UnconditionalAccess | ConditionalDependency` produces an + * Observe that `NonNullAccess | Dependency` produces an * unconditionally accessed conditional dependency. We currently use these * as we use unconditional dependencies. (i.e. to codegen change variables) */ - if (resultIsUnconditional) { + if (resultisNonNull) { if (resultIsDependency) { - return PropertyAccessType.UnconditionalDependency; + return PropertyAccessType.NonNullDependency; } else { - return PropertyAccessType.UnconditionalAccess; + return PropertyAccessType.NonNullAccess; } } else { - // result is optional if (resultIsDependency) { - return PropertyAccessType.OptionalDependency; + return PropertyAccessType.Dependency; } else { - return PropertyAccessType.OptionalAccess; + return PropertyAccessType.Access; } } } -type TreeNode = { - properties: Map>; - accessType: T; +type DependencyNode = { + properties: Map; + accessType: PropertyAccessType; }; -type HoistableNode = TreeNode<'Optional' | 'NonNull'>; -type DependencyNode = TreeNode; -/** - * TODO: this is directly pasted from DeriveMinimalDependencies. Since we no - * longer have conditionally accessed nodes, we can simplify - * - * Recursively calculates minimal dependencies in a subtree. - * @param node DependencyNode representing a dependency subtree. - * @returns a minimal list of dependencies in this subtree. - */ -function collectMinimalDependenciesInSubtree( +type ReduceResultNode = { + path: Array; +}; + +function assertWellFormedTree(node: DependencyNode): void { + let nonNullInChildren = false; + for (const childNode of node.properties.values()) { + assertWellFormedTree(childNode); + nonNullInChildren ||= isNonNull(childNode.accessType); + } + if (nonNullInChildren) { + CompilerError.invariant(isNonNull(node.accessType), { + reason: + '[DeriveMinimialDependencies] Not well formed tree, unexpected non-null node', + description: node.accessType, + loc: GeneratedSource, + }); + } +} + +function deriveMinimalDependenciesInSubtree( node: DependencyNode, - rootIdentifier: Identifier, - path: Array, - results: Set, -): void { + path: Array, +): Array { if (isDependency(node.accessType)) { - results.add({identifier: rootIdentifier, path}); + /** + * If this node is a dependency, we truncate the subtree + * and return this node. e.g. deps=[`obj.a`, `obj.a.b`] + * reduces to deps=[`obj.a`] + */ + return [{path}]; } else { - for (const [childName, childNode] of node.properties) { - collectMinimalDependenciesInSubtree( - childNode, - rootIdentifier, - [ - ...path, - { - property: childName, - optional: isOptional(childNode.accessType), - }, - ], - results, - ); + if (isNonNull(node.accessType)) { + /* + * Only recurse into subtree dependencies if this node + * is known to be non-null. + */ + const result: Array = []; + for (const [childName, childNode] of node.properties) { + result.push( + ...deriveMinimalDependenciesInSubtree(childNode, [ + ...path, + childName, + ]), + ); + } + return result; + } else { + /* + * This only occurs when this subtree contains a dependency, + * but this node is potentially nullish. As we currently + * don't record optional property paths as scope dependencies, + * we truncate and record this node as a dependency. + */ + return [{path}]; } } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts index 873082bdbe..615ec18feb 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -367,7 +367,6 @@ export type BasicBlock = { preds: Set; phis: Set; }; -export type TBasicBlock = BasicBlock & {terminal: T}; /* * Terminal nodes generally represent statements that affect control flow, such as 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 ab2cf4cf56..1fe218c352 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -18,8 +18,8 @@ import { IdentifierId, } from './HIR'; import { + BlockInfo, collectHoistablePropertyLoads, - keyByScopeId, } from './CollectHoistablePropertyLoads'; import { ScopeBlockTraversal, @@ -32,60 +32,37 @@ import {Stack, empty} from '../Utils/Stack'; import {CompilerError} from '../CompilerError'; import {Iterable_some} from '../Utils/utils'; import {ReactiveScopeDependencyTreeHIR} from './DeriveMinimalDependenciesHIR'; -import {collectOptionalChainSidemap} from './CollectOptionalChainDependencies'; export function propagateScopeDependenciesHIR(fn: HIRFunction): void { const usedOutsideDeclaringScope = findTemporariesUsedOutsideDeclaringScope(fn); const temporaries = collectTemporariesSidemap(fn, usedOutsideDeclaringScope); - const { - temporariesReadInOptional, - processedInstrsInOptional, - hoistableObjects, - } = collectOptionalChainSidemap(fn); - const hoistablePropertyLoads = keyByScopeId( - fn, - collectHoistablePropertyLoads(fn, temporaries, hoistableObjects, null), - ); + const hoistablePropertyLoads = collectHoistablePropertyLoads(fn, temporaries); const scopeDeps = collectDependencies( fn, usedOutsideDeclaringScope, - new Map([...temporaries, ...temporariesReadInOptional]), - processedInstrsInOptional, + temporaries, ); /** * Derive the minimal set of hoistable dependencies for each scope. */ for (const [scope, deps] of scopeDeps) { - if (deps.length === 0) { - continue; - } + const tree = new ReactiveScopeDependencyTreeHIR(); /** - * Step 1: Find hoistable accesses, given the basic block in which the scope - * begins. + * Step 1: Add every dependency used by this scope (e.g. `a.b.c`) */ - const hoistables = hoistablePropertyLoads.get(scope.id); - CompilerError.invariant(hoistables != null, { - reason: '[PropagateScopeDependencies] Scope not found in tracked blocks', - loc: GeneratedSource, - }); - /** - * Step 2: Calculate hoistable dependencies. - */ - const tree = new ReactiveScopeDependencyTreeHIR( - [...hoistables.assumedNonNullObjects].map(o => o.fullPath), - ); for (const dep of deps) { tree.addDependency({...dep}); } - /** - * Step 3: Reduce dependencies to a minimal set. + * Step 2: Mark hoistable dependencies, given the basic block in + * which the scope begins. */ + recordHoistablePropertyReads(hoistablePropertyLoads, scope.id, tree); const candidates = tree.deriveMinimalDependencies(); for (const candidateDep of candidates) { if ( @@ -211,7 +188,7 @@ function findTemporariesUsedOutsideDeclaringScope( * of $1, as the evaluation of `arr.length` changes between instructions $1 and * $3. We do not track $1 -> arr.length in this case. */ -export function collectTemporariesSidemap( +function collectTemporariesSidemap( fn: HIRFunction, usedOutsideDeclaringScope: ReadonlySet, ): ReadonlyMap { @@ -224,12 +201,7 @@ export function collectTemporariesSidemap( ); if (value.kind === 'PropertyLoad' && !usedOutside) { - const property = getProperty( - value.object, - value.property, - false, - temporaries, - ); + const property = getProperty(value.object, value.property, temporaries); temporaries.set(lvalue.identifier.id, property); } else if ( value.kind === 'LoadLocal' && @@ -250,7 +222,6 @@ export function collectTemporariesSidemap( function getProperty( object: Place, propertyName: string, - optional: boolean, temporaries: ReadonlyMap, ): ReactiveScopeDependency { /* @@ -282,12 +253,15 @@ function getProperty( if (resolvedDependency == null) { property = { identifier: object.identifier, - path: [{property: propertyName, optional}], + path: [{property: propertyName, optional: false}], }; } else { property = { identifier: resolvedDependency.identifier, - path: [...resolvedDependency.path, {property: propertyName, optional}], + path: [ + ...resolvedDependency.path, + {property: propertyName, optional: false}, + ], }; } return property; @@ -435,13 +409,8 @@ class Context { ); } - visitProperty(object: Place, property: string, optional: boolean): void { - const nextDependency = getProperty( - object, - property, - optional, - this.#temporaries, - ); + visitProperty(object: Place, property: string): void { + const nextDependency = getProperty(object, property, this.#temporaries); this.visitDependency(nextDependency); } @@ -520,7 +489,7 @@ function handleInstruction(instr: Instruction, context: Context): void { } } else if (value.kind === 'PropertyLoad') { if (context.isUsedOutsideDeclaringScope(lvalue)) { - context.visitProperty(value.object, value.property, false); + context.visitProperty(value.object, value.property); } } else if (value.kind === 'StoreLocal') { context.visitOperand(value.value); @@ -575,7 +544,6 @@ function collectDependencies( fn: HIRFunction, usedOutsideDeclaringScope: ReadonlySet, temporaries: ReadonlyMap, - processedInstrsInOptional: ReadonlySet, ): Map> { const context = new Context(usedOutsideDeclaringScope, temporaries); @@ -604,26 +572,33 @@ function collectDependencies( 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].id); - if (maybeOptionalChain) { - context.visitDependency(maybeOptionalChain); - } - } - } for (const instr of block.instructions) { - if (!processedInstrsInOptional.has(instr.id)) { - handleInstruction(instr, context); - } + handleInstruction(instr, context); } - - if (!processedInstrsInOptional.has(block.terminal.id)) { - for (const place of eachTerminalOperand(block.terminal)) { - context.visitOperand(place); - } + for (const place of eachTerminalOperand(block.terminal)) { + context.visitOperand(place); } } return context.deps; } + +/** + * Compute the set of hoistable property reads. + */ +function recordHoistablePropertyReads( + nodes: ReadonlyMap, + scopeId: ScopeId, + tree: ReactiveScopeDependencyTreeHIR, +): void { + const node = nodes.get(scopeId); + CompilerError.invariant(node != null, { + reason: '[PropagateScopeDependencies] Scope not found in tracked blocks', + loc: GeneratedSource, + }); + + for (const item of node.assumedNonNullObjects) { + tree.markNodesNonNull({ + ...item.fullPath, + }); + } +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts index 0836f84fdb..6b813d5975 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Utils/utils.ts @@ -82,17 +82,6 @@ export function getOrInsertDefault( return defaultValue; } } -export function Set_equal(a: ReadonlySet, b: ReadonlySet): boolean { - if (a.size !== b.size) { - return false; - } - for (const item of a) { - if (!b.has(item)) { - return false; - } - } - return true; -} export function Set_union(a: ReadonlySet, b: ReadonlySet): Set { const union = new Set(a); @@ -139,25 +128,6 @@ export function nonNull, U>( return value != null; } -export function arrayNonNulls, U>( - arr: Array, -): arr is Array { - return arr.every(e => e != null); -} - -export function Set_filter( - source: ReadonlySet, - fn: (arg: T) => boolean, -): Set { - const result = new Set(); - for (const entry of source) { - if (fn(entry)) { - result.add(entry); - } - } - return result; -} - export function hasNode( input: NodePath, ): input is NodePath> { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md index 8114b8a373..32e2c9fd64 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.expect.md @@ -2,15 +2,27 @@ ## Input ```javascript -import { CONST_TRUE, Stringify, setProperty } from "shared-runtime"; +import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime'; -function Component({arg}) { - const obj = CONST_TRUE ? {inner: {value: "hello"}} : null; +/** + * Fixture showing an edge case for ReactiveScope variable propagation. + * + * Found differences in evaluator results + * Non-forget (expected): + *
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
+ *
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
+ * Forget: + *
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
+ * [[ (exception in render) Error: invariant broken ]] + * + */ +function Component() { + const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null; const boxedInner = [obj?.inner]; - useHook(); - setProperty(obj, arg); + useIdentity(null); + mutate(obj); if (boxedInner[0] !== obj?.inner) { - throw new Error("invariant broken"); + throw new Error('invariant broken'); } return ; } @@ -18,8 +30,8 @@ function Component({arg}) { export const FIXTURE_ENTRYPOINT = { fn: Component, params: [{arg: 0}], - sequentialRenders: [{arg: 0}, {arg: 1}] -} + sequentialRenders: [{arg: 0}, {arg: 1}], +}; ``` @@ -27,37 +39,46 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; -import { CONST_TRUE, Stringify, setProperty } from "shared-runtime"; +import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime"; -function Component(t0) { - const $ = _c(5); - const { arg } = t0; +/** + * Fixture showing an edge case for ReactiveScope variable propagation. + * + * Found differences in evaluator results + * Non-forget (expected): + *
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
+ *
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
+ * Forget: + *
{"obj":{"inner":{"value":"hello"},"wat0":"joe"},"inner":["[[ cyclic ref *2 ]]"]}
+ * [[ (exception in render) Error: invariant broken ]] + * + */ +function Component() { + const $ = _c(4); const obj = CONST_TRUE ? { inner: { value: "hello" } } : null; - const t1 = obj?.inner; - let t2; - if ($[0] !== t1) { - t2 = [t1]; - $[0] = t1; - $[1] = t2; + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = [obj?.inner]; + $[0] = t0; } else { - t2 = $[1]; + t0 = $[0]; } - const boxedInner = t2; - useHook(); - setProperty(obj, arg); + const boxedInner = t0; + useIdentity(null); + mutate(obj); if (boxedInner[0] !== obj?.inner) { throw new Error("invariant broken"); } - let t3; - if ($[2] !== obj || $[3] !== boxedInner) { - t3 = ; - $[2] = obj; - $[3] = boxedInner; - $[4] = t3; + let t1; + if ($[1] !== obj || $[2] !== boxedInner) { + t1 = ; + $[1] = obj; + $[2] = boxedInner; + $[3] = t1; } else { - t3 = $[4]; + t1 = $[3]; } - return t3; + return t1; } export const FIXTURE_ENTRYPOINT = { @@ -67,7 +88,4 @@ export const FIXTURE_ENTRYPOINT = { }; ``` - -### Eval output -(kind: ok) [[ (exception in render) ReferenceError: useHook is not defined ]] -[[ (exception in render) ReferenceError: useHook is not defined ]] \ No newline at end of file + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx index f3be666062..a1a78bfa7e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-invalid-phi-as-dependency.tsx @@ -1,4 +1,4 @@ -import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime"; +import {CONST_TRUE, Stringify, mutate, useIdentity} from 'shared-runtime'; /** * Fixture showing an edge case for ReactiveScope variable propagation. @@ -13,12 +13,12 @@ import { CONST_TRUE, Stringify, mutate, useIdentity } from "shared-runtime"; * */ function Component() { - const obj = CONST_TRUE ? {inner: {value: "hello"}} : null; + const obj = CONST_TRUE ? {inner: {value: 'hello'}} : null; const boxedInner = [obj?.inner]; useIdentity(null); mutate(obj); if (boxedInner[0] !== obj?.inner) { - throw new Error("invariant broken"); + throw new Error('invariant broken'); } return ; } @@ -26,5 +26,5 @@ function Component() { export const FIXTURE_ENTRYPOINT = { fn: Component, params: [{arg: 0}], - sequentialRenders: [{arg: 0}, {arg: 1}] -} + sequentialRenders: [{arg: 0}, {arg: 1}], +}; 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 deleted file mode 100644 index 56ca1f7722..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.expect.md +++ /dev/null @@ -1,65 +0,0 @@ - -## Input - -```javascript -import {identity} from 'shared-runtime'; - -/** - * Not safe to hoist read of maybeNullObject.value.inner outside of the - * try-catch block, as that might throw - */ -function useFoo(maybeNullObject: {value: {inner: number}} | null) { - const y = []; - try { - y.push(identity(maybeNullObject.value.inner)); - } catch { - y.push('null'); - } - - return y; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [null], - sequentialRenders: [null, {value: 2}, {value: 3}, null], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { identity } from "shared-runtime"; - -/** - * Not safe to hoist read of maybeNullObject.value.inner outside of the - * try-catch block, as that might throw - */ -function useFoo(maybeNullObject) { - const $ = _c(2); - let y; - if ($[0] !== maybeNullObject.value.inner) { - y = []; - try { - y.push(identity(maybeNullObject.value.inner)); - } catch { - y.push("null"); - } - $[0] = maybeNullObject.value.inner; - $[1] = y; - } else { - y = $[1]; - } - return y; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [null], - sequentialRenders: [null, { value: 2 }, { value: 3 }, null], -}; - -``` - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.ts deleted file mode 100644 index 555ace1940..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/bug-try-catch-maybe-null-dependency.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {identity} from 'shared-runtime'; - -/** - * Not safe to hoist read of maybeNullObject.value.inner outside of the - * try-catch block, as that might throw - */ -function useFoo(maybeNullObject: {value: {inner: number}} | null) { - const y = []; - try { - y.push(identity(maybeNullObject.value.inner)); - } catch { - y.push('null'); - } - - return y; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [null], - sequentialRenders: [null, {value: 2}, {value: 3}, null], -}; 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 deleted file mode 100644 index 9bce09be6c..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.expect.md +++ /dev/null @@ -1,51 +0,0 @@ - -## Input - -```javascript -function useFoo({a}) { - let x = []; - x.push(a?.b.c?.d.e); - x.push(a.b?.c.d?.e); - return x; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: null}, {a: {}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -function useFoo(t0) { - const $ = _c(2); - const { a } = t0; - let x; - if ($[0] !== a.b.c.d) { - x = []; - x.push(a?.b.c?.d.e); - x.push(a.b?.c.d?.e); - $[0] = a.b.c.d; - $[1] = x; - } else { - x = $[1]; - } - return x; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: null }, { a: {} }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.ts deleted file mode 100644 index 910ecd0a2d..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/infer-sequential-optional-chain-nonnull.ts +++ /dev/null @@ -1,12 +0,0 @@ -function useFoo({a}) { - let x = []; - x.push(a?.b.c?.d.e); - x.push(a.b?.c.d?.e); - return x; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: null}, {a: {}}], -}; 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 deleted file mode 100644 index 0acf33b2ed..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.expect.md +++ /dev/null @@ -1,229 +0,0 @@ - -## Input - -```javascript -import {identity} from 'shared-runtime'; - -/** - * identity(...)?.toString() is the outer optional, and prop?.value is the inner - * one. - * Note that prop?. - */ -function useFoo({ - prop1, - prop2, - prop3, - prop4, - prop5, - prop6, -}: { - prop1: null | {value: number}; - prop2: null | {inner: {value: number}}; - prop3: null | {fn: (val: any) => NonNullable}; - prop4: null | {inner: {value: number}}; - prop5: null | {fn: (val: any) => NonNullable}; - prop6: null | {inner: {value: number}}; -}) { - // prop1?.value should be hoisted as the dependency of x - const x = identity(prop1?.value)?.toString(); - - // prop2?.inner.value should be hoisted as the dependency of y - const y = identity(prop2?.inner.value)?.toString(); - - // prop3 and prop4?.inner should be hoisted as the dependency of z - const z = prop3?.fn(prop4?.inner.value).toString(); - - // prop5 and prop6?.inner should be hoisted as the dependency of zz - const zz = prop5?.fn(prop6?.inner.value)?.toString(); - return [x, y, z, zz]; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - ], - sequentialRenders: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: 4}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: undefined}}, - prop3: {fn: identity}, - prop4: {inner: {value: undefined}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {}, - prop3: {fn: identity}, - prop4: {}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { identity } from "shared-runtime"; - -/** - * identity(...)?.toString() is the outer optional, and prop?.value is the inner - * one. - * Note that prop?. - */ -function useFoo(t0) { - const $ = _c(15); - const { prop1, prop2, prop3, prop4, prop5, prop6 } = t0; - let t1; - if ($[0] !== prop1?.value) { - t1 = identity(prop1?.value)?.toString(); - $[0] = prop1?.value; - $[1] = t1; - } else { - t1 = $[1]; - } - const x = t1; - let t2; - if ($[2] !== prop2?.inner) { - t2 = identity(prop2?.inner.value)?.toString(); - $[2] = prop2?.inner; - $[3] = t2; - } else { - t2 = $[3]; - } - const y = t2; - let t3; - if ($[4] !== prop3 || $[5] !== prop4) { - t3 = prop3?.fn(prop4?.inner.value).toString(); - $[4] = prop3; - $[5] = prop4; - $[6] = t3; - } else { - t3 = $[6]; - } - const z = t3; - let t4; - if ($[7] !== prop5 || $[8] !== prop6) { - t4 = prop5?.fn(prop6?.inner.value)?.toString(); - $[7] = prop5; - $[8] = prop6; - $[9] = t4; - } else { - t4 = $[9]; - } - const zz = t4; - let t5; - if ($[10] !== x || $[11] !== y || $[12] !== z || $[13] !== zz) { - t5 = [x, y, z, zz]; - $[10] = x; - $[11] = y; - $[12] = z; - $[13] = zz; - $[14] = t5; - } else { - t5 = $[14]; - } - return t5; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - ], - - sequentialRenders: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - { - prop1: { value: 2 }, - prop2: { inner: { value: 3 } }, - prop3: { fn: identity }, - prop4: { inner: { value: 4 } }, - prop5: { fn: identity }, - prop6: { inner: { value: 4 } }, - }, - { - prop1: { value: 2 }, - prop2: { inner: { value: 3 } }, - prop3: { fn: identity }, - prop4: { inner: { value: 4 } }, - prop5: { fn: identity }, - prop6: { inner: { value: undefined } }, - }, - { - prop1: { value: 2 }, - prop2: { inner: { value: undefined } }, - prop3: { fn: identity }, - prop4: { inner: { value: undefined } }, - prop5: { fn: identity }, - prop6: { inner: { value: undefined } }, - }, - { - prop1: { value: 2 }, - prop2: {}, - prop3: { fn: identity }, - prop4: {}, - prop5: { fn: identity }, - prop6: { inner: { value: undefined } }, - }, - ], -}; - -``` - -### Eval output -(kind: ok) [null,null,null,null] -["2","3","4","4"] -["2","3","4",null] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'toString') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'value') ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.ts deleted file mode 100644 index d00cb4fee6..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/nested-optional-chains.ts +++ /dev/null @@ -1,91 +0,0 @@ -import {identity} from 'shared-runtime'; - -/** - * identity(...)?.toString() is the outer optional, and prop?.value is the inner - * one. - * Note that prop?. - */ -function useFoo({ - prop1, - prop2, - prop3, - prop4, - prop5, - prop6, -}: { - prop1: null | {value: number}; - prop2: null | {inner: {value: number}}; - prop3: null | {fn: (val: any) => NonNullable}; - prop4: null | {inner: {value: number}}; - prop5: null | {fn: (val: any) => NonNullable}; - prop6: null | {inner: {value: number}}; -}) { - // prop1?.value should be hoisted as the dependency of x - const x = identity(prop1?.value)?.toString(); - - // prop2?.inner.value should be hoisted as the dependency of y - const y = identity(prop2?.inner.value)?.toString(); - - // prop3 and prop4?.inner should be hoisted as the dependency of z - const z = prop3?.fn(prop4?.inner.value).toString(); - - // prop5 and prop6?.inner should be hoisted as the dependency of zz - const zz = prop5?.fn(prop6?.inner.value)?.toString(); - return [x, y, z, zz]; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - ], - sequentialRenders: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: 4}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: undefined}}, - prop3: {fn: identity}, - prop4: {inner: {value: undefined}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {}, - prop3: {fn: identity}, - prop4: {}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md index 77875f789d..c34b79a848 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.expect.md @@ -3,29 +3,12 @@ ```javascript // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies -import {identity, ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; - -function Component({arg}) { +function Component(props) { const data = useMemo(() => { - return arg?.items.edges?.nodes.map(identity); - }, [arg?.items.edges?.nodes]); - return ( - - ); + return props?.items.edges?.nodes.map(); + }, [props?.items.edges?.nodes]); + return ; } -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: null}], - sequentialRenders: [ - {arg: null}, - {arg: null}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - ], -}; ``` @@ -33,66 +16,33 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies -import { identity, ValidateMemoization } from "shared-runtime"; -import { useMemo } from "react"; +function Component(props) { + const $ = _c(4); -function Component(t0) { - const $ = _c(7); - const { arg } = t0; - - arg?.items.edges?.nodes; + props?.items.edges?.nodes; + let t0; let t1; + if ($[0] !== props?.items.edges?.nodes) { + t1 = props?.items.edges?.nodes.map(); + $[0] = props?.items.edges?.nodes; + $[1] = t1; + } else { + t1 = $[1]; + } + t0 = t1; + const data = t0; let t2; - if ($[0] !== arg?.items.edges?.nodes) { - t2 = arg?.items.edges?.nodes.map(identity); - $[0] = arg?.items.edges?.nodes; - $[1] = t2; + if ($[2] !== data) { + t2 = ; + $[2] = data; + $[3] = t2; } else { - t2 = $[1]; + t2 = $[3]; } - t1 = t2; - const data = t1; - - const t3 = arg?.items.edges?.nodes; - let t4; - if ($[2] !== t3) { - t4 = [t3]; - $[2] = t3; - $[3] = t4; - } else { - t4 = $[3]; - } - let t5; - if ($[4] !== t4 || $[5] !== data) { - t5 = ; - $[4] = t4; - $[5] = data; - $[6] = t5; - } else { - t5 = $[6]; - } - return t5; + return t2; } -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ arg: null }], - sequentialRenders: [ - { arg: null }, - { arg: null }, - { arg: { items: { edges: null } } }, - { arg: { items: { edges: null } } }, - { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } }, - { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } }, - ], -}; - ``` ### Eval output -(kind: ok)
{"inputs":[null]}
-
{"inputs":[null]}
-
{"inputs":[null]}
-
{"inputs":[null]}
-
{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}
-
{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}
\ No newline at end of file +(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-as-memo-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js index 73f0f4d421..d82d36b547 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-as-memo-dep.js @@ -1,24 +1,7 @@ // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies -import {identity, ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; - -function Component({arg}) { +function Component(props) { const data = useMemo(() => { - return arg?.items.edges?.nodes.map(identity); - }, [arg?.items.edges?.nodes]); - return ( - - ); + return props?.items.edges?.nodes.map(); + }, [props?.items.edges?.nodes]); + return ; } -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: null}], - sequentialRenders: [ - {arg: null}, - {arg: null}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md index 6e44a97b45..a4cf6d767d 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.expect.md @@ -4,27 +4,15 @@ ```javascript // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies import {ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; -function Component({arg}) { +function Component(props) { const data = useMemo(() => { const x = []; - x.push(arg?.items); + x.push(props?.items); return x; - }, [arg?.items]); - return ; + }, [props?.items]); + return ; } -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: {items: 2}}], - sequentialRenders: [ - {arg: {items: 2}}, - {arg: {items: 2}}, - {arg: null}, - {arg: null}, - ], -}; - ``` ## Code @@ -32,60 +20,44 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies import { ValidateMemoization } from "shared-runtime"; -import { useMemo } from "react"; -function Component(t0) { +function Component(props) { const $ = _c(7); - const { arg } = t0; - arg?.items; - let t1; + props?.items; + let t0; let x; - if ($[0] !== arg?.items) { + if ($[0] !== props?.items) { x = []; - x.push(arg?.items); - $[0] = arg?.items; + x.push(props?.items); + $[0] = props?.items; $[1] = x; } else { x = $[1]; } - t1 = x; - const data = t1; - const t2 = arg?.items; + t0 = x; + const data = t0; + const t1 = props?.items; + let t2; + if ($[2] !== t1) { + t2 = [t1]; + $[2] = t1; + $[3] = t2; + } else { + t2 = $[3]; + } let t3; - if ($[2] !== t2) { - t3 = [t2]; - $[2] = t2; - $[3] = t3; - } else { - t3 = $[3]; - } - let t4; - if ($[4] !== t3 || $[5] !== data) { - t4 = ; - $[4] = t3; + if ($[4] !== t2 || $[5] !== data) { + t3 = ; + $[4] = t2; $[5] = data; - $[6] = t4; + $[6] = t3; } else { - t4 = $[6]; + t3 = $[6]; } - return t4; + return t3; } -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ arg: { items: 2 } }], - sequentialRenders: [ - { arg: { items: 2 } }, - { arg: { items: 2 } }, - { arg: null }, - { arg: null }, - ], -}; - ``` ### Eval output -(kind: ok)
{"inputs":[2],"output":[2]}
-
{"inputs":[2],"output":[2]}
-
{"inputs":[null],"output":[null]}
-
{"inputs":[null],"output":[null]}
\ No newline at end of file +(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-single.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js index 62ac31dd6d..5750d7af3a 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/optional-member-expression-single.js @@ -1,22 +1,10 @@ // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies import {ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; -function Component({arg}) { +function Component(props) { const data = useMemo(() => { const x = []; - x.push(arg?.items); + x.push(props?.items); return x; - }, [arg?.items]); - return ; + }, [props?.items]); + return ; } - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: {items: 2}}], - sequentialRenders: [ - {arg: {items: 2}}, - {arg: {items: 2}}, - {arg: null}, - {arg: null}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-call-chain-in-optional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-call-chain-in-optional.expect.md index e0196bdc19..8b52187920 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-call-chain-in-optional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-call-chain-in-optional.expect.md @@ -26,7 +26,7 @@ export const FIXTURE_ENTRYPONT = { 2 | function useFoo(props: {value: {x: string; y: string} | null}) { 3 | const value = props.value; > 4 | return createArray(value?.x, value?.y)?.join(', '); - | ^^^^^^^^ Todo: Unexpected terminal kind `optional` for optional fallthrough block (4:4) + | ^^^^^^^^ Todo: Unexpected terminal kind `optional` for optional test block (4:4) 5 | } 6 | 7 | function createArray(...args: Array): Array { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.expect.md new file mode 100644 index 0000000000..e885982310 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.expect.md @@ -0,0 +1,32 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +function Component(props) { + const data = useMemo(() => { + return props?.items.edges?.nodes.map(); + }, [props?.items.edges?.nodes]); + return ; +} + +``` + + +## Error + +``` + 1 | // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR + 2 | function Component(props) { +> 3 | const data = useMemo(() => { + | ^^^^^^^ +> 4 | return props?.items.edges?.nodes.map(); + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +> 5 | }, [props?.items.edges?.nodes]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (3:5) + 6 | return ; + 7 | } + 8 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.js new file mode 100644 index 0000000000..6ff87d0c46 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-as-memo-dep.js @@ -0,0 +1,7 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +function Component(props) { + const data = useMemo(() => { + return props?.items.edges?.nodes.map(); + }, [props?.items.edges?.nodes]); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.expect.md new file mode 100644 index 0000000000..3559b2bd58 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.expect.md @@ -0,0 +1,42 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + x.push(props.items); + return x; + }, [props.items]); + return ; +} + +``` + + +## Error + +``` + 2 | import {ValidateMemoization} from 'shared-runtime'; + 3 | function Component(props) { +> 4 | const data = useMemo(() => { + | ^^^^^^^ +> 5 | const x = []; + | ^^^^^^^^^^^^^^^^^ +> 6 | x.push(props?.items); + | ^^^^^^^^^^^^^^^^^ +> 7 | x.push(props.items); + | ^^^^^^^^^^^^^^^^^ +> 8 | return x; + | ^^^^^^^^^^^^^^^^^ +> 9 | }, [props.items]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:9) + 10 | return ; + 11 | } + 12 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.js similarity index 100% rename from compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.js rename to compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single-with-unconditional.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md new file mode 100644 index 0000000000..429f168836 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.expect.md @@ -0,0 +1,39 @@ + +## Input + +```javascript +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + return x; + }, [props?.items]); + return ; +} + +``` + + +## Error + +``` + 2 | import {ValidateMemoization} from 'shared-runtime'; + 3 | function Component(props) { +> 4 | const data = useMemo(() => { + | ^^^^^^^ +> 5 | const x = []; + | ^^^^^^^^^^^^^^^^^ +> 6 | x.push(props?.items); + | ^^^^^^^^^^^^^^^^^ +> 7 | return x; + | ^^^^^^^^^^^^^^^^^ +> 8 | }, [props?.items]); + | ^^^^ CannotPreserveMemoization: React Compiler has skipped optimizing this component because the existing manual memoization could not be preserved. The inferred dependencies did not match the manually specified dependencies, which could cause the value to change more or less frequently than expected (4:8) + 9 | return ; + 10 | } + 11 | +``` + + \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.js new file mode 100644 index 0000000000..535a0ce074 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/error.todo-optional-member-expression-single.js @@ -0,0 +1,10 @@ +// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR +import {ValidateMemoization} from 'shared-runtime'; +function Component(props) { + const data = useMemo(() => { + const x = []; + x.push(props?.items); + return x; + }, [props?.items]); + return ; +} diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.expect.md deleted file mode 100644 index b87453e287..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.expect.md +++ /dev/null @@ -1,54 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -function useFoo({a}) { - let x = []; - x.push(a?.b.c?.d.e); - x.push(a.b?.c.d?.e); - return x; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: null}, {a: {}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -function useFoo(t0) { - const $ = _c(2); - const { a } = t0; - let x; - 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.e; - $[1] = x; - } else { - x = $[1]; - } - return x; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: null }, { a: {} }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -[[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.ts deleted file mode 100644 index 81a4a33c61..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/infer-sequential-optional-chain-nonnull.ts +++ /dev/null @@ -1,14 +0,0 @@ -// @enablePropagateDepsInHIR - -function useFoo({a}) { - let x = []; - x.push(a?.b.c?.d.e); - x.push(a.b?.c.d?.e); - return x; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: null}, {a: {}}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.expect.md deleted file mode 100644 index 56b987c677..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.expect.md +++ /dev/null @@ -1,232 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {identity} from 'shared-runtime'; - -/** - * identity(...)?.toString() is the outer optional, and prop?.value is the inner - * one. - * Note that prop?. - */ -function useFoo({ - prop1, - prop2, - prop3, - prop4, - prop5, - prop6, -}: { - prop1: null | {value: number}; - prop2: null | {inner: {value: number}}; - prop3: null | {fn: (val: any) => NonNullable}; - prop4: null | {inner: {value: number}}; - prop5: null | {fn: (val: any) => NonNullable}; - prop6: null | {inner: {value: number}}; -}) { - // prop1?.value should be hoisted as the dependency of x - const x = identity(prop1?.value)?.toString(); - - // prop2?.inner.value should be hoisted as the dependency of y - const y = identity(prop2?.inner.value)?.toString(); - - // prop3 and prop4?.inner should be hoisted as the dependency of z - const z = prop3?.fn(prop4?.inner.value).toString(); - - // prop5 and prop6?.inner should be hoisted as the dependency of zz - const zz = prop5?.fn(prop6?.inner.value)?.toString(); - return [x, y, z, zz]; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - ], - sequentialRenders: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: 4}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: undefined}}, - prop3: {fn: identity}, - prop4: {inner: {value: undefined}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {}, - prop3: {fn: identity}, - prop4: {}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { identity } from "shared-runtime"; - -/** - * identity(...)?.toString() is the outer optional, and prop?.value is the inner - * one. - * Note that prop?. - */ -function useFoo(t0) { - const $ = _c(15); - const { prop1, prop2, prop3, prop4, prop5, prop6 } = t0; - let t1; - if ($[0] !== prop1?.value) { - t1 = identity(prop1?.value)?.toString(); - $[0] = prop1?.value; - $[1] = t1; - } else { - t1 = $[1]; - } - const x = t1; - let t2; - if ($[2] !== prop2?.inner.value) { - t2 = identity(prop2?.inner.value)?.toString(); - $[2] = prop2?.inner.value; - $[3] = t2; - } else { - t2 = $[3]; - } - const y = t2; - let t3; - if ($[4] !== prop3 || $[5] !== prop4?.inner) { - t3 = prop3?.fn(prop4?.inner.value).toString(); - $[4] = prop3; - $[5] = prop4?.inner; - $[6] = t3; - } else { - t3 = $[6]; - } - const z = t3; - let t4; - if ($[7] !== prop5 || $[8] !== prop6?.inner) { - t4 = prop5?.fn(prop6?.inner.value)?.toString(); - $[7] = prop5; - $[8] = prop6?.inner; - $[9] = t4; - } else { - t4 = $[9]; - } - const zz = t4; - let t5; - if ($[10] !== x || $[11] !== y || $[12] !== z || $[13] !== zz) { - t5 = [x, y, z, zz]; - $[10] = x; - $[11] = y; - $[12] = z; - $[13] = zz; - $[14] = t5; - } else { - t5 = $[14]; - } - return t5; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - ], - - sequentialRenders: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - { - prop1: { value: 2 }, - prop2: { inner: { value: 3 } }, - prop3: { fn: identity }, - prop4: { inner: { value: 4 } }, - prop5: { fn: identity }, - prop6: { inner: { value: 4 } }, - }, - { - prop1: { value: 2 }, - prop2: { inner: { value: 3 } }, - prop3: { fn: identity }, - prop4: { inner: { value: 4 } }, - prop5: { fn: identity }, - prop6: { inner: { value: undefined } }, - }, - { - prop1: { value: 2 }, - prop2: { inner: { value: undefined } }, - prop3: { fn: identity }, - prop4: { inner: { value: undefined } }, - prop5: { fn: identity }, - prop6: { inner: { value: undefined } }, - }, - { - prop1: { value: 2 }, - prop2: {}, - prop3: { fn: identity }, - prop4: {}, - prop5: { fn: identity }, - prop6: { inner: { value: undefined } }, - }, - ], -}; - -``` - -### Eval output -(kind: ok) [null,null,null,null] -["2","3","4","4"] -["2","3","4",null] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'toString') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'value') ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.ts deleted file mode 100644 index 48f3b2de2a..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/nested-optional-chains.ts +++ /dev/null @@ -1,93 +0,0 @@ -// @enablePropagateDepsInHIR - -import {identity} from 'shared-runtime'; - -/** - * identity(...)?.toString() is the outer optional, and prop?.value is the inner - * one. - * Note that prop?. - */ -function useFoo({ - prop1, - prop2, - prop3, - prop4, - prop5, - prop6, -}: { - prop1: null | {value: number}; - prop2: null | {inner: {value: number}}; - prop3: null | {fn: (val: any) => NonNullable}; - prop4: null | {inner: {value: number}}; - prop5: null | {fn: (val: any) => NonNullable}; - prop6: null | {inner: {value: number}}; -}) { - // prop1?.value should be hoisted as the dependency of x - const x = identity(prop1?.value)?.toString(); - - // prop2?.inner.value should be hoisted as the dependency of y - const y = identity(prop2?.inner.value)?.toString(); - - // prop3 and prop4?.inner should be hoisted as the dependency of z - const z = prop3?.fn(prop4?.inner.value).toString(); - - // prop5 and prop6?.inner should be hoisted as the dependency of zz - const zz = prop5?.fn(prop6?.inner.value)?.toString(); - return [x, y, z, zz]; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - ], - sequentialRenders: [ - { - prop1: null, - prop2: null, - prop3: null, - prop4: null, - prop5: null, - prop6: null, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: 4}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: 3}}, - prop3: {fn: identity}, - prop4: {inner: {value: 4}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {inner: {value: undefined}}, - prop3: {fn: identity}, - prop4: {inner: {value: undefined}}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - { - prop1: {value: 2}, - prop2: {}, - prop3: {fn: identity}, - prop4: {}, - prop5: {fn: identity}, - prop6: {inner: {value: undefined}}, - }, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md deleted file mode 100644 index d0486cd8c2..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.expect.md +++ /dev/null @@ -1,98 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {identity, ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; - -function Component({arg}) { - const data = useMemo(() => { - return arg?.items.edges?.nodes.map(identity); - }, [arg?.items.edges?.nodes]); - return ( - - ); -} -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: null}], - sequentialRenders: [ - {arg: null}, - {arg: null}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import { identity, ValidateMemoization } from "shared-runtime"; -import { useMemo } from "react"; - -function Component(t0) { - const $ = _c(7); - const { arg } = t0; - - arg?.items.edges?.nodes; - let t1; - let t2; - if ($[0] !== arg?.items.edges?.nodes) { - t2 = arg?.items.edges?.nodes.map(identity); - $[0] = arg?.items.edges?.nodes; - $[1] = t2; - } else { - t2 = $[1]; - } - t1 = t2; - const data = t1; - - const t3 = arg?.items.edges?.nodes; - let t4; - if ($[2] !== t3) { - t4 = [t3]; - $[2] = t3; - $[3] = t4; - } else { - t4 = $[3]; - } - let t5; - if ($[4] !== t4 || $[5] !== data) { - t5 = ; - $[4] = t4; - $[5] = data; - $[6] = t5; - } else { - t5 = $[6]; - } - return t5; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ arg: null }], - sequentialRenders: [ - { arg: null }, - { arg: null }, - { arg: { items: { edges: null } } }, - { arg: { items: { edges: null } } }, - { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } }, - { arg: { items: { edges: { nodes: [1, 2, "hello"] } } } }, - ], -}; - -``` - -### Eval output -(kind: ok)
{"inputs":[null]}
-
{"inputs":[null]}
-
{"inputs":[null]}
-
{"inputs":[null]}
-
{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}
-
{"inputs":[[1,2,"hello"]],"output":[1,2,"hello"]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.js deleted file mode 100644 index d248c472f5..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-as-memo-dep.js +++ /dev/null @@ -1,24 +0,0 @@ -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {identity, ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; - -function Component({arg}) { - const data = useMemo(() => { - return arg?.items.edges?.nodes.map(identity); - }, [arg?.items.edges?.nodes]); - return ( - - ); -} -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: null}], - sequentialRenders: [ - {arg: null}, - {arg: null}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: null}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - {arg: {items: {edges: {nodes: [1, 2, 'hello']}}}}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.expect.md deleted file mode 100644 index b4a55fcb61..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single-with-unconditional.expect.md +++ /dev/null @@ -1,62 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {ValidateMemoization} from 'shared-runtime'; -function Component(props) { - const data = useMemo(() => { - const x = []; - x.push(props?.items); - x.push(props.items); - return x; - }, [props.items]); - return ; -} - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import { ValidateMemoization } from "shared-runtime"; -function Component(props) { - const $ = _c(7); - let t0; - let x; - if ($[0] !== props.items) { - x = []; - x.push(props?.items); - x.push(props.items); - $[0] = props.items; - $[1] = x; - } else { - x = $[1]; - } - t0 = x; - const data = t0; - let t1; - if ($[2] !== props.items) { - t1 = [props.items]; - $[2] = props.items; - $[3] = t1; - } else { - t1 = $[3]; - } - let t2; - if ($[4] !== t1 || $[5] !== data) { - t2 = ; - $[4] = t1; - $[5] = data; - $[6] = t2; - } else { - t2 = $[6]; - } - return t2; -} - -``` - -### Eval output -(kind: exception) Fixture not implemented \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.expect.md deleted file mode 100644 index f15b9b8e9b..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.expect.md +++ /dev/null @@ -1,91 +0,0 @@ - -## Input - -```javascript -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; -function Component({arg}) { - const data = useMemo(() => { - const x = []; - x.push(arg?.items); - return x; - }, [arg?.items]); - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: {items: 2}}], - sequentialRenders: [ - {arg: {items: 2}}, - {arg: {items: 2}}, - {arg: null}, - {arg: null}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import { ValidateMemoization } from "shared-runtime"; -import { useMemo } from "react"; -function Component(t0) { - const $ = _c(7); - const { arg } = t0; - - arg?.items; - let t1; - let x; - if ($[0] !== arg?.items) { - x = []; - x.push(arg?.items); - $[0] = arg?.items; - $[1] = x; - } else { - x = $[1]; - } - t1 = x; - const data = t1; - const t2 = arg?.items; - let t3; - if ($[2] !== t2) { - t3 = [t2]; - $[2] = t2; - $[3] = t3; - } else { - t3 = $[3]; - } - let t4; - if ($[4] !== t3 || $[5] !== data) { - t4 = ; - $[4] = t3; - $[5] = data; - $[6] = t4; - } else { - t4 = $[6]; - } - return t4; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{ arg: { items: 2 } }], - sequentialRenders: [ - { arg: { items: 2 } }, - { arg: { items: 2 } }, - { arg: null }, - { arg: null }, - ], -}; - -``` - -### Eval output -(kind: ok)
{"inputs":[2],"output":[2]}
-
{"inputs":[2],"output":[2]}
-
{"inputs":[null],"output":[null]}
-
{"inputs":[null],"output":[null]}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.js deleted file mode 100644 index 8f54a0edb5..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/optional-member-expression-single.js +++ /dev/null @@ -1,22 +0,0 @@ -// @validatePreserveExistingMemoizationGuarantees @enableOptionalDependencies @enablePropagateDepsInHIR -import {ValidateMemoization} from 'shared-runtime'; -import {useMemo} from 'react'; -function Component({arg}) { - const data = useMemo(() => { - const x = []; - x.push(arg?.items); - return x; - }, [arg?.items]); - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Component, - params: [{arg: {items: 2}}], - sequentialRenders: [ - {arg: {items: 2}}, - {arg: {items: 2}}, - {arg: null}, - {arg: null}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md index 9a524e6357..6db3983d10 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reactive-dependencies-non-optional-properties-inside-optional-chain.expect.md @@ -16,9 +16,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function Component(props) { const $ = _c(2); let t0; - if ($[0] !== props.post.feedback.comments?.edges) { + if ($[0] !== props.post.feedback.comments) { t0 = props.post.feedback.comments?.edges?.map(render); - $[0] = props.post.feedback.comments?.edges; + $[0] = props.post.feedback.comments; $[1] = t0; } else { t0 = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md index f13bfe7d61..d56dcb63ae 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/conditional-member-expr.expect.md @@ -31,10 +31,10 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function Component(props) { const $ = _c(2); let x; - if ($[0] !== props.a?.b) { + if ($[0] !== props.a) { x = []; x.push(props.a?.b); - $[0] = props.a?.b; + $[0] = props.a; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-local-var.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-local-var.expect.md deleted file mode 100644 index c0f8aa97cd..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-local-var.expect.md +++ /dev/null @@ -1,97 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {shallowCopy, mutate, Stringify} from 'shared-runtime'; - -function useFoo({ - a, - shouldReadA, -}: { - a: {b: {c: number}; x: number}; - shouldReadA: boolean; -}) { - const local = shallowCopy(a); - mutate(local); - return ( - { - if (shouldReadA) return local.b.c; - return null; - }} - shouldInvokeFns={true} - /> - ); -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - 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 { shallowCopy, mutate, Stringify } from "shared-runtime"; - -function useFoo(t0) { - const $ = _c(5); - const { a, shouldReadA } = t0; - let local; - if ($[0] !== a) { - local = shallowCopy(a); - mutate(local); - $[0] = a; - $[1] = local; - } else { - local = $[1]; - } - let t1; - if ($[2] !== shouldReadA || $[3] !== local) { - t1 = ( - { - if (shouldReadA) { - return local.b.c; - } - return null; - }} - shouldInvokeFns={true} - /> - ); - $[2] = shouldReadA; - $[3] = local; - $[4] = t1; - } else { - t1 = $[4]; - } - return t1; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - 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 undefined (reading 'c') ]] -
{"fn":{"kind":"Function","result":null},"shouldInvokeFns":true}
-
{"fn":{"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-function-cond-access-local-var.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-local-var.tsx deleted file mode 100644 index fdf22dc970..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-local-var.tsx +++ /dev/null @@ -1,33 +0,0 @@ -// @enablePropagateDepsInHIR - -import {shallowCopy, mutate, Stringify} from 'shared-runtime'; - -function useFoo({ - a, - shouldReadA, -}: { - a: {b: {c: number}; x: number}; - shouldReadA: boolean; -}) { - const local = shallowCopy(a); - mutate(local); - return ( - { - if (shouldReadA) return local.b.c; - return null; - }} - shouldInvokeFns={true} - /> - ); -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - 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/infer-function-cond-access-not-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-not-hoisted.expect.md deleted file mode 100644 index e37b8365a2..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-not-hoisted.expect.md +++ /dev/null @@ -1,80 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function Foo({a, shouldReadA}) { - return ( - { - if (shouldReadA) return a.b.c; - return null; - }} - shouldInvokeFns={true} - /> - ); -} - -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 = ( - { - if (shouldReadA) { - return a.b.c; - } - return null; - }} - shouldInvokeFns={true} - /> - ); - $[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') ]] -
{"fn":{"kind":"Function","result":null},"shouldInvokeFns":true}
-
{"fn":{"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-function-cond-access-not-hoisted.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-not-hoisted.tsx deleted file mode 100644 index 5c71d57750..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-cond-access-not-hoisted.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function Foo({a, shouldReadA}) { - return ( - { - if (shouldReadA) return a.b.c; - return null; - }} - shouldInvokeFns={true} - /> - ); -} - -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/infer-function-uncond-access-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md deleted file mode 100644 index 395d9e1fdf..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md +++ /dev/null @@ -1,51 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b.c} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { Stringify } from "shared-runtime"; - -function useFoo(a) { - const $ = _c(2); - let t0; - if ($[0] !== a.b.c) { - t0 = a.b.c} shouldInvokeFns={true} />; - $[0] = a.b.c; - $[1] = t0; - } else { - t0 = $[1]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] \ 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-function-uncond-access-hoisted.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.tsx deleted file mode 100644 index a02139c409..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoisted.tsx +++ /dev/null @@ -1,13 +0,0 @@ -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b.c} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md deleted file mode 100644 index 89b4d281f8..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md +++ /dev/null @@ -1,92 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn will be uncond evaluated, so we can safely evaluate {a., - // a.b. [a, a.b.c]; - useIdentity(null); - const x = makeArray(); - if (cond) { - x.push(identity(a.b.c)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: 4}}, cond: true}, - {a: {b: {c: 4}}, cond: true}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { identity, makeArray, Stringify, useIdentity } from "shared-runtime"; - -function Foo(t0) { - const $ = _c(8); - const { a, cond } = t0; - let t1; - if ($[0] !== a) { - t1 = () => [a, a.b.c]; - $[0] = a; - $[1] = t1; - } else { - t1 = $[1]; - } - const fn = t1; - useIdentity(null); - let x; - if ($[2] !== cond || $[3] !== a.b.c) { - x = makeArray(); - if (cond) { - x.push(identity(a.b.c)); - } - $[2] = cond; - $[3] = a.b.c; - $[4] = x; - } else { - x = $[4]; - } - let t2; - if ($[5] !== fn || $[6] !== x) { - t2 = ; - $[5] = fn; - $[6] = x; - $[7] = t2; - } else { - t2 = $[7]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{ a: null, cond: true }], - sequentialRenders: [ - { a: null, cond: true }, - { a: { b: { c: 4 } }, cond: true }, - { a: { b: { c: 4 } }, cond: true }, - ], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -
{"fn":{"kind":"Function","result":[{"b":{"c":4}},4]},"x":[4],"shouldInvokeFns":true}
-
{"fn":{"kind":"Function","result":[{"b":{"c":4}},4]},"x":[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-function-uncond-access-hoists-other-dep.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.tsx deleted file mode 100644 index a9956ed8a5..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.tsx +++ /dev/null @@ -1,25 +0,0 @@ -// @enablePropagateDepsInHIR - -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn will be uncond evaluated, so we can safely evaluate {a., - // a.b. [a, a.b.c]; - useIdentity(null); - const x = makeArray(); - if (cond) { - x.push(identity(a.b.c)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: 4}}, cond: true}, - {a: {b: {c: 4}}, cond: true}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md deleted file mode 100644 index 741a30d7de..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.expect.md +++ /dev/null @@ -1,73 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {mutate, shallowCopy, Stringify} from 'shared-runtime'; - -function useFoo({a}: {a: {b: {c: number}}}) { - const local = shallowCopy(a); - mutate(local); - const fn = () => local.b.c; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { mutate, shallowCopy, Stringify } from "shared-runtime"; - -function useFoo(t0) { - const $ = _c(6); - const { a } = t0; - let local; - if ($[0] !== a) { - local = shallowCopy(a); - mutate(local); - $[0] = a; - $[1] = local; - } else { - local = $[1]; - } - let t1; - if ($[2] !== local.b.c) { - t1 = () => local.b.c; - $[2] = local.b.c; - $[3] = t1; - } else { - t1 = $[3]; - } - const fn = t1; - let t2; - if ($[4] !== fn) { - t2 = ; - $[4] = fn; - $[5] = t2; - } else { - t2 = $[5]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] -
{"fn":{"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-function-uncond-access-local-var.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.tsx deleted file mode 100644 index 16a0964351..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-access-local-var.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// @enablePropagateDepsInHIR - -import {mutate, shallowCopy, Stringify} from 'shared-runtime'; - -function useFoo({a}: {a: {b: {c: number}}}) { - const local = shallowCopy(a); - mutate(local); - const fn = () => local.b.c; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md deleted file mode 100644 index 591e04de7b..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md +++ /dev/null @@ -1,91 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn can be uncond evaluated, so we can safely evaluate a.b?.c. - const fn = () => [a, a.b?.c.d]; - useIdentity(null); - const arr = makeArray(); - if (cond) { - arr.push(identity(a.b?.c.e)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: {d: 5}}}, cond: true}, - {a: {b: null}, cond: false}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { identity, makeArray, Stringify, useIdentity } from "shared-runtime"; - -function Foo(t0) { - const $ = _c(8); - const { a, cond } = t0; - let t1; - if ($[0] !== a) { - t1 = () => [a, a.b?.c.d]; - $[0] = a; - $[1] = t1; - } else { - t1 = $[1]; - } - const fn = t1; - useIdentity(null); - let arr; - if ($[2] !== cond || $[3] !== a.b?.c.e) { - arr = makeArray(); - if (cond) { - arr.push(identity(a.b?.c.e)); - } - $[2] = cond; - $[3] = a.b?.c.e; - $[4] = arr; - } else { - arr = $[4]; - } - let t2; - if ($[5] !== fn || $[6] !== arr) { - t2 = ; - $[5] = fn; - $[6] = arr; - $[7] = t2; - } else { - t2 = $[7]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{ a: null, cond: true }], - sequentialRenders: [ - { a: null, cond: true }, - { a: { b: { c: { d: 5 } } }, cond: true }, - { a: { b: null }, cond: false }, - ], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -
{"fn":{"kind":"Function","result":[{"b":{"c":{"d":5}}},5]},"arr":[null],"shouldInvokeFns":true}
-
{"fn":{"kind":"Function","result":[{"b":null},null]},"arr":[],"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-function-uncond-optional-hoists-other-dep.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.tsx deleted file mode 100644 index 3b538de991..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.tsx +++ /dev/null @@ -1,24 +0,0 @@ -// @enablePropagateDepsInHIR - -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn can be uncond evaluated, so we can safely evaluate a.b?.c. - const fn = () => [a, a.b?.c.d]; - useIdentity(null); - const arr = makeArray(); - if (cond) { - arr.push(identity(a.b?.c.e)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: {d: 5}}}, cond: true}, - {a: {b: null}, cond: false}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access-local-var.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access-local-var.expect.md deleted file mode 100644 index ca65ce72bc..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access-local-var.expect.md +++ /dev/null @@ -1,73 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {shallowCopy, Stringify, mutate} from 'shared-runtime'; - -function useFoo({a}: {a: {b: {c: number}}}) { - const local = shallowCopy(a); - mutate(local); - const fn = () => [() => local.b.c]; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { shallowCopy, Stringify, mutate } from "shared-runtime"; - -function useFoo(t0) { - const $ = _c(6); - const { a } = t0; - let local; - if ($[0] !== a) { - local = shallowCopy(a); - mutate(local); - $[0] = a; - $[1] = local; - } else { - local = $[1]; - } - let t1; - if ($[2] !== local.b.c) { - t1 = () => [() => local.b.c]; - $[2] = local.b.c; - $[3] = t1; - } else { - t1 = $[3]; - } - const fn = t1; - let t2; - if ($[4] !== fn) { - t2 = ; - $[4] = fn; - $[5] = t2; - } else { - t2 = $[5]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] -
{"fn":{"kind":"Function","result":[{"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-nested-function-uncond-access-local-var.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access-local-var.tsx deleted file mode 100644 index d351a19464..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access-local-var.tsx +++ /dev/null @@ -1,16 +0,0 @@ -// @enablePropagateDepsInHIR - -import {shallowCopy, Stringify, mutate} from 'shared-runtime'; - -function useFoo({a}: {a: {b: {c: number}}}) { - const local = shallowCopy(a); - mutate(local); - const fn = () => [() => local.b.c]; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.expect.md deleted file mode 100644 index 421f7a2130..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.expect.md +++ /dev/null @@ -1,65 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - const fn = () => { - return () => ({ - value: a.b.c, - }); - }; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { Stringify } from "shared-runtime"; - -function useFoo(a) { - const $ = _c(4); - let t0; - if ($[0] !== a.b.c) { - t0 = () => () => ({ value: a.b.c }); - $[0] = a.b.c; - $[1] = t0; - } else { - t0 = $[1]; - } - const fn = t0; - let t1; - if ($[2] !== fn) { - t1 = ; - $[2] = fn; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] \ 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-nested-function-uncond-access.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.tsx deleted file mode 100644 index 1c711c0538..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-nested-function-uncond-access.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - const fn = () => { - return () => ({ - value: a.b.c, - }); - }; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-object-method-uncond-access.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-object-method-uncond-access.expect.md deleted file mode 100644 index 9685e0c87d..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-object-method-uncond-access.expect.md +++ /dev/null @@ -1,69 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {identity, Stringify} from 'shared-runtime'; - -function useFoo(a) { - const x = { - fn() { - return identity(a.b.c); - }, - }; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { identity, Stringify } from "shared-runtime"; - -function useFoo(a) { - const $ = _c(4); - let t0; - if ($[0] !== a.b.c) { - t0 = { - fn() { - return identity(a.b.c); - }, - }; - $[0] = a.b.c; - $[1] = t0; - } else { - t0 = $[1]; - } - const x = t0; - let t1; - if ($[2] !== x) { - t1 = ; - $[2] = x; - $[3] = t1; - } else { - t1 = $[3]; - } - return t1; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] \ 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-object-method-uncond-access.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-object-method-uncond-access.tsx deleted file mode 100644 index 812de3dde9..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/infer-object-method-uncond-access.tsx +++ /dev/null @@ -1,18 +0,0 @@ -// @enablePropagateDepsInHIR - -import {identity, Stringify} from 'shared-runtime'; - -function useFoo(a) { - const x = { - fn() { - return identity(a.b.c); - }, - }; - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md index a13a918a31..0f155c79de 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain.expect.md @@ -46,11 +46,11 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function Component(props) { const $ = _c(2); let x; - if ($[0] !== props.a.b) { + if ($[0] !== props.a) { x = []; x.push(props.a?.b); x.push(props.a.b.c); - $[0] = props.a.b; + $[0] = props.a; $[1] = x; } else { x = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md index df9dec4fb6..cf2d1d4137 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/memberexpr-join-optional-chain2.expect.md @@ -22,25 +22,16 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR function Component(props) { - const $ = _c(5); + const $ = _c(2); let x; - if ($[0] !== props.items?.length || $[1] !== props.items?.edges) { + if ($[0] !== props.items) { x = []; x.push(props.items?.length); - let t0; - if ($[3] !== props.items?.edges) { - t0 = props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []; - $[3] = props.items?.edges; - $[4] = t0; - } else { - t0 = $[4]; - } - x.push(t0); - $[0] = props.items?.length; - $[1] = props.items?.edges; - $[2] = x; + x.push(props.items?.edges?.map?.(render)?.filter?.(Boolean) ?? []); + $[0] = props.items; + $[1] = x; } else { - x = $[2]; + x = $[1]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/merge-uncond-optional-chain-and-cond.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/merge-uncond-optional-chain-and-cond.expect.md deleted file mode 100644 index 8703c30cb0..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/merge-uncond-optional-chain-and-cond.expect.md +++ /dev/null @@ -1,72 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR -import {identity} from 'shared-runtime'; - -/** - * Very contrived text fixture showing that it's technically incorrect to merge - * a conditional dependency (e.g. dep.path in `cond ? dep.path : ...`) and an - * unconditionally evaluated optional chain (`dep?.path`). - * - * - * when screen is non-null, useFoo returns { title: null } or "(not null)" - * when screen is null, useFoo throws - */ -function useFoo({screen}: {screen: null | undefined | {title_text: null}}) { - return screen?.title_text != null - ? '(not null)' - : identity({title: screen.title_text}); -} -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{screen: null}], - sequentialRenders: [{screen: {title_bar: undefined}}, {screen: null}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR -import { identity } from "shared-runtime"; - -/** - * Very contrived text fixture showing that it's technically incorrect to merge - * a conditional dependency (e.g. dep.path in `cond ? dep.path : ...`) and an - * unconditionally evaluated optional chain (`dep?.path`). - * - * - * when screen is non-null, useFoo returns { title: null } or "(not null)" - * when screen is null, useFoo throws - */ -function useFoo(t0) { - const $ = _c(2); - const { screen } = t0; - let t1; - if ($[0] !== screen) { - t1 = - screen?.title_text != null - ? "(not null)" - : identity({ title: screen.title_text }); - $[0] = screen; - $[1] = t1; - } else { - t1 = $[1]; - } - return t1; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ screen: null }], - sequentialRenders: [{ screen: { title_bar: undefined } }, { screen: null }], -}; - -``` - -### Eval output -(kind: ok) {} -[[ (exception in render) TypeError: Cannot read properties of null (reading 'title_text') ]] \ 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/merge-uncond-optional-chain-and-cond.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/merge-uncond-optional-chain-and-cond.ts deleted file mode 100644 index 2275412d77..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/reduce-reactive-deps/merge-uncond-optional-chain-and-cond.ts +++ /dev/null @@ -1,22 +0,0 @@ -// @enablePropagateDepsInHIR -import {identity} from 'shared-runtime'; - -/** - * Very contrived text fixture showing that it's technically incorrect to merge - * a conditional dependency (e.g. dep.path in `cond ? dep.path : ...`) and an - * unconditionally evaluated optional chain (`dep?.path`). - * - * - * when screen is non-null, useFoo returns { title: null } or "(not null)" - * when screen is null, useFoo throws - */ -function useFoo({screen}: {screen: null | undefined | {title_text: null}}) { - return screen?.title_text != null - ? '(not null)' - : identity({title: screen.title_text}); -} -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{screen: null}], - sequentialRenders: [{screen: {title_bar: undefined}}, {screen: null}], -}; 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 deleted file mode 100644 index 958f97d733..0000000000 --- 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 +++ /dev/null @@ -1,67 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b?.c.d?.e} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [ - {a: null}, - {a: {b: null}}, - {a: {b: {c: {d: null}}}}, - , - {a: {b: {c: {d: {e: 4}}}}}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR - -import { Stringify } from "shared-runtime"; - -function useFoo(a) { - const $ = _c(2); - let t0; - if ($[0] !== a.b) { - t0 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a.b; - $[1] = t0; - } else { - t0 = $[1]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [ - { a: null }, - { a: { b: null } }, - { a: { b: { c: { d: null } } } }, - - , - { a: { b: { c: { d: { e: 4 } } } } }, - ], -}; - -``` - -### Eval output -(kind: ok)
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
-
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
-
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
-[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] -
{"fn":{"kind":"Function"},"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/todo-infer-function-uncond-optionals-hoisted.tsx 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.tsx deleted file mode 100644 index 79be2ba6bd..0000000000 --- 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.tsx +++ /dev/null @@ -1,19 +0,0 @@ -// @enablePropagateDepsInHIR - -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b?.c.d?.e} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [ - {a: null}, - {a: {b: null}}, - {a: {b: {c: {d: null}}}}, - , - {a: {b: {c: {d: {e: 4}}}}}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md index 39f301432e..cf4e4f9327 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/repro-scope-missing-mutable-range.expect.md @@ -24,14 +24,14 @@ function HomeDiscoStoreItemTileRating(props) { const $ = _c(4); const item = useFragment(); let count; - if ($[0] !== item?.aggregates) { + if ($[0] !== item) { count = 0; const aggregates = item?.aggregates || []; aggregates.forEach((aggregate) => { count = count + (aggregate.count || 0); count; }); - $[0] = item?.aggregates; + $[0] = item; $[1] = count; } else { count = $[1]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.expect.md deleted file mode 100644 index cd2c4eb9df..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.expect.md +++ /dev/null @@ -1,79 +0,0 @@ - -## Input - -```javascript -// @enablePropagateDepsInHIR -import {identity} from 'shared-runtime'; - -/** - * Not safe to hoist read of maybeNullObject.value.inner outside of the - * try-catch block, as that might throw - */ -function useFoo(maybeNullObject: {value: {inner: number}} | null) { - const y = []; - try { - y.push(identity(maybeNullObject.value.inner)); - } catch { - y.push('null'); - } - - return y; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [null], - sequentialRenders: [null, {value: 2}, {value: 3}, null], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR -import { identity } from "shared-runtime"; - -/** - * Not safe to hoist read of maybeNullObject.value.inner outside of the - * try-catch block, as that might throw - */ -function useFoo(maybeNullObject) { - const $ = _c(4); - let y; - if ($[0] !== maybeNullObject) { - y = []; - try { - 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; - $[1] = y; - } else { - y = $[1]; - } - return y; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [null], - sequentialRenders: [null, { value: 2 }, { value: 3 }, null], -}; - -``` - -### Eval output -(kind: ok) ["null"] -[null] -[null] -["null"] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.ts b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.ts deleted file mode 100644 index bdbd903117..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-maybe-null-dependency.ts +++ /dev/null @@ -1,23 +0,0 @@ -// @enablePropagateDepsInHIR -import {identity} from 'shared-runtime'; - -/** - * Not safe to hoist read of maybeNullObject.value.inner outside of the - * try-catch block, as that might throw - */ -function useFoo(maybeNullObject: {value: {inner: number}} | null) { - const y = []; - try { - y.push(identity(maybeNullObject.value.inner)); - } catch { - y.push('null'); - } - - return y; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [null], - sequentialRenders: [null, {value: 2}, {value: 3}, null], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md index f69994b0a8..914001f373 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch-escaping.expect.md @@ -32,9 +32,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR const { throwInput } = require("shared-runtime"); function Component(props) { - const $ = _c(3); + const $ = _c(2); let x; - if ($[0] !== props.y || $[1] !== props.e) { + if ($[0] !== props) { try { const y = []; y.push(props.y); @@ -44,11 +44,10 @@ function Component(props) { e.push(props.e); x = e; } - $[0] = props.y; - $[1] = props.e; - $[2] = x; + $[0] = props; + $[1] = x; } else { - x = $[2]; + x = $[1]; } return x; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md index bc47228371..30ecdf6d59 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/propagate-scope-deps-hir-fork/try-catch-try-value-modified-in-catch.expect.md @@ -31,9 +31,9 @@ import { c as _c } from "react/compiler-runtime"; // @enablePropagateDepsInHIR const { throwInput } = require("shared-runtime"); function Component(props) { - const $ = _c(3); + const $ = _c(2); let t0; - if ($[0] !== props.y || $[1] !== props.e) { + if ($[0] !== props) { t0 = Symbol.for("react.early_return_sentinel"); bb0: { try { @@ -47,11 +47,10 @@ function Component(props) { break bb0; } } - $[0] = props.y; - $[1] = props.e; - $[2] = t0; + $[0] = props; + $[1] = t0; } else { - t0 = $[2]; + t0 = $[1]; } if (t0 !== Symbol.for("react.early_return_sentinel")) { return t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/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 deleted file mode 100644 index 4d45d3f3c6..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.expect.md +++ /dev/null @@ -1,73 +0,0 @@ - -## Input - -```javascript -import {Stringify} from 'shared-runtime'; - -function Foo({a, shouldReadA}) { - return ( - { - if (shouldReadA) return a.b.c; - return null; - }} - shouldInvokeFns={true} - /> - ); -} - -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"; -import { Stringify } from "shared-runtime"; - -function Foo(t0) { - const $ = _c(3); - const { a, shouldReadA } = t0; - let t1; - if ($[0] !== shouldReadA || $[1] !== a.b.c) { - t1 = ( - { - if (shouldReadA) { - return a.b.c; - } - return null; - }} - shouldInvokeFns={true} - /> - ); - $[0] = shouldReadA; - $[1] = a.b.c; - $[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 }, - ], -}; - -``` - \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.tsx deleted file mode 100644 index e571ee7b95..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {Stringify} from 'shared-runtime'; - -function Foo({a, shouldReadA}) { - return ( - { - if (shouldReadA) return a.b.c; - return null; - }} - shouldInvokeFns={true} - /> - ); -} - -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/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md deleted file mode 100644 index ec9b504e8d..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoisted.expect.md +++ /dev/null @@ -1,48 +0,0 @@ - -## Input - -```javascript -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b.c} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { Stringify } from "shared-runtime"; - -function useFoo(a) { - const $ = _c(2); - let t0; - if ($[0] !== a.b.c) { - t0 = a.b.c} shouldInvokeFns={true} />; - $[0] = a.b.c; - $[1] = t0; - } else { - t0 = $[1]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [{ a: null }, { a: { b: { c: 4 } } }], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] -[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'c') ]] \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoisted.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoisted.tsx deleted file mode 100644 index 4895a7002a..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoisted.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b.c} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [{a: null}, {a: {b: {c: 4}}}], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md deleted file mode 100644 index dd5f2c145e..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.expect.md +++ /dev/null @@ -1,89 +0,0 @@ - -## Input - -```javascript -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn will be uncond evaluated, so we can safely evaluate {a., - // a.b. [a, a.b.c]; - useIdentity(null); - const x = makeArray(); - if (cond) { - x.push(identity(a.b.c)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: 4}}, cond: true}, - {a: {b: {c: 4}}, cond: true}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { identity, makeArray, Stringify, useIdentity } from "shared-runtime"; - -function Foo(t0) { - const $ = _c(8); - const { a, cond } = t0; - let t1; - if ($[0] !== a) { - t1 = () => [a, a.b.c]; - $[0] = a; - $[1] = t1; - } else { - t1 = $[1]; - } - const fn = t1; - useIdentity(null); - let x; - if ($[2] !== cond || $[3] !== a) { - x = makeArray(); - if (cond) { - x.push(identity(a.b.c)); - } - $[2] = cond; - $[3] = a; - $[4] = x; - } else { - x = $[4]; - } - let t2; - if ($[5] !== fn || $[6] !== x) { - t2 = ; - $[5] = fn; - $[6] = x; - $[7] = t2; - } else { - t2 = $[7]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{ a: null, cond: true }], - sequentialRenders: [ - { a: null, cond: true }, - { a: { b: { c: 4 } }, cond: true }, - { a: { b: { c: 4 } }, cond: true }, - ], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -
{"fn":{"kind":"Function","result":[{"b":{"c":4}},4]},"x":[4],"shouldInvokeFns":true}
-
{"fn":{"kind":"Function","result":[{"b":{"c":4}},4]},"x":[4],"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.tsx deleted file mode 100644 index fdc4babc6d..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-access-hoists-other-dep.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn will be uncond evaluated, so we can safely evaluate {a., - // a.b. [a, a.b.c]; - useIdentity(null); - const x = makeArray(); - if (cond) { - x.push(identity(a.b.c)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: 4}}, cond: true}, - {a: {b: {c: 4}}, cond: true}, - ], -}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md deleted file mode 100644 index bc4d0fc3df..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.expect.md +++ /dev/null @@ -1,88 +0,0 @@ - -## Input - -```javascript -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn can be uncond evaluated, so we can safely evaluate a.b?.c. - const fn = () => [a, a.b?.c.d]; - useIdentity(null); - const arr = makeArray(); - if (cond) { - arr.push(identity(a.b?.c.e)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: {d: 5}}}, cond: true}, - {a: {b: null}, cond: false}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { identity, makeArray, Stringify, useIdentity } from "shared-runtime"; - -function Foo(t0) { - const $ = _c(8); - const { a, cond } = t0; - let t1; - if ($[0] !== a) { - t1 = () => [a, a.b?.c.d]; - $[0] = a; - $[1] = t1; - } else { - t1 = $[1]; - } - const fn = t1; - useIdentity(null); - let arr; - if ($[2] !== cond || $[3] !== a) { - arr = makeArray(); - if (cond) { - arr.push(identity(a.b?.c.e)); - } - $[2] = cond; - $[3] = a; - $[4] = arr; - } else { - arr = $[4]; - } - let t2; - if ($[5] !== fn || $[6] !== arr) { - t2 = ; - $[5] = fn; - $[6] = arr; - $[7] = t2; - } else { - t2 = $[7]; - } - return t2; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{ a: null, cond: true }], - sequentialRenders: [ - { a: null, cond: true }, - { a: { b: { c: { d: 5 } } }, cond: true }, - { a: { b: null }, cond: false }, - ], -}; - -``` - -### Eval output -(kind: ok) [[ (exception in render) TypeError: Cannot read properties of null (reading 'b') ]] -
{"fn":{"kind":"Function","result":[{"b":{"c":{"d":5}}},5]},"arr":[null],"shouldInvokeFns":true}
-
{"fn":{"kind":"Function","result":[{"b":null},null]},"arr":[],"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.tsx deleted file mode 100644 index 7facd98539..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/infer-function-uncond-optional-hoists-other-dep.tsx +++ /dev/null @@ -1,22 +0,0 @@ -import {identity, makeArray, Stringify, useIdentity} from 'shared-runtime'; - -function Foo({a, cond}) { - // Assume fn can be uncond evaluated, so we can safely evaluate a.b?.c. - const fn = () => [a, a.b?.c.d]; - useIdentity(null); - const arr = makeArray(); - if (cond) { - arr.push(identity(a.b?.c.e)); - } - return ; -} - -export const FIXTURE_ENTRYPOINT = { - fn: Foo, - params: [{a: null, cond: true}], - sequentialRenders: [ - {a: null, cond: true}, - {a: {b: {c: {d: 5}}}, cond: true}, - {a: {b: null}, cond: false}, - ], -}; 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 deleted file mode 100644 index 499abf992c..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.expect.md +++ /dev/null @@ -1,64 +0,0 @@ - -## Input - -```javascript -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b?.c.d?.e} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [ - {a: null}, - {a: {b: null}}, - {a: {b: {c: {d: null}}}}, - , - {a: {b: {c: {d: {e: 4}}}}}, - ], -}; - -``` - -## Code - -```javascript -import { c as _c } from "react/compiler-runtime"; -import { Stringify } from "shared-runtime"; - -function useFoo(a) { - const $ = _c(2); - let t0; - if ($[0] !== a.b) { - t0 = a.b?.c.d?.e} shouldInvokeFns={true} />; - $[0] = a.b; - $[1] = t0; - } else { - t0 = $[1]; - } - return t0; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{ a: null }], - sequentialRenders: [ - { a: null }, - { a: { b: null } }, - { a: { b: { c: { d: null } } } }, - - , - { a: { b: { c: { d: { e: 4 } } } } }, - ], -}; - -``` - -### Eval output -(kind: ok)
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
-
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
-
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
-[[ (exception in render) TypeError: Cannot read properties of undefined (reading 'b') ]] -
{"fn":{"kind":"Function"},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.tsx b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.tsx deleted file mode 100644 index 076be2089a..0000000000 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/reduce-reactive-deps/todo-infer-function-uncond-optionals-hoisted.tsx +++ /dev/null @@ -1,17 +0,0 @@ -import {Stringify} from 'shared-runtime'; - -function useFoo(a) { - return a.b?.c.d?.e} shouldInvokeFns={true} />; -} - -export const FIXTURE_ENTRYPOINT = { - fn: useFoo, - params: [{a: null}], - sequentialRenders: [ - {a: null}, - {a: {b: null}}, - {a: {b: {c: {d: null}}}}, - , - {a: {b: {c: {d: {e: 4}}}}}, - ], -}; diff --git a/compiler/packages/snap/src/SproutTodoFilter.ts b/compiler/packages/snap/src/SproutTodoFilter.ts index 5baca4b5dc..4333a4c327 100644 --- a/compiler/packages/snap/src/SproutTodoFilter.ts +++ b/compiler/packages/snap/src/SproutTodoFilter.ts @@ -478,9 +478,7 @@ const skipFilter = new Set([ 'fbt/bug-fbt-plural-multiple-function-calls', 'fbt/bug-fbt-plural-multiple-mixed-call-tag', 'bug-invalid-hoisting-functionexpr', - 'bug-try-catch-maybe-null-dependency', 'bug-invalid-phi-as-dependency', - 'reduce-reactive-deps/bug-infer-function-cond-access-not-hoisted', 'reduce-reactive-deps/bug-merge-uncond-optional-chain-and-cond', 'original-reactive-scopes-fork/bug-nonmutating-capture-in-unsplittable-memo-block', 'original-reactive-scopes-fork/bug-hoisted-declaration-with-scope', diff --git a/packages/react-client/src/ReactFlightClient.js b/packages/react-client/src/ReactFlightClient.js index 83c509dab4..2f60ccddb4 100644 --- a/packages/react-client/src/ReactFlightClient.js +++ b/packages/react-client/src/ReactFlightClient.js @@ -1287,6 +1287,21 @@ function parseModelString( createFormData, ); } + case 'Z': { + // Error + if (__DEV__) { + const ref = value.slice(2); + return getOutlinedModel( + response, + ref, + parentObject, + key, + resolveErrorDev, + ); + } else { + return resolveErrorProd(response); + } + } case 'i': { // Iterator const ref = value.slice(2); @@ -1881,11 +1896,7 @@ function formatV8Stack( } type ErrorWithDigest = Error & {digest?: string}; -function resolveErrorProd( - response: Response, - id: number, - digest: string, -): void { +function resolveErrorProd(response: Response): Error { if (__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes @@ -1899,25 +1910,17 @@ function resolveErrorProd( ' may provide additional details about the nature of the error.', ); error.stack = 'Error: ' + error.message; - (error: any).digest = digest; - const errorWithDigest: ErrorWithDigest = (error: any); - const chunks = response._chunks; - const chunk = chunks.get(id); - if (!chunk) { - chunks.set(id, createErrorChunk(response, errorWithDigest)); - } else { - triggerErrorOnChunk(chunk, errorWithDigest); - } + return error; } function resolveErrorDev( response: Response, - id: number, - digest: string, - message: string, - stack: ReactStackTrace, - env: string, -): void { + errorInfo: {message: string, stack: ReactStackTrace, env: string, ...}, +): Error { + const message: string = errorInfo.message; + const stack: ReactStackTrace = errorInfo.stack; + const env: string = errorInfo.env; + if (!__DEV__) { // These errors should never make it into a build so we don't need to encode them in codes.json // eslint-disable-next-line react-internal/prod-error-codes @@ -1957,16 +1960,8 @@ function resolveErrorDev( } } - (error: any).digest = digest; (error: any).environmentName = env; - const errorWithDigest: ErrorWithDigest = (error: any); - const chunks = response._chunks; - const chunk = chunks.get(id); - if (!chunk) { - chunks.set(id, createErrorChunk(response, errorWithDigest)); - } else { - triggerErrorOnChunk(chunk, errorWithDigest); - } + return error; } function resolvePostponeProd(response: Response, id: number): void { @@ -2622,17 +2617,20 @@ function processFullStringRow( } case 69 /* "E" */: { const errorInfo = JSON.parse(row); + let error; if (__DEV__) { - resolveErrorDev( - response, - id, - errorInfo.digest, - errorInfo.message, - errorInfo.stack, - errorInfo.env, - ); + error = resolveErrorDev(response, errorInfo); } else { - resolveErrorProd(response, id, errorInfo.digest); + error = resolveErrorProd(response); + } + (error: any).digest = errorInfo.digest; + const errorWithDigest: ErrorWithDigest = (error: any); + const chunks = response._chunks; + const chunk = chunks.get(id); + if (!chunk) { + chunks.set(id, createErrorChunk(response, errorWithDigest)); + } else { + triggerErrorOnChunk(chunk, errorWithDigest); } return; } @@ -2642,11 +2640,22 @@ function processFullStringRow( } case 68 /* "D" */: { if (__DEV__) { - const debugInfo: ReactComponentInfo | ReactAsyncInfo = parseModel( - response, - row, - ); - resolveDebugInfo(response, id, debugInfo); + const chunk: ResolvedModelChunk = + createResolvedModelChunk(response, row); + initializeModelChunk(chunk); + const initializedChunk: SomeChunk = + chunk; + if (initializedChunk.status === INITIALIZED) { + resolveDebugInfo(response, id, initializedChunk.value); + } else { + // TODO: This is not going to resolve in the right order if there's more than one. + chunk.then( + v => resolveDebugInfo(response, id, v), + e => { + // Ignore debug info errors for now. Unnecessary noise. + }, + ); + } return; } // Fallthrough to share the error with Console entries. diff --git a/packages/react-client/src/__tests__/ReactFlight-test.js b/packages/react-client/src/__tests__/ReactFlight-test.js index 34986dc623..857ce99868 100644 --- a/packages/react-client/src/__tests__/ReactFlight-test.js +++ b/packages/react-client/src/__tests__/ReactFlight-test.js @@ -308,6 +308,10 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + firstName: 'Seb', + lastName: 'Smith', + }, }, ] : undefined, @@ -347,6 +351,10 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + firstName: 'Seb', + lastName: 'Smith', + }, }, ] : undefined, @@ -653,6 +661,46 @@ describe('ReactFlight', () => { `); }); + it('can transport Error objects as values', async () => { + function ComponentClient({prop}) { + return ` + is error: ${prop instanceof Error} + message: ${prop.message} + stack: ${normalizeCodeLocInfo(prop.stack).split('\n').slice(0, 2).join('\n')} + environmentName: ${prop.environmentName} + `; + } + const Component = clientReference(ComponentClient); + + function ServerComponent() { + const error = new Error('hello'); + return ; + } + + const transport = ReactNoopFlightServer.render(); + + await act(async () => { + ReactNoop.render(await ReactNoopFlightClient.read(transport)); + }); + + if (__DEV__) { + expect(ReactNoop).toMatchRenderedOutput(` + is error: true + message: hello + stack: Error: hello + in ServerComponent (at **) + environmentName: Server + `); + } else { + expect(ReactNoop).toMatchRenderedOutput(` + is error: true + message: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error. + stack: Error: An error occurred in the Server Components render. The specific message is omitted in production builds to avoid leaking sensitive details. A digest property is included on this error instance which may provide additional details about the nature of the error. + environmentName: undefined + `); + } + }); + it('can transport cyclic objects', async () => { function ComponentClient({prop}) { expect(prop.obj.obj.obj).toBe(prop.obj.obj); @@ -2625,6 +2673,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + transport: expect.arrayContaining([]), + }, }, ] : undefined, @@ -2643,6 +2694,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2658,6 +2710,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in myLazy (at **)\n in lazyInitializer (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2673,6 +2726,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2747,6 +2801,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + transport: expect.arrayContaining([]), + }, }, ] : undefined, @@ -2764,6 +2821,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in ServerComponent (at **)' : undefined, + props: { + children: {}, + }, }, ] : undefined, @@ -2780,6 +2840,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, ] : undefined, @@ -2938,6 +2999,7 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: {}, }, { env: 'B', @@ -3068,6 +3130,9 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Object. (at **)' : undefined, + props: { + firstName: 'Seb', + }, }; expect(getDebugInfo(greeting)).toEqual([ greetInfo, @@ -3079,6 +3144,14 @@ describe('ReactFlight', () => { stack: gate(flag => flag.enableOwnerStacks) ? ' in Greeting (at **)' : undefined, + props: { + children: expect.objectContaining({ + type: 'span', + props: { + children: ['Hello, ', 'Seb'], + }, + }), + }, }, ]); // The owner that created the span was the outer server component. diff --git a/packages/react-devtools-core/src/standalone.js b/packages/react-devtools-core/src/standalone.js index bfd05cf522..d65b41b478 100644 --- a/packages/react-devtools-core/src/standalone.js +++ b/packages/react-devtools-core/src/standalone.js @@ -17,7 +17,6 @@ import {registerDevToolsEventLogger} from 'react-devtools-shared/src/registerDev import {Server} from 'ws'; import {join} from 'path'; import {readFileSync} from 'fs'; -import {installHook} from 'react-devtools-shared/src/hook'; import DevTools from 'react-devtools-shared/src/devtools/views/DevTools'; import {doesFilePathExist, launchEditor} from './editor'; import { @@ -29,8 +28,6 @@ import {localStorageSetItem} from 'react-devtools-shared/src/storage'; import type {FrontendBridge} from 'react-devtools-shared/src/bridge'; import type {Source} from 'react-devtools-shared/src/shared/types'; -installHook(window); - export type StatusTypes = 'server-connected' | 'devtools-connected' | 'error'; export type StatusListener = (message: string, status: StatusTypes) => void; export type OnDisconnectedCallback = () => void; @@ -371,9 +368,12 @@ function startServer( '\n;' + backendFile.toString() + '\n;' + + 'ReactDevToolsBackend.initialize();' + + '\n' + `ReactDevToolsBackend.connectToDevTools({port: ${port}, host: '${host}', useHttps: ${ useHttps ? 'true' : 'false' - }});`, + }}); + `, ); }); diff --git a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js index 0d6d8d02a1..fb1fa6c5f2 100644 --- a/packages/react-devtools-shared/src/__tests__/profilingCache-test.js +++ b/packages/react-devtools-shared/src/__tests__/profilingCache-test.js @@ -13,7 +13,6 @@ import type Store from 'react-devtools-shared/src/devtools/store'; import {getVersionedRenderImplementation} from './utils'; describe('ProfilingCache', () => { - let PropTypes; let React; let ReactDOM; let ReactDOMClient; @@ -34,7 +33,6 @@ describe('ProfilingCache', () => { store.collapseNodesByDefault = false; store.recordChangeDescriptions = true; - PropTypes = require('prop-types'); React = require('react'); ReactDOM = require('react-dom'); ReactDOMClient = require('react-dom/client'); @@ -309,692 +307,6 @@ describe('ProfilingCache', () => { }); }); - // @reactVersion >= 16.9 - // @reactVersion <= 18.2 - it('should record changed props/state/context/hooks for React version [16.9; 18.2] with legacy context', () => { - let instance = null; - - const ModernContext = React.createContext(0); - - class LegacyContextProvider extends React.Component { - static childContextTypes = { - count: PropTypes.number, - }; - state = {count: 0}; - getChildContext() { - return this.state; - } - render() { - instance = this; - return ( - - - - - - - ); - } - } - - const FunctionComponentWithHooks = ({count}) => { - React.useMemo(() => count, [count]); - return null; - }; - - class ModernContextConsumer extends React.Component { - static contextType = ModernContext; - render() { - return ; - } - } - - class LegacyContextConsumer extends React.Component { - static contextTypes = { - count: PropTypes.number, - }; - render() { - return ; - } - } - - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => render()); - expect(instance).not.toBeNull(); - utils.act(() => (instance: any).setState({count: 1})); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => store.profilerStore.stopProfiling()); - - const rootID = store.roots[0]; - - let changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - expect(changeDescriptions[0]).toMatchInlineSnapshot(` - Map { - 2 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 4 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 5 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 6 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "isFirstMount": true, - "props": null, - "state": null, - }, - } - `); - expect(changeDescriptions[1]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "count", - ], - "state": null, - }, - 4 => { - "context": true, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "count", - ], - "state": null, - }, - 6 => { - "context": [ - "count", - ], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": [ - "count", - ], - }, - } - `); - expect(changeDescriptions[2]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 4 => { - "context": false, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 6 => { - "context": [], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "foo", - ], - "state": [], - }, - } - `); - expect(changeDescriptions[3]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 4 => { - "context": false, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 6 => { - "context": [], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "foo", - "bar", - ], - "state": [], - }, - } - `); - expect(changeDescriptions[4]).toMatchInlineSnapshot(` - Map { - 5 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 4 => { - "context": false, - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 7 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [], - "state": null, - }, - 6 => { - "context": [], - "didHooksChange": false, - "hooks": null, - "isFirstMount": false, - "props": [], - "state": null, - }, - 2 => { - "context": null, - "didHooksChange": false, - "hooks": [], - "isFirstMount": false, - "props": [ - "bar", - ], - "state": [], - }, - } - `); - - utils.exportImportHelper(bridge, store); - - const prevChangeDescriptions = [...changeDescriptions]; - - changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - - for (let commitIndex = 0; commitIndex < 5; commitIndex++) { - expect(changeDescriptions[commitIndex]).toEqual( - prevChangeDescriptions[commitIndex], - ); - } - }); - - // @reactVersion > 18.2 - // @gate !disableLegacyContext - it('should record changed props/state/context/hooks for React version (18.2; ∞) with legacy context enabled', () => { - let instance = null; - - const ModernContext = React.createContext(0); - - class LegacyContextProvider extends React.Component { - static childContextTypes = { - count: PropTypes.number, - }; - state = {count: 0}; - getChildContext() { - return this.state; - } - render() { - instance = this; - return ( - - - - - - - ); - } - } - - const FunctionComponentWithHooks = ({count}) => { - React.useMemo(() => count, [count]); - return null; - }; - - class ModernContextConsumer extends React.Component { - static contextType = ModernContext; - render() { - return ; - } - } - - class LegacyContextConsumer extends React.Component { - static contextTypes = { - count: PropTypes.number, - }; - render() { - return ; - } - } - - utils.act(() => store.profilerStore.startProfiling()); - utils.act(() => render()); - expect(instance).not.toBeNull(); - utils.act(() => (instance: any).setState({count: 1})); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => render()); - utils.act(() => store.profilerStore.stopProfiling()); - - const rootID = store.roots[0]; - - let changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - expect(changeDescriptions[0]).toEqual( - new Map([ - [ - 2, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 4, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 5, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 6, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - isFirstMount: true, - props: null, - state: null, - }, - ], - ]), - ); - - expect(changeDescriptions[1]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['count'], - state: null, - }, - ], - [ - 4, - { - context: true, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['count'], - state: null, - }, - ], - [ - 6, - { - context: ['count'], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: ['count'], - }, - ], - ]), - ); - - expect(changeDescriptions[2]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 4, - { - context: false, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 6, - { - context: [], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['foo'], - state: [], - }, - ], - ]), - ); - - expect(changeDescriptions[3]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 4, - { - context: false, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 6, - { - context: [], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['foo', 'bar'], - state: [], - }, - ], - ]), - ); - - expect(changeDescriptions[4]).toEqual( - new Map([ - [ - 5, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 4, - { - context: false, - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 7, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 6, - { - context: [], - didHooksChange: false, - hooks: null, - isFirstMount: false, - props: [], - state: null, - }, - ], - [ - 2, - { - context: null, - didHooksChange: false, - hooks: [], - isFirstMount: false, - props: ['bar'], - state: [], - }, - ], - ]), - ); - - utils.exportImportHelper(bridge, store); - - const prevChangeDescriptions = [...changeDescriptions]; - - changeDescriptions = store.profilerStore - .getDataForRoot(rootID) - .commitData.map(commitData => commitData.changeDescriptions); - expect(changeDescriptions).toHaveLength(5); - - for (let commitIndex = 0; commitIndex < 5; commitIndex++) { - expect(changeDescriptions[commitIndex]).toEqual( - prevChangeDescriptions[commitIndex], - ); - } - }); - // @reactVersion >= 18.0 it('should properly detect changed hooks', () => { const Context = React.createContext(0); diff --git a/packages/react-devtools-shared/src/__tests__/setupTests.js b/packages/react-devtools-shared/src/__tests__/setupTests.js index d4afd05899..0a821345fc 100644 --- a/packages/react-devtools-shared/src/__tests__/setupTests.js +++ b/packages/react-devtools-shared/src/__tests__/setupTests.js @@ -150,7 +150,10 @@ function patchConsoleForTestingBeforeHookInstallation() { firstArg.startsWith( 'The current testing environment is not configured to support act', ) || - firstArg.startsWith('You seem to have overlapping act() calls')) + firstArg.startsWith('You seem to have overlapping act() calls') || + firstArg.startsWith( + 'ReactDOM.render is no longer supported in React 18.', + )) ) { // DevTools intentionally wraps updates with acts from both DOM and test-renderer, // since test updates are expected to impact both renderers. diff --git a/packages/react-devtools-shared/src/backend/fiber/renderer.js b/packages/react-devtools-shared/src/backend/fiber/renderer.js index 4fac8839ae..9732bf105c 100644 --- a/packages/react-devtools-shared/src/backend/fiber/renderer.js +++ b/packages/react-devtools-shared/src/backend/fiber/renderer.js @@ -4348,8 +4348,7 @@ export function attach( const componentInfo = virtualInstance.data; const key = typeof componentInfo.key === 'string' ? componentInfo.key : null; - const props = null; // TODO: Track props on ReactComponentInfo; - + const props = componentInfo.props == null ? null : componentInfo.props; const owners: null | Array = getOwnersListFromInstance(virtualInstance); diff --git a/packages/react-devtools-shared/src/hydration.js b/packages/react-devtools-shared/src/hydration.js index c5b78135e7..c21efe40a8 100644 --- a/packages/react-devtools-shared/src/hydration.js +++ b/packages/react-devtools-shared/src/hydration.js @@ -216,16 +216,19 @@ export function dehydrate( if (level >= LEVEL_THRESHOLD && !isPathAllowedCheck) { return createDehydrated(type, true, data, cleaned, path); } - return data.map((item, i) => - dehydrate( - item, + const arr: Array = []; + for (let i = 0; i < data.length; i++) { + arr[i] = dehydrateKey( + data, + i, cleaned, unserializable, path.concat([i]), isPathAllowed, isPathAllowedCheck ? 1 : level + 1, - ), - ); + ); + } + return arr; case 'html_all_collection': case 'typed_array': @@ -311,8 +314,9 @@ export function dehydrate( } = {}; getAllEnumerableKeys(data).forEach(key => { const name = key.toString(); - object[name] = dehydrate( - data[key], + object[name] = dehydrateKey( + data, + key, cleaned, unserializable, path.concat([name]), @@ -373,6 +377,46 @@ export function dehydrate( } } +function dehydrateKey( + parent: Object, + key: number | string | symbol, + cleaned: Array>, + unserializable: Array>, + path: Array, + isPathAllowed: (path: Array) => boolean, + level: number = 0, +): $PropertyType { + try { + return dehydrate( + parent[key], + cleaned, + unserializable, + path, + isPathAllowed, + level, + ); + } catch (error) { + let preview = ''; + if ( + typeof error === 'object' && + error !== null && + typeof error.stack === 'string' + ) { + preview = error.stack; + } else if (typeof error === 'string') { + preview = error; + } + cleaned.push(path); + return { + inspectable: false, + preview_short: '[Exception]', + preview_long: preview ? '[Exception: ' + preview + ']' : '[Exception]', + name: preview, + type: 'unknown', + }; + } +} + export function fillInPath( object: Object, data: DehydratedData, diff --git a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js index 57f2acfa53..26f56a938c 100644 --- a/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactLegacyUpdates-test.js @@ -1427,7 +1427,7 @@ describe('ReactLegacyUpdates', () => { } } - let limit = 105; + let limit = 55; await expect(async () => { await act(() => { ReactDOM.render(, container); diff --git a/packages/react-dom/src/__tests__/ReactUpdates-test.js b/packages/react-dom/src/__tests__/ReactUpdates-test.js index 247a53531c..faf4b29551 100644 --- a/packages/react-dom/src/__tests__/ReactUpdates-test.js +++ b/packages/react-dom/src/__tests__/ReactUpdates-test.js @@ -1542,7 +1542,7 @@ describe('ReactUpdates', () => { } } - let limit = 105; + let limit = 55; const root = ReactDOMClient.createRoot(container); await expect(async () => { await act(() => { diff --git a/packages/react-reconciler/src/ReactFiberWorkLoop.js b/packages/react-reconciler/src/ReactFiberWorkLoop.js index ec5484c4b6..196859f6fa 100644 --- a/packages/react-reconciler/src/ReactFiberWorkLoop.js +++ b/packages/react-reconciler/src/ReactFiberWorkLoop.js @@ -608,13 +608,13 @@ let pendingPassiveEffectsRenderEndTime: number = -0; // Profiling-only let pendingPassiveTransitions: Array | null = null; // Use these to prevent an infinite loop of nested updates -const NESTED_UPDATE_LIMIT = 100; +const NESTED_UPDATE_LIMIT = 50; let nestedUpdateCount: number = 0; let rootWithNestedUpdates: FiberRoot | null = null; let isFlushingPassiveEffects = false; let didScheduleUpdateDuringPassiveEffects = false; -const NESTED_PASSIVE_UPDATE_LIMIT = 100; +const NESTED_PASSIVE_UPDATE_LIMIT = 50; let nestedPassiveUpdateCount: number = 0; let rootWithPassiveNestedUpdates: FiberRoot | null = null; diff --git a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js index 0408a63fc5..882c0bbb01 100644 --- a/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js +++ b/packages/react-server-dom-webpack/src/__tests__/ReactFlightDOMBrowser-test.js @@ -709,7 +709,7 @@ describe('ReactFlightDOMBrowser', () => { expect(container.innerHTML).toBe(expectedHtml); if (__DEV__) { - const resolvedPath1b = await response.value[0].props.children[1]._payload; + const resolvedPath1b = response.value[0].props.children[1]; expect(resolvedPath1b._owner).toEqual( expect.objectContaining({ @@ -1028,8 +1028,10 @@ describe('ReactFlightDOMBrowser', () => { expect(flightResponse).toContain('(loading everything)'); expect(flightResponse).toContain('(loading sidebar)'); expect(flightResponse).toContain('(loading posts)'); - expect(flightResponse).not.toContain(':friends:'); - expect(flightResponse).not.toContain(':name:'); + if (!__DEV__) { + expect(flightResponse).not.toContain(':friends:'); + expect(flightResponse).not.toContain(':name:'); + } await serverAct(() => { resolveFriends(); diff --git a/packages/react-server/src/ReactFlightServer.js b/packages/react-server/src/ReactFlightServer.js index 5d79482de0..5db03d6281 100644 --- a/packages/react-server/src/ReactFlightServer.js +++ b/packages/react-server/src/ReactFlightServer.js @@ -1148,14 +1148,20 @@ function renderFunctionComponent( ? null : filterStackTrace(request, task.debugStack, 1); // $FlowFixMe[cannot-write] + componentDebugInfo.props = props; + // $FlowFixMe[cannot-write] componentDebugInfo.debugStack = task.debugStack; // $FlowFixMe[cannot-write] componentDebugInfo.debugTask = task.debugTask; + } else { + // $FlowFixMe[cannot-write] + componentDebugInfo.props = props; } // We outline this model eagerly so that we can refer to by reference as an owner. // If we had a smarter way to dedupe we might not have to do this if there ends up // being no references to this as an owner. - outlineModel(request, componentDebugInfo); + + outlineComponentInfo(request, componentDebugInfo); emitDebugChunk(request, componentDebugID, componentDebugInfo); // We've emitted the latest environment for this task so we track that. @@ -1582,6 +1588,13 @@ function renderClientElement( } else if (keyPath !== null) { key = keyPath + ',' + key; } + if (__DEV__) { + if (task.debugOwner !== null) { + // Ensure we outline this owner if it is the first time we see it. + // So that we can refer to it directly. + outlineComponentInfo(request, task.debugOwner); + } + } const element = __DEV__ ? enableOwnerStacks ? [ @@ -1702,6 +1715,7 @@ function renderElement( task.debugStack === null ? null : filterStackTrace(request, task.debugStack, 1), + props: props, debugStack: task.debugStack, debugTask: task.debugTask, }; @@ -2128,7 +2142,7 @@ function serializeSet(request: Request, set: Set): string { function serializeConsoleMap( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, map: Map, ): string { // Like serializeMap but for renderConsoleValue. @@ -2139,7 +2153,7 @@ function serializeConsoleMap( function serializeConsoleSet( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, set: Set, ): string { // Like serializeMap but for renderConsoleValue. @@ -2263,23 +2277,6 @@ function escapeStringValue(value: string): string { } } -function isReactComponentInfo(value: any): boolean { - // TODO: We don't currently have a brand check on ReactComponentInfo. Reconsider. - return ( - ((typeof value.debugTask === 'object' && - value.debugTask !== null && - // $FlowFixMe[method-unbinding] - typeof value.debugTask.run === 'function') || - value.debugStack instanceof Error) && - (enableOwnerStacks - ? isArray((value: any).stack) || (value: any).stack === null - : typeof (value: any).stack === 'undefined') && - typeof value.name === 'string' && - typeof value.env === 'string' && - value.owner !== undefined - ); -} - let modelRoot: null | ReactClientValue = false; function renderModel( @@ -2688,6 +2685,9 @@ function renderModelDestructive( if (typeof FormData === 'function' && value instanceof FormData) { return serializeFormData(request, value); } + if (value instanceof Error) { + return serializeErrorValue(request, value); + } if (enableBinaryFlight) { if (value instanceof ArrayBuffer) { @@ -2792,25 +2792,6 @@ function renderModelDestructive( ); } if (__DEV__) { - if (isReactComponentInfo(value)) { - // This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we - // need to omit it before serializing. - const componentDebugInfo: Omit< - ReactComponentInfo, - 'debugTask' | 'debugStack', - > = { - name: (value: any).name, - env: (value: any).env, - key: (value: any).key, - owner: (value: any).owner, - }; - if (enableOwnerStacks) { - // $FlowFixMe[cannot-write] - componentDebugInfo.stack = (value: any).stack; - } - return componentDebugInfo; - } - if (objectName(value) !== 'Object') { callWithDebugContextInDEV(request, task, () => { console.error( @@ -3114,6 +3095,36 @@ function emitPostponeChunk( request.completedErrorChunks.push(processedChunk); } +function serializeErrorValue(request: Request, error: Error): string { + if (__DEV__) { + let message; + let stack: ReactStackTrace; + let env = (0, request.environmentName)(); + try { + // eslint-disable-next-line react-internal/safe-string-coercion + message = String(error.message); + stack = filterStackTrace(request, error, 0); + const errorEnv = (error: any).environmentName; + if (typeof errorEnv === 'string') { + // This probably came from another FlightClient as a pass through. + // Keep the environment name. + env = errorEnv; + } + } catch (x) { + message = 'An error occurred but serializing the error message failed.'; + stack = []; + } + const errorInfo = {message, stack, env}; + const id = outlineModel(request, errorInfo); + return '$Z' + id.toString(16); + } else { + // In prod we don't emit any information about this Error object to avoid + // unintentional leaks. Since this doesn't actually throw on the server + // we don't go through onError and so don't register any digest neither. + return '$Z'; + } +} + function emitErrorChunk( request: Request, id: number, @@ -3208,7 +3219,7 @@ function emitDebugChunk( // We use the console encoding so that we can dedupe objects but don't necessarily // use the full serialization that requires a task. - const counter = {objectCount: 0}; + const counter = {objectLimit: 500}; function replacer( this: | {+[key: string | number]: ReactClientValue} @@ -3232,6 +3243,61 @@ function emitDebugChunk( request.completedRegularChunks.push(processedChunk); } +function outlineComponentInfo( + request: Request, + componentInfo: ReactComponentInfo, +): void { + if (!__DEV__) { + // These errors should never make it into a build so we don't need to encode them in codes.json + // eslint-disable-next-line react-internal/prod-error-codes + throw new Error( + 'outlineComponentInfo should never be called in production mode. This is a bug in React.', + ); + } + + if (request.writtenObjects.has(componentInfo)) { + // Already written + return; + } + + if (componentInfo.owner != null) { + // Ensure the owner is already outlined. + outlineComponentInfo(request, componentInfo.owner); + } + + // Limit the number of objects we write to prevent emitting giant props objects. + let objectLimit = 10; + if (componentInfo.stack != null) { + // Ensure we have enough object limit to encode the stack trace. + objectLimit += componentInfo.stack.length; + } + + // We use the console encoding so that we can dedupe objects but don't necessarily + // use the full serialization that requires a task. + const counter = {objectLimit}; + + // We can't serialize the ConsoleTask/Error objects so we need to omit them before serializing. + const componentDebugInfo: Omit< + ReactComponentInfo, + 'debugTask' | 'debugStack', + > = { + name: componentInfo.name, + env: componentInfo.env, + key: componentInfo.key, + owner: componentInfo.owner, + }; + if (enableOwnerStacks) { + // $FlowFixMe[cannot-write] + componentDebugInfo.stack = componentInfo.stack; + } + // Ensure we serialize props after the stack to favor the stack being complete. + // $FlowFixMe[cannot-write] + componentDebugInfo.props = componentInfo.props; + + const id = outlineConsoleValue(request, counter, componentDebugInfo); + request.writtenObjects.set(componentInfo, serializeByValueID(id)); +} + function emitTypedArrayChunk( request: Request, id: number, @@ -3289,7 +3355,7 @@ function serializeEval(source: string): string { // in the depth it can encode. function renderConsoleValue( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, parent: | {+[propertyName: string | number]: ReactClientValue} | $ReadOnlyArray, @@ -3333,23 +3399,64 @@ function renderConsoleValue( } } - if (counter.objectCount > 500) { + const writtenObjects = request.writtenObjects; + const existingReference = writtenObjects.get(value); + if (existingReference !== undefined) { + // We've already emitted this as a real object, so we can + // just refer to that by its existing reference. + return existingReference; + } + + if (counter.objectLimit <= 0) { // We've reached our max number of objects to serialize across the wire so we serialize this // as a marker so that the client can error when this is accessed by the console. return serializeLimitedObject(); } - counter.objectCount++; + counter.objectLimit--; + + switch ((value: any).$$typeof) { + case REACT_ELEMENT_TYPE: { + const element: ReactElement = (value: any); + + if (element._owner != null) { + outlineComponentInfo(request, element._owner); + } + if (enableOwnerStacks) { + let debugStack: null | ReactStackTrace = null; + if (element._debugStack != null) { + // Outline the debug stack so that it doesn't get cut off. + debugStack = filterStackTrace(request, element._debugStack, 1); + const stackId = outlineConsoleValue( + request, + {objectLimit: debugStack.length + 2}, + debugStack, + ); + request.writtenObjects.set(debugStack, serializeByValueID(stackId)); + } + return [ + REACT_ELEMENT_TYPE, + element.type, + element.key, + element.props, + element._owner, + debugStack, + element._store.validated, + ]; + } + + return [ + REACT_ELEMENT_TYPE, + element.type, + element.key, + element.props, + element._owner, + ]; + } + } - const writtenObjects = request.writtenObjects; - const existingReference = writtenObjects.get(value); // $FlowFixMe[method-unbinding] if (typeof value.then === 'function') { - if (existingReference !== undefined) { - // We've seen this promise before, so we can just refer to the same result. - return existingReference; - } - const thenable: Thenable = (value: any); switch (thenable.status) { case 'fulfilled': { @@ -3383,12 +3490,6 @@ function renderConsoleValue( return serializeInfinitePromise(); } - if (existingReference !== undefined) { - // We've already emitted this as a real object, so we can - // just refer to that by its existing reference. - return existingReference; - } - if (isArray(value)) { return value; } @@ -3403,6 +3504,9 @@ function renderConsoleValue( if (typeof FormData === 'function' && value instanceof FormData) { return serializeFormData(request, value); } + if (value instanceof Error) { + return serializeErrorValue(request, value); + } if (enableBinaryFlight) { if (value instanceof ArrayBuffer) { @@ -3467,25 +3571,6 @@ function renderConsoleValue( return Array.from((value: any)); } - if (isReactComponentInfo(value)) { - // This looks like a ReactComponentInfo. We can't serialize the ConsoleTask object so we - // need to omit it before serializing. - const componentDebugInfo: Omit< - ReactComponentInfo, - 'debugTask' | 'debugStack', - > = { - name: (value: any).name, - env: (value: any).env, - key: (value: any).key, - owner: (value: any).owner, - }; - if (enableOwnerStacks) { - // $FlowFixMe[cannot-write] - componentDebugInfo.stack = (value: any).stack; - } - return componentDebugInfo; - } - // $FlowFixMe[incompatible-return] return value; } @@ -3566,7 +3651,7 @@ function renderConsoleValue( function outlineConsoleValue( request: Request, - counter: {objectCount: number}, + counter: {objectLimit: number}, model: ReactClientValue, ): number { if (!__DEV__) { @@ -3593,7 +3678,9 @@ function outlineConsoleValue( value, ); } catch (x) { - return 'unknown value'; + return ( + 'Unknown Value: React could not send it from the server.\n' + x.message + ); } } @@ -3624,7 +3711,7 @@ function emitConsoleChunk( ); } - const counter = {objectCount: 0}; + const counter = {objectLimit: 500}; function replacer( this: | {+[key: string | number]: ReactClientValue} @@ -3641,10 +3728,17 @@ function emitConsoleChunk( value, ); } catch (x) { - return 'unknown value'; + return ( + 'Unknown Value: React could not send it from the server.\n' + x.message + ); } } + // Ensure the owner is already outlined. + if (owner != null) { + outlineComponentInfo(request, owner); + } + // TODO: Don't double badge if this log came from another Flight Client. const env = (0, request.environmentName)(); const payload = [methodName, stackTrace, owner, env]; @@ -3668,7 +3762,7 @@ function forwardDebugInfo( // We outline this model eagerly so that we can refer to by reference as an owner. // If we had a smarter way to dedupe we might not have to do this if there ends up // being no references to this as an owner. - outlineModel(request, debugInfo[i]); + outlineComponentInfo(request, (debugInfo[i]: any)); } emitDebugChunk(request, id, debugInfo[i]); } diff --git a/packages/shared/ReactFeatureFlags.js b/packages/shared/ReactFeatureFlags.js index 9935784b53..cd9bfedaca 100644 --- a/packages/shared/ReactFeatureFlags.js +++ b/packages/shared/ReactFeatureFlags.js @@ -157,6 +157,12 @@ export const retryLaneExpirationMs = 5000; export const syncLaneExpirationMs = 250; export const transitionLaneExpirationMs = 5000; +/** + * Enables a new error detection for infinite render loops from updates caused + * by setState or similar outside of the component owning the state. + */ +export const enableInfiniteRenderLoopDetection = false; + // ----------------------------------------------------------------------------- // Ready for next major. // @@ -204,12 +210,6 @@ export const enableFilterEmptyStringAttributesDOM = true; // Disabled caching behavior of `react/cache` in client runtimes. export const disableClientCache = true; -/** - * Enables a new error detection for infinite render loops from updates caused - * by setState or similar outside of the component owning the state. - */ -export const enableInfiniteRenderLoopDetection = true; - // Subtle breaking changes to JSX runtime to make it faster, like passing `ref` // as a normal prop instead of stripping it from the props object. diff --git a/packages/shared/ReactTypes.js b/packages/shared/ReactTypes.js index 7d51fbe472..54eccd5538 100644 --- a/packages/shared/ReactTypes.js +++ b/packages/shared/ReactTypes.js @@ -193,6 +193,7 @@ export type ReactComponentInfo = { +key?: null | string, +owner?: null | ReactComponentInfo, +stack?: null | ReactStackTrace, + +props?: null | {[name: string]: mixed}, // Stashed Data for the Specific Execution Environment. Not part of the transport protocol +debugStack?: null | Error, +debugTask?: null | ConsoleTask, diff --git a/packages/shared/forks/ReactFeatureFlags.native-fb.js b/packages/shared/forks/ReactFeatureFlags.native-fb.js index 6997bf5f68..9dcc0f5d03 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.native-fb.js @@ -60,7 +60,7 @@ export const enableFizzExternalRuntime = true; export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = true; export const enableHalt = false; -export const enableInfiniteRenderLoopDetection = true; +export const enableInfiniteRenderLoopDetection = false; export const enableContextProfiling = false; export const enableLazyContextPropagation = true; export const enableLegacyCache = false; diff --git a/packages/shared/forks/ReactFeatureFlags.native-oss.js b/packages/shared/forks/ReactFeatureFlags.native-oss.js index 729cdef96d..741b44daf7 100644 --- a/packages/shared/forks/ReactFeatureFlags.native-oss.js +++ b/packages/shared/forks/ReactFeatureFlags.native-oss.js @@ -51,7 +51,7 @@ export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; export const enableHalt = false; export const enableHiddenSubtreeInsertionEffectCleanup = false; -export const enableInfiniteRenderLoopDetection = true; +export const enableInfiniteRenderLoopDetection = false; export const enableLazyContextPropagation = true; export const enableContextProfiling = false; export const enableLegacyCache = false; diff --git a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js index e0d946e54e..0cb1497edd 100644 --- a/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js +++ b/packages/shared/forks/ReactFeatureFlags.test-renderer.native-fb.js @@ -41,7 +41,7 @@ export const enableFizzExternalRuntime = true; export const enableFlightReadableStream = true; export const enableGetInspectorDataForInstanceInProduction = false; export const enableHalt = false; -export const enableInfiniteRenderLoopDetection = true; +export const enableInfiniteRenderLoopDetection = false; export const enableLazyContextPropagation = true; export const enableContextProfiling = false; export const enableHiddenSubtreeInsertionEffectCleanup = true;