From 228e066ab55da4b3347497e82fee3e573836bea9 Mon Sep 17 00:00:00 2001 From: Mofei Zhang Date: Wed, 30 Apr 2025 16:51:35 -0400 Subject: [PATCH] [compiler][bugfix] expand StoreContext to const / let / function variants ```js function Component() { useEffect(() => { let hasCleanedUp = false; document.addEventListener(..., () => hasCleanedUp ? foo() : bar()); // effect return values shouldn't be typed as frozen return () => { hasCleanedUp = true; } }; } ``` ### Problem `PruneHoistedContexts` currently strips hoisted declarations and rewrites the first `StoreContext` reassignment to a declaration. For example, in the following example, instruction 0 is removed while a synthetic `DeclareContext let` is inserted before instruction 1. ```js // source const cb = () => x; // reference that causes x to be hoisted let x = 4; x = 5; // React Compiler IR [0] DeclareContext HoistedLet 'x' ... [1] StoreContext reassign 'x' = 4 [2] StoreContext reassign 'x' = 5 ``` Currently, we don't account for `DeclareContext let`. As a result, we're rewriting to insert duplicate declarations. ```js // source const cb = () => x; // reference that causes x to be hoisted let x; x = 5; // React Compiler IR [0] DeclareContext HoistedLet 'x' ... [1] DeclareContext Let 'x' [2] StoreContext reassign 'x' = 5 ``` ### Solution Instead of always lowering context variables to a DeclareContext followed by a StoreContext reassign, we can keep `kind: 'Const' | 'Let' | 'Reassign' | etc` on StoreContext. Pros: - retain more information in HIR, so we can codegen easily `const` and `let` context variable declarations back - pruning hoisted `DeclareContext` instructions is simple. Cons: - passes are more verbose as we need to check for both `DeclareContext` and `StoreContext` declarations ~(note: also see alternative implementation in https://github.com/facebook/react/pull/32745)~ ### Testing Context variables are tricky. I synced and diffed changes in a large meta codebase and feel pretty confident about landing this. About 0.01% of compiled files changed. Among these changes, ~25% were [direct bugfixes](https://www.internalfb.com/phabricator/paste/view/P1800029094). The [other changes](https://www.internalfb.com/phabricator/paste/view/P1800028575) were primarily due to changed (corrected) mutable ranges from https://github.com/facebook/react/pull/33047. I tried to represent most interesting changes in new test fixtures ` --- .../src/HIR/BuildHIR.ts | 53 ++++--- .../src/HIR/HIR.ts | 35 +++- .../src/HIR/PropagateScopeDependenciesHIR.ts | 70 +++++--- .../src/Inference/InferMutableLifetimes.ts | 10 +- .../src/Inference/InferReferenceEffects.ts | 17 +- .../ReactiveScopes/CodegenReactiveFunction.ts | 16 ++ .../ReactiveScopes/PruneHoistedContexts.ts | 149 +++++------------- ...ction-alias-computed-load-3-iife.expect.md | 3 +- ...g-function-alias-computed-load-3.expect.md | 3 +- .../codegen-inline-iife-reassign.expect.md | 3 +- ...ble-reassigned-outside-of-lambda.expect.md | 3 +- ...p-with-context-variable-iterator.expect.md | 67 ++++---- ...for-loop-with-context-variable-iterator.js | 17 +- ...-context-variable-in-outlined-fn.expect.md | 82 ++++++++++ ...hoisted-context-variable-in-outlined-fn.js | 25 +++ .../hoisting-invalid-tdz-let.expect.md | 3 +- ...claration-without-initialization.expect.md | 64 ++++++++ ...-let-declaration-without-initialization.js | 18 +++ ...oisting-nested-let-declaration-2.expect.md | 3 +- .../hoisting-nested-let-declaration.expect.md | 6 +- ...sting-reassigned-let-declaration.expect.md | 3 +- ...reassigned-twice-let-declaration.expect.md | 3 +- .../hoisting-simple-let-declaration.expect.md | 6 +- .../jsx-captures-context-variable.expect.md | 129 +++++++++++++++ .../compiler/jsx-captures-context-variable.js | 40 +++++ ...mbda-reassign-shadowed-primitive.expect.md | 3 +- .../mutate-captured-arg-separately.expect.md | 3 +- ...llback-extended-contextvar-scope.expect.md | 3 +- ...k-reordering-deplist-controlflow.expect.md | 3 +- ...-reordering-depslist-controlflow.expect.md | 3 +- ...ro-context-var-reassign-no-scope.expect.md | 108 +++++++++++++ .../repro-context-var-reassign-no-scope.js | 31 ++++ ...urned-inner-fn-reassigns-context.expect.md | 3 +- .../use-effect-cleanup-reassigns.expect.md | 122 ++++++++++++++ .../compiler/use-effect-cleanup-reassigns.js | 38 +++++ .../use-operator-conditional.expect.md | 3 +- 36 files changed, 916 insertions(+), 232 deletions(-) create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.js create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.expect.md create mode 100644 compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.js diff --git a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts index cba4bf93edb65..b9f82eea18e9f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/BuildHIR.ts @@ -3609,31 +3609,40 @@ function lowerAssignment( let temporary; if (builder.isContextIdentifier(lvalue)) { - if (kind !== InstructionKind.Reassign && !isHoistedIdentifier) { - if (kind === InstructionKind.Const) { - builder.errors.push({ - reason: `Expected \`const\` declaration not to be reassigned`, - severity: ErrorSeverity.InvalidJS, - loc: lvalue.node.loc ?? null, - suggestions: null, - }); - } - lowerValueToTemporary(builder, { - kind: 'DeclareContext', - lvalue: { - kind: InstructionKind.Let, - place: {...place}, - }, - loc: place.loc, + if (kind === InstructionKind.Const && !isHoistedIdentifier) { + builder.errors.push({ + reason: `Expected \`const\` declaration not to be reassigned`, + severity: ErrorSeverity.InvalidJS, + loc: lvalue.node.loc ?? null, + suggestions: null, }); } - temporary = lowerValueToTemporary(builder, { - kind: 'StoreContext', - lvalue: {place: {...place}, kind: InstructionKind.Reassign}, - value, - loc, - }); + if ( + kind !== InstructionKind.Const && + kind !== InstructionKind.Reassign && + kind !== InstructionKind.Let && + kind !== InstructionKind.Function + ) { + builder.errors.push({ + reason: `Unexpected context variable kind`, + severity: ErrorSeverity.InvalidJS, + loc: lvalue.node.loc ?? null, + suggestions: null, + }); + temporary = lowerValueToTemporary(builder, { + kind: 'UnsupportedNode', + node: lvalueNode, + loc: lvalueNode.loc ?? GeneratedSource, + }); + } else { + temporary = lowerValueToTemporary(builder, { + kind: 'StoreContext', + lvalue: {place: {...place}, kind}, + value, + loc, + }); + } } else { const typeAnnotation = lvalue.get('typeAnnotation'); let type: t.FlowType | t.TSType | null; 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 ea58b3c49425e..611b5bd210226 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/HIR.ts @@ -746,6 +746,27 @@ export enum InstructionKind { Function = 'Function', } +export function convertHoistedLValueKind( + kind: InstructionKind, +): InstructionKind | null { + switch (kind) { + case InstructionKind.HoistedLet: + return InstructionKind.Let; + case InstructionKind.HoistedConst: + return InstructionKind.Const; + case InstructionKind.HoistedFunction: + return InstructionKind.Function; + case InstructionKind.Let: + case InstructionKind.Const: + case InstructionKind.Function: + case InstructionKind.Reassign: + case InstructionKind.Catch: + return null; + default: + assertExhaustive(kind, 'Unexpected lvalue kind'); + } +} + function _staticInvariantInstructionValueHasLocation( value: InstructionValue, ): SourceLocation { @@ -880,8 +901,20 @@ export type InstructionValue = | StoreLocal | { kind: 'StoreContext'; + /** + * StoreContext kinds: + * Reassign: context variable reassignment in source + * Const: const declaration + assignment in source + * ('const' context vars are ones whose declarations are hoisted) + * Let: let declaration + assignment in source + * Function: function declaration in source (similar to `const`) + */ lvalue: { - kind: InstructionKind.Reassign; + kind: + | InstructionKind.Reassign + | InstructionKind.Const + | InstructionKind.Let + | InstructionKind.Function; place: Place; }; value: Place; 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 3d183e8e72c68..96b9e51710887 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/HIR/PropagateScopeDependenciesHIR.ts @@ -30,6 +30,7 @@ import { FunctionExpression, ObjectMethod, PropertyLiteral, + convertHoistedLValueKind, } from './HIR'; import { collectHoistablePropertyLoads, @@ -246,12 +247,18 @@ function isLoadContextMutable( id: InstructionId, ): instrValue is LoadContext { if (instrValue.kind === 'LoadContext') { - CompilerError.invariant(instrValue.place.identifier.scope != null, { - reason: - '[PropagateScopeDependencies] Expected all context variables to be assigned a scope', - loc: instrValue.loc, - }); - return id >= instrValue.place.identifier.scope.range.end; + /** + * Not all context variables currently have scopes due to limitations of + * mutability analysis for function expressions. + * + * Currently, many function expressions references are inferred to be + * 'Read' | 'Freeze' effects which don't replay mutable effects of captured + * context. + */ + return ( + instrValue.place.identifier.scope != null && + id >= instrValue.place.identifier.scope.range.end + ); } return false; } @@ -471,6 +478,9 @@ export class DependencyCollectionContext { } this.#reassignments.set(identifier, decl); } + hasDeclared(identifier: Identifier): boolean { + return this.#declarations.has(identifier.declarationId); + } // Checks if identifier is a valid dependency in the current scope #checkValidDependency(maybeDependency: ReactiveScopeDependency): boolean { @@ -672,21 +682,21 @@ export function handleInstruction( }); } else if (value.kind === 'DeclareLocal' || value.kind === 'DeclareContext') { /* - * Some variables may be declared and never initialized. We need - * to retain (and hoist) these declarations if they are included - * in a reactive scope. One approach is to simply add all `DeclareLocal`s - * as scope declarations. + * Some variables may be declared and never initialized. We need to retain + * (and hoist) these declarations if they are included in a reactive scope. + * One approach is to simply add all `DeclareLocal`s as scope declarations. + * + * Context variables with hoisted declarations only become live after their + * first assignment. We only declare real DeclareLocal / DeclareContext + * instructions (not hoisted ones) to avoid generating dependencies on + * hoisted declarations. */ - - /* - * We add context variable declarations here, not at `StoreContext`, since - * context Store / Loads are modeled as reads and mutates to the underlying - * variable reference (instead of through intermediate / inlined temporaries) - */ - context.declare(value.lvalue.place.identifier, { - id, - scope: context.currentScope, - }); + if (convertHoistedLValueKind(value.lvalue.kind) === null) { + context.declare(value.lvalue.place.identifier, { + id, + scope: context.currentScope, + }); + } } else if (value.kind === 'Destructure') { context.visitOperand(value.value); for (const place of eachPatternOperand(value.lvalue.pattern)) { @@ -698,6 +708,26 @@ export function handleInstruction( scope: context.currentScope, }); } + } else if (value.kind === 'StoreContext') { + /** + * Some StoreContext variables have hoisted declarations. If we're storing + * to a context variable that hasn't yet been declared, the StoreContext is + * the declaration. + * (see corresponding logic in PruneHoistedContext) + */ + if ( + !context.hasDeclared(value.lvalue.place.identifier) || + value.lvalue.kind !== InstructionKind.Reassign + ) { + context.declare(value.lvalue.place.identifier, { + id, + scope: context.currentScope, + }); + } + + for (const operand of eachInstructionValueOperand(value)) { + context.visitOperand(operand); + } } else { for (const operand of eachInstructionValueOperand(value)) { context.visitOperand(operand); diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts index 45b5462efbe52..5057a7ac88ca3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferMutableLifetimes.ts @@ -176,9 +176,15 @@ export function inferMutableLifetimes( if ( instr.value.kind === 'DeclareContext' || (instr.value.kind === 'StoreContext' && - instr.value.lvalue.kind !== InstructionKind.Reassign) + instr.value.lvalue.kind !== InstructionKind.Reassign && + !contextVariableDeclarationInstructions.has( + instr.value.lvalue.place.identifier, + )) ) { - // Save declarations of context variables + /** + * Save declarations of context variables if they hasn't already been + * declared (due to hoisted declarations). + */ contextVariableDeclarationInstructions.set( instr.value.lvalue.place.identifier, instr.id, diff --git a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts index 724b67e5fa73e..d1546038edcbe 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/Inference/InferReferenceEffects.ts @@ -407,9 +407,14 @@ class InferenceState { freezeValues(values: Set, reason: Set): void { for (const value of values) { - if (value.kind === 'DeclareContext') { + if ( + value.kind === 'DeclareContext' || + (value.kind === 'StoreContext' && + (value.lvalue.kind === InstructionKind.Let || + value.lvalue.kind === InstructionKind.Const)) + ) { /** - * Avoid freezing hoisted context declarations + * Avoid freezing context variable declarations, hoisted or otherwise * function Component() { * const cb = useBar(() => foo(2)); // produces a hoisted context declaration * const foo = useFoo(); // reassigns to the context variable @@ -1606,6 +1611,14 @@ function inferBlock( ); const lvalue = instr.lvalue; + if (instrValue.lvalue.kind !== InstructionKind.Reassign) { + state.initialize(instrValue, { + kind: ValueKind.Mutable, + reason: new Set([ValueReason.Other]), + context: new Set(), + }); + state.define(instrValue.lvalue.place, instrValue); + } state.alias(lvalue, instrValue.value); lvalue.effect = Effect.Store; continuation = {kind: 'funeffects'}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts index 02994ef0f96b3..790a64b407316 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/CodegenReactiveFunction.ts @@ -1000,6 +1000,14 @@ function codegenTerminal( lval = codegenLValue(cx, iterableItem.value.lvalue.pattern); break; } + case 'StoreContext': { + CompilerError.throwTodo({ + reason: 'Support non-trivial for..in inits', + description: null, + loc: terminal.init.loc, + suggestions: null, + }); + } default: CompilerError.invariant(false, { reason: `Expected a StoreLocal or Destructure to be assigned to the collection`, @@ -1092,6 +1100,14 @@ function codegenTerminal( lval = codegenLValue(cx, iterableItem.value.lvalue.pattern); break; } + case 'StoreContext': { + CompilerError.throwTodo({ + reason: 'Support non-trivial for..of inits', + description: null, + loc: terminal.init.loc, + suggestions: null, + }); + } default: CompilerError.invariant(false, { reason: `Expected a StoreLocal or Destructure to be assigned to the collection`, diff --git a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts index b3754721cab37..66b1e1ce152b9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts +++ b/compiler/packages/babel-plugin-react-compiler/src/ReactiveScopes/PruneHoistedContexts.ts @@ -5,14 +5,16 @@ * LICENSE file in the root directory of this source tree. */ -import {CompilerError} from '..'; import { - DeclarationId, + convertHoistedLValueKind, + IdentifierId, InstructionKind, ReactiveFunction, ReactiveInstruction, + ReactiveScopeBlock, ReactiveStatement, } from '../HIR'; +import {empty, Stack} from '../Utils/Stack'; import { ReactiveFunctionTransform, Transformed, @@ -24,133 +26,54 @@ import { * original instruction kind. */ export function pruneHoistedContexts(fn: ReactiveFunction): void { - const hoistedIdentifiers: HoistedIdentifiers = new Map(); - visitReactiveFunction(fn, new Visitor(), hoistedIdentifiers); + visitReactiveFunction(fn, new Visitor(), { + activeScopes: empty(), + }); } -const REWRITTEN_HOISTED_CONST: unique symbol = Symbol( - 'REWRITTEN_HOISTED_CONST', -); -const REWRITTEN_HOISTED_LET: unique symbol = Symbol('REWRITTEN_HOISTED_LET'); +type VisitorState = { + activeScopes: Stack>; +}; -type HoistedIdentifiers = Map< - DeclarationId, - | InstructionKind - | typeof REWRITTEN_HOISTED_CONST - | typeof REWRITTEN_HOISTED_LET ->; - -class Visitor extends ReactiveFunctionTransform { +class Visitor extends ReactiveFunctionTransform { + override visitScope(scope: ReactiveScopeBlock, state: VisitorState): void { + state.activeScopes = state.activeScopes.push( + new Set(scope.scope.declarations.keys()), + ); + this.traverseScope(scope, state); + state.activeScopes.pop(); + } override transformInstruction( instruction: ReactiveInstruction, - state: HoistedIdentifiers, + state: VisitorState, ): Transformed { this.visitInstruction(instruction, state); /** * Remove hoisted declarations to preserve TDZ */ - if ( - instruction.value.kind === 'DeclareContext' && - instruction.value.lvalue.kind === 'HoistedConst' - ) { - state.set( - instruction.value.lvalue.place.identifier.declarationId, - InstructionKind.Const, - ); - return {kind: 'remove'}; - } - - if ( - instruction.value.kind === 'DeclareContext' && - instruction.value.lvalue.kind === 'HoistedLet' - ) { - state.set( - instruction.value.lvalue.place.identifier.declarationId, - InstructionKind.Let, + if (instruction.value.kind === 'DeclareContext') { + const maybeNonHoisted = convertHoistedLValueKind( + instruction.value.lvalue.kind, ); - return {kind: 'remove'}; + if (maybeNonHoisted != null) { + return {kind: 'remove'}; + } } - if ( - instruction.value.kind === 'DeclareContext' && - instruction.value.lvalue.kind === 'HoistedFunction' + instruction.value.kind === 'StoreContext' && + instruction.value.lvalue.kind !== InstructionKind.Reassign ) { - state.set( - instruction.value.lvalue.place.identifier.declarationId, - InstructionKind.Function, - ); - return {kind: 'remove'}; - } - - if (instruction.value.kind === 'StoreContext') { - const kind = state.get( - instruction.value.lvalue.place.identifier.declarationId, + /** + * Rewrite StoreContexts let/const/functions that will be pre-declared in + * codegen to reassignments. + */ + const lvalueId = instruction.value.lvalue.place.identifier.id; + const isDeclaredByScope = state.activeScopes.find(scope => + scope.has(lvalueId), ); - if (kind != null) { - CompilerError.invariant(kind !== REWRITTEN_HOISTED_CONST, { - reason: 'Expected exactly one store to a hoisted const variable', - loc: instruction.loc, - }); - if ( - kind === InstructionKind.Const || - kind === InstructionKind.Function - ) { - state.set( - instruction.value.lvalue.place.identifier.declarationId, - REWRITTEN_HOISTED_CONST, - ); - return { - kind: 'replace', - value: { - kind: 'instruction', - instruction: { - ...instruction, - value: { - ...instruction.value, - lvalue: { - ...instruction.value.lvalue, - kind, - }, - type: null, - kind: 'StoreLocal', - }, - }, - }, - }; - } else if (kind !== REWRITTEN_HOISTED_LET) { - /** - * Context variables declared with let may have reassignments. Only - * insert a `DeclareContext` for the first encountered `StoreContext` - * instruction. - */ - state.set( - instruction.value.lvalue.place.identifier.declarationId, - REWRITTEN_HOISTED_LET, - ); - return { - kind: 'replace-many', - value: [ - { - kind: 'instruction', - instruction: { - id: instruction.id, - lvalue: null, - value: { - kind: 'DeclareContext', - lvalue: { - kind: InstructionKind.Let, - place: {...instruction.value.lvalue.place}, - }, - loc: instruction.value.loc, - }, - loc: instruction.loc, - }, - }, - {kind: 'instruction', instruction}, - ], - }; - } + if (isDeclaredByScope) { + instruction.value.lvalue.kind = InstructionKind.Reassign; } } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md index 3e57b7dc7c255..f0267c3309f5b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3-iife.expect.md @@ -34,8 +34,7 @@ function bar(a, b) { if ($[0] !== a || $[1] !== b) { const x = [a, b]; y = {}; - let t; - t = {}; + let t = {}; y = x[0][1]; t = x[1][0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3.expect.md index e68445fbd71fe..238b748e34699 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/capturing-function-alias-computed-load-3.expect.md @@ -35,8 +35,7 @@ function bar(a, b) { if ($[0] !== a || $[1] !== b) { const x = [a, b]; y = {}; - let t; - t = {}; + let t = {}; const f0 = function () { y = x[0][1]; t = x[1][0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md index cf85967682607..9c8fc0f1c5b67 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/codegen-inline-iife-reassign.expect.md @@ -33,8 +33,7 @@ function useTest() { const $ = _c(1); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - let w; - w = {}; + let w = {}; const t1 = (w = 42); const t2 = w; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-outside-of-lambda.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-outside-of-lambda.expect.md index f7bd8d49cb784..2edb60a5a2b28 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-outside-of-lambda.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/context-variable-reassigned-outside-of-lambda.expect.md @@ -30,8 +30,7 @@ function Component(props) { const $ = _c(1); let t0; if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - let x; - x = null; + let x = null; const callback = () => { console.log(x); }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.expect.md index d92e1919625d6..db9ea1d5b628c 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.expect.md @@ -2,13 +2,22 @@ ## Input ```javascript +import {Stringify, useIdentity} from 'shared-runtime'; + function Component() { - const data = useData(); + const data = useIdentity( + new Map([ + [0, 'value0'], + [1, 'value1'], + ]) + ); const items = []; // NOTE: `i` is a context variable because it's reassigned and also referenced // within a closure, the `onClick` handler of each item for (let i = MIN; i <= MAX; i += INCREMENT) { - items.push(
data.set(i)} />); + items.push( + data.get(i)} shouldInvokeFns={true} /> + ); } return <>{items}; } @@ -17,10 +26,6 @@ const MIN = 0; const MAX = 3; const INCREMENT = 1; -function useData() { - return new Map(); -} - export const FIXTURE_ENTRYPOINT = { params: [], fn: Component, @@ -32,41 +37,47 @@ export const FIXTURE_ENTRYPOINT = { ```javascript import { c as _c } from "react/compiler-runtime"; +import { Stringify, useIdentity } from "shared-runtime"; + function Component() { - const $ = _c(2); - const data = useData(); + const $ = _c(3); let t0; - if ($[0] !== data) { + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = new Map([ + [0, "value0"], + [1, "value1"], + ]); + $[0] = t0; + } else { + t0 = $[0]; + } + const data = useIdentity(t0); + let t1; + if ($[1] !== data) { const items = []; for (let i = MIN; i <= MAX; i = i + INCREMENT, i) { - items.push(
data.set(i)} />); + items.push( + data.get(i)} + shouldInvokeFns={true} + />, + ); } - t0 = <>{items}; - $[0] = data; - $[1] = t0; + t1 = <>{items}; + $[1] = data; + $[2] = t1; } else { - t0 = $[1]; + t1 = $[2]; } - return t0; + return t1; } const MIN = 0; const MAX = 3; const INCREMENT = 1; -function useData() { - const $ = _c(1); - let t0; - if ($[0] === Symbol.for("react.memo_cache_sentinel")) { - t0 = new Map(); - $[0] = t0; - } else { - t0 = $[0]; - } - return t0; -} - export const FIXTURE_ENTRYPOINT = { params: [], fn: Component, @@ -75,4 +86,4 @@ export const FIXTURE_ENTRYPOINT = { ``` ### Eval output -(kind: ok)
\ No newline at end of file +(kind: ok)
{"onClick":{"kind":"Function","result":"value0"},"shouldInvokeFns":true}
{"onClick":{"kind":"Function","result":"value1"},"shouldInvokeFns":true}
{"onClick":{"kind":"Function"},"shouldInvokeFns":true}
{"onClick":{"kind":"Function"},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.js index 86e222e9e05cc..f7dbd76a6449e 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.js +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/for-loop-with-context-variable-iterator.js @@ -1,10 +1,19 @@ +import {Stringify, useIdentity} from 'shared-runtime'; + function Component() { - const data = useData(); + const data = useIdentity( + new Map([ + [0, 'value0'], + [1, 'value1'], + ]) + ); const items = []; // NOTE: `i` is a context variable because it's reassigned and also referenced // within a closure, the `onClick` handler of each item for (let i = MIN; i <= MAX; i += INCREMENT) { - items.push(
data.set(i)} />); + items.push( + data.get(i)} shouldInvokeFns={true} /> + ); } return <>{items}; } @@ -13,10 +22,6 @@ const MIN = 0; const MAX = 3; const INCREMENT = 1; -function useData() { - return new Map(); -} - export const FIXTURE_ENTRYPOINT = { params: [], fn: Component, diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.expect.md new file mode 100644 index 0000000000000..cbcb4486d92d6 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.expect.md @@ -0,0 +1,82 @@ + +## Input + +```javascript +import {CONST_TRUE, useIdentity} from 'shared-runtime'; + +const hidden = CONST_TRUE; +function useFoo() { + const makeCb = useIdentity(() => { + const logIntervalId = () => { + log(intervalId); + }; + + let intervalId; + if (!hidden) { + intervalId = 2; + } + return () => { + logIntervalId(); + }; + }); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { CONST_TRUE, useIdentity } from "shared-runtime"; + +const hidden = CONST_TRUE; +function useFoo() { + const $ = _c(4); + const makeCb = useIdentity(_temp); + let t0; + if ($[0] !== makeCb) { + t0 = makeCb(); + $[0] = makeCb; + $[1] = t0; + } else { + t0 = $[1]; + } + let t1; + if ($[2] !== t0) { + t1 = ; + $[2] = t0; + $[3] = t1; + } else { + t1 = $[3]; + } + return t1; +} +function _temp() { + const logIntervalId = () => { + log(intervalId); + }; + let intervalId; + if (!hidden) { + intervalId = 2; + } + return () => { + logIntervalId(); + }; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; + +``` + +### Eval output +(kind: exception) Stringify is not defined \ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.js new file mode 100644 index 0000000000000..a87a35ddba27d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisted-context-variable-in-outlined-fn.js @@ -0,0 +1,25 @@ +import {CONST_TRUE, useIdentity} from 'shared-runtime'; + +const hidden = CONST_TRUE; +function useFoo() { + const makeCb = useIdentity(() => { + const logIntervalId = () => { + log(intervalId); + }; + + let intervalId; + if (!hidden) { + intervalId = 2; + } + return () => { + logIntervalId(); + }; + }); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: useFoo, + params: [], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-invalid-tdz-let.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-invalid-tdz-let.expect.md index 2c2b559f02622..4951aaa9f3011 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-invalid-tdz-let.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-invalid-tdz-let.expect.md @@ -30,8 +30,7 @@ function Foo() { getX = () => x; console.log(getX()); - let x; - x = 4; + let x = 4; x = x + 5; $[0] = getX; } else { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.expect.md new file mode 100644 index 0000000000000..0e4b3b64e82d8 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.expect.md @@ -0,0 +1,64 @@ + +## Input + +```javascript +import {CONST_NUMBER1, Stringify} from 'shared-runtime'; + +function useHook({cond}) { + 'use memo'; + const getX = () => x; + + let x; + if (cond) { + x = CONST_NUMBER1; + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => {}, + params: [{cond: true}], + sequentialRenders: [{cond: true}, {cond: true}, {cond: false}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { CONST_NUMBER1, Stringify } from "shared-runtime"; + +function useHook(t0) { + "use memo"; + const $ = _c(2); + const { cond } = t0; + let t1; + if ($[0] !== cond) { + const getX = () => x; + + let x; + if (cond) { + x = CONST_NUMBER1; + } + + t1 = ; + $[0] = cond; + $[1] = t1; + } else { + t1 = $[1]; + } + return t1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => {}, + params: [{ cond: true }], + sequentialRenders: [{ cond: true }, { cond: true }, { cond: false }], +}; + +``` + +### Eval output +(kind: ok) + diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.js new file mode 100644 index 0000000000000..cc4131951564d --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-let-declaration-without-initialization.js @@ -0,0 +1,18 @@ +import {CONST_NUMBER1, Stringify} from 'shared-runtime'; + +function useHook({cond}) { + 'use memo'; + const getX = () => x; + + let x; + if (cond) { + x = CONST_NUMBER1; + } + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: () => {}, + params: [{cond: true}], + sequentialRenders: [{cond: true}, {cond: true}, {cond: false}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration-2.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration-2.expect.md index aa6ee80d69bf1..fe0c6618f51ad 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration-2.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration-2.expect.md @@ -36,8 +36,7 @@ function hoisting(cond) { items.push(bar()); }; - let bar; - bar = _temp; + let bar = _temp; foo(); } $[0] = cond; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration.expect.md index 25205ca02372c..faf7a064d748f 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-nested-let-declaration.expect.md @@ -41,11 +41,9 @@ function hoisting() { return result; }; - let foo; - foo = () => bar + baz; + let foo = () => bar + baz; - let bar; - bar = 3; + let bar = 3; const baz = 2; t0 = qux(); $[0] = t0; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-let-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-let-declaration.expect.md index 3f7b16cf23697..c4a969c8884c6 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-let-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-let-declaration.expect.md @@ -37,8 +37,7 @@ function useHook(t0) { if ($[0] !== cond) { const getX = () => x; - let x; - x = CONST_NUMBER0; + let x = CONST_NUMBER0; if (cond) { x = x + CONST_NUMBER1; x; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-twice-let-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-twice-let-declaration.expect.md index 54d49b9282cdf..cdeb9c60aabc5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-twice-let-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-reassigned-twice-let-declaration.expect.md @@ -38,8 +38,7 @@ function useHook(t0) { if ($[0] !== cond) { const getX = () => x; - let x; - x = CONST_NUMBER0; + let x = CONST_NUMBER0; if (cond) { x = x + CONST_NUMBER1; x; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md index 958f2c7afdf8f..8d694a984aed5 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/hoisting-simple-let-declaration.expect.md @@ -29,10 +29,8 @@ function hoisting() { if ($[0] === Symbol.for("react.memo_cache_sentinel")) { foo = () => bar + baz; - let bar; - bar = 3; - let baz; - baz = 2; + let bar = 3; + let baz = 2; $[0] = foo; } else { foo = $[0]; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.expect.md new file mode 100644 index 0000000000000..c1a9ad205c87e --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.expect.md @@ -0,0 +1,129 @@ + +## Input + +```javascript +import {Stringify, useIdentity} from 'shared-runtime'; + +function Component({prop1, prop2}) { + 'use memo'; + + const data = useIdentity( + new Map([ + [0, 'value0'], + [1, 'value1'], + ]) + ); + let i = 0; + const items = []; + items.push( + data.get(i) + prop1} + shouldInvokeFns={true} + /> + ); + i = i + 1; + items.push( + data.get(i) + prop2} + shouldInvokeFns={true} + /> + ); + return <>{items}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop1: 'prop1', prop2: 'prop2'}], + sequentialRenders: [ + {prop1: 'prop1', prop2: 'prop2'}, + {prop1: 'prop1', prop2: 'prop2'}, + {prop1: 'changed', prop2: 'prop2'}, + ], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { Stringify, useIdentity } from "shared-runtime"; + +function Component(t0) { + "use memo"; + const $ = _c(12); + const { prop1, prop2 } = t0; + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = new Map([ + [0, "value0"], + [1, "value1"], + ]); + $[0] = t1; + } else { + t1 = $[0]; + } + const data = useIdentity(t1); + let t2; + if ($[1] !== data || $[2] !== prop1 || $[3] !== prop2) { + let i = 0; + const items = []; + items.push( + data.get(i) + prop1} + shouldInvokeFns={true} + />, + ); + i = i + 1; + + const t3 = i; + let t4; + if ($[5] !== data || $[6] !== i || $[7] !== prop2) { + t4 = () => data.get(i) + prop2; + $[5] = data; + $[6] = i; + $[7] = prop2; + $[8] = t4; + } else { + t4 = $[8]; + } + let t5; + if ($[9] !== t3 || $[10] !== t4) { + t5 = ; + $[9] = t3; + $[10] = t4; + $[11] = t5; + } else { + t5 = $[11]; + } + items.push(t5); + t2 = <>{items}; + $[1] = data; + $[2] = prop1; + $[3] = prop2; + $[4] = t2; + } else { + t2 = $[4]; + } + return t2; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ prop1: "prop1", prop2: "prop2" }], + sequentialRenders: [ + { prop1: "prop1", prop2: "prop2" }, + { prop1: "prop1", prop2: "prop2" }, + { prop1: "changed", prop2: "prop2" }, + ], +}; + +``` + +### Eval output +(kind: ok)
{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}
{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}
+
{"onClick":{"kind":"Function","result":"value1prop1"},"shouldInvokeFns":true}
{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}
+
{"onClick":{"kind":"Function","result":"value1changed"},"shouldInvokeFns":true}
{"onClick":{"kind":"Function","result":"value1prop2"},"shouldInvokeFns":true}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.js new file mode 100644 index 0000000000000..eb1b324cc1295 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/jsx-captures-context-variable.js @@ -0,0 +1,40 @@ +import {Stringify, useIdentity} from 'shared-runtime'; + +function Component({prop1, prop2}) { + 'use memo'; + + const data = useIdentity( + new Map([ + [0, 'value0'], + [1, 'value1'], + ]) + ); + let i = 0; + const items = []; + items.push( + data.get(i) + prop1} + shouldInvokeFns={true} + /> + ); + i = i + 1; + items.push( + data.get(i) + prop2} + shouldInvokeFns={true} + /> + ); + return <>{items}; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop1: 'prop1', prop2: 'prop2'}], + sequentialRenders: [ + {prop1: 'prop1', prop2: 'prop2'}, + {prop1: 'prop1', prop2: 'prop2'}, + {prop1: 'changed', prop2: 'prop2'}, + ], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-reassign-shadowed-primitive.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-reassign-shadowed-primitive.expect.md index dcba618e77348..d008f60e8ec75 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-reassign-shadowed-primitive.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/lambda-reassign-shadowed-primitive.expect.md @@ -37,8 +37,7 @@ function Component() { } const x = t0; - let x_0; - x_0 = 56; + let x_0 = 56; const fn = function () { x_0 = 42; }; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mutate-captured-arg-separately.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mutate-captured-arg-separately.expect.md index 990b1bf14713b..f3f21f8f6222b 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mutate-captured-arg-separately.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/mutate-captured-arg-separately.expect.md @@ -33,8 +33,7 @@ function component(a) { m(x); }; - let x; - x = { a }; + let x = { a }; m(x); $[0] = a; $[1] = y; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md index 9ce4a62e710d4..0a151b6ca30d1 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-extended-contextvar-scope.expect.md @@ -65,8 +65,7 @@ function useBar(t0, cond) { } else { t1 = $[0]; } - let x; - x = useIdentity(t1); + let x = useIdentity(t1); if (cond) { x = b; } diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md index ee4e4634cbc21..080cc0a74a609 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useCallback-reordering-deplist-controlflow.expect.md @@ -47,8 +47,7 @@ function Foo(t0) { if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) { const x = [arr1]; - let y; - y = []; + let y = []; getVal1 = _temp; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md index 5fc0ec510b3d1..4c452dfabd1e9 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/preserve-memo-validation/useMemo-reordering-depslist-controlflow.expect.md @@ -47,8 +47,7 @@ function Foo(t0) { if ($[0] !== arr1 || $[1] !== arr2 || $[2] !== foo) { const x = [arr1]; - let y; - y = []; + let y = []; let t2; let t3; if ($[5] === Symbol.for("react.memo_cache_sentinel")) { diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md new file mode 100644 index 0000000000000..cacd564922142 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.expect.md @@ -0,0 +1,108 @@ + +## Input + +```javascript +import {useState, useEffect} from 'react'; +import {invoke, Stringify} from 'shared-runtime'; + +function Content() { + const [announcement, setAnnouncement] = useState(''); + const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]); + + // This was originally passed down as an onClick, but React Compiler's test + // evaluator doesn't yet support events outside of React + useEffect(() => { + if (users.length === 2) { + let removedUserName = ''; + setUsers(prevUsers => { + const newUsers = [...prevUsers]; + removedUserName = newUsers.at(-1).name; + newUsers.pop(); + return newUsers; + }); + + setAnnouncement(`Removed user (${removedUserName})`); + } + }, [users]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Content, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useState, useEffect } from "react"; +import { invoke, Stringify } from "shared-runtime"; + +function Content() { + const $ = _c(8); + const [announcement, setAnnouncement] = useState(""); + let t0; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t0 = [{ name: "John Doe" }, { name: "Jane Doe" }]; + $[0] = t0; + } else { + t0 = $[0]; + } + const [users, setUsers] = useState(t0); + let t1; + if ($[1] !== users.length) { + t1 = () => { + if (users.length === 2) { + let removedUserName = ""; + setUsers((prevUsers) => { + const newUsers = [...prevUsers]; + removedUserName = newUsers.at(-1).name; + newUsers.pop(); + return newUsers; + }); + + setAnnouncement(`Removed user (${removedUserName})`); + } + }; + $[1] = users.length; + $[2] = t1; + } else { + t1 = $[2]; + } + let t2; + if ($[3] !== users) { + t2 = [users]; + $[3] = users; + $[4] = t2; + } else { + t2 = $[4]; + } + useEffect(t1, t2); + let t3; + if ($[5] !== announcement || $[6] !== users) { + t3 = ; + $[5] = announcement; + $[6] = users; + $[7] = t3; + } else { + t3 = $[7]; + } + return t3; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Content, + params: [{}], + sequentialRenders: [{}, {}], +}; + +``` + +### Eval output +(kind: ok)
{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}
+
{"users":[{"name":"John Doe"}],"announcement":"Removed user (Jane Doe)"}
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.js new file mode 100644 index 0000000000000..ea37e07491b2f --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-context-var-reassign-no-scope.js @@ -0,0 +1,31 @@ +import {useState, useEffect} from 'react'; +import {invoke, Stringify} from 'shared-runtime'; + +function Content() { + const [announcement, setAnnouncement] = useState(''); + const [users, setUsers] = useState([{name: 'John Doe'}, {name: 'Jane Doe'}]); + + // This was originally passed down as an onClick, but React Compiler's test + // evaluator doesn't yet support events outside of React + useEffect(() => { + if (users.length === 2) { + let removedUserName = ''; + setUsers(prevUsers => { + const newUsers = [...prevUsers]; + removedUserName = newUsers.at(-1).name; + newUsers.pop(); + return newUsers; + }); + + setAnnouncement(`Removed user (${removedUserName})`); + } + }, [users]); + + return ; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Content, + params: [{}], + sequentialRenders: [{}, {}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-reassigns-context.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-reassigns-context.expect.md index c09eaf060ad47..da6b57defe0b3 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-reassigns-context.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/repro-returned-inner-fn-reassigns-context.expect.md @@ -62,8 +62,7 @@ function Foo(t0) { myVar = _temp; }; - let myVar; - myVar = _temp2; + let myVar = _temp2; useIdentity(); const fn = fnFactory(); diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.expect.md new file mode 100644 index 0000000000000..7a6d633427395 --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.expect.md @@ -0,0 +1,122 @@ + +## Input + +```javascript +import {useEffect, useState} from 'react'; + +/** + * Example of a function expression whose return value shouldn't have + * a "freeze" effect on all operands. + * + * This is because the function expression is passed to `useEffect` and + * thus is not a render function. `cleanedUp` is also created within + * the effect and is not a render variable. + */ +function Component({prop}) { + const [cleanupCount, setCleanupCount] = useState(0); + + useEffect(() => { + let cleanedUp = false; + setTimeout(() => { + if (!cleanedUp) { + cleanedUp = true; + setCleanupCount(c => c + 1); + } + }, 0); + // This return value should not have freeze effects + // on its operands + return () => { + if (!cleanedUp) { + cleanedUp = true; + setCleanupCount(c => c + 1); + } + }; + }, [prop]); + return
{cleanupCount}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop: 5}], + sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}], +}; + +``` + +## Code + +```javascript +import { c as _c } from "react/compiler-runtime"; +import { useEffect, useState } from "react"; + +/** + * Example of a function expression whose return value shouldn't have + * a "freeze" effect on all operands. + * + * This is because the function expression is passed to `useEffect` and + * thus is not a render function. `cleanedUp` is also created within + * the effect and is not a render variable. + */ +function Component(t0) { + const $ = _c(5); + const { prop } = t0; + const [cleanupCount, setCleanupCount] = useState(0); + let t1; + if ($[0] === Symbol.for("react.memo_cache_sentinel")) { + t1 = () => { + let cleanedUp = false; + setTimeout(() => { + if (!cleanedUp) { + cleanedUp = true; + setCleanupCount(_temp); + } + }, 0); + return () => { + if (!cleanedUp) { + cleanedUp = true; + setCleanupCount(_temp2); + } + }; + }; + $[0] = t1; + } else { + t1 = $[0]; + } + let t2; + if ($[1] !== prop) { + t2 = [prop]; + $[1] = prop; + $[2] = t2; + } else { + t2 = $[2]; + } + useEffect(t1, t2); + let t3; + if ($[3] !== cleanupCount) { + t3 =
{cleanupCount}
; + $[3] = cleanupCount; + $[4] = t3; + } else { + t3 = $[4]; + } + return t3; +} +function _temp2(c_0) { + return c_0 + 1; +} +function _temp(c) { + return c + 1; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{ prop: 5 }], + sequentialRenders: [{ prop: 5 }, { prop: 5 }, { prop: 6 }], +}; + +``` + +### Eval output +(kind: ok)
0
+
0
+
1
\ No newline at end of file diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.js b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.js new file mode 100644 index 0000000000000..9910461613ecd --- /dev/null +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-effect-cleanup-reassigns.js @@ -0,0 +1,38 @@ +import {useEffect, useState} from 'react'; + +/** + * Example of a function expression whose return value shouldn't have + * a "freeze" effect on all operands. + * + * This is because the function expression is passed to `useEffect` and + * thus is not a render function. `cleanedUp` is also created within + * the effect and is not a render variable. + */ +function Component({prop}) { + const [cleanupCount, setCleanupCount] = useState(0); + + useEffect(() => { + let cleanedUp = false; + setTimeout(() => { + if (!cleanedUp) { + cleanedUp = true; + setCleanupCount(c => c + 1); + } + }, 0); + // This return value should not have freeze effects + // on its operands + return () => { + if (!cleanedUp) { + cleanedUp = true; + setCleanupCount(c => c + 1); + } + }; + }, [prop]); + return
{cleanupCount}
; +} + +export const FIXTURE_ENTRYPOINT = { + fn: Component, + params: [{prop: 5}], + sequentialRenders: [{prop: 5}, {prop: 5}, {prop: 6}], +}; diff --git a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md index 6ad460347fa50..e00a5648164a4 100644 --- a/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md +++ b/compiler/packages/babel-plugin-react-compiler/src/__tests__/fixtures/compiler/use-operator-conditional.expect.md @@ -79,8 +79,7 @@ function Component(props) { function Inner(props) { const $ = _c(7); - let input; - input = null; + let input = null; if (props.cond) { input = use(FooContext); }